wagent 1.0.0
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/README.md +103 -0
- package/dist/channels/baileys/baileys.adapter.js +992 -0
- package/dist/channels/baileys/baileys.auth.js +96 -0
- package/dist/channels/baileys/baileys.events.js +284 -0
- package/dist/channels/baileys/baileys.version.js +86 -0
- package/dist/channels/channel.interface.js +6 -0
- package/dist/config.js +120 -0
- package/dist/constants.js +59 -0
- package/dist/db/client.js +160 -0
- package/dist/db/schema.js +189 -0
- package/dist/index.js +873 -0
- package/dist/services/instance-manager.js +185 -0
- package/dist/types/channel.types.js +4 -0
- package/dist/types/db.types.js +4 -0
- package/dist/utils/logger.js +39 -0
- package/package.json +43 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { db } from "../db/client.js";
|
|
4
|
+
import { instances, authKeys, queueStats } from "../db/schema.js";
|
|
5
|
+
import { BaileysAdapter } from "../channels/baileys/baileys.adapter.js";
|
|
6
|
+
import { INSTANCE_ID_PREFIX } from "../constants.js";
|
|
7
|
+
import { createChildLogger } from "../utils/logger.js";
|
|
8
|
+
const logger = createChildLogger({ service: "instance-manager" });
|
|
9
|
+
export class InstanceManager {
|
|
10
|
+
adapters = new Map();
|
|
11
|
+
globalHandlers = new Set();
|
|
12
|
+
/** Register a global event handler that receives all instance events */
|
|
13
|
+
onAnyEvent(handler) {
|
|
14
|
+
this.globalHandlers.add(handler);
|
|
15
|
+
}
|
|
16
|
+
/** Initialize: load existing instances from DB and reconnect those with status "open"/connected */
|
|
17
|
+
async init() {
|
|
18
|
+
const rows = db.select().from(instances).all();
|
|
19
|
+
for (const row of rows) {
|
|
20
|
+
if (row.status === "connected" || row.status === "connecting") {
|
|
21
|
+
try {
|
|
22
|
+
const adapter = this.createAdapter(row.id, row.channel);
|
|
23
|
+
this.adapters.set(row.id, { adapter, channel: row.channel });
|
|
24
|
+
this.bindAdapterEvents(row.id, adapter);
|
|
25
|
+
await adapter.connect();
|
|
26
|
+
logger.info({ instanceId: row.id }, "Reconnected instance on startup");
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
logger.error({ instanceId: row.id, err }, "Failed to reconnect instance on startup");
|
|
30
|
+
db.update(instances)
|
|
31
|
+
.set({ status: "disconnected", updatedAt: Date.now() })
|
|
32
|
+
.where(eq(instances.id, row.id))
|
|
33
|
+
.run();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// ---- Instance CRUD ----
|
|
39
|
+
async createInstance(name, channel = "baileys") {
|
|
40
|
+
const id = `${INSTANCE_ID_PREFIX}${randomUUID().slice(0, 8)}`;
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
db.insert(instances)
|
|
43
|
+
.values({
|
|
44
|
+
id,
|
|
45
|
+
name,
|
|
46
|
+
channel,
|
|
47
|
+
status: "disconnected",
|
|
48
|
+
createdAt: now,
|
|
49
|
+
updatedAt: now,
|
|
50
|
+
})
|
|
51
|
+
.run();
|
|
52
|
+
db.insert(queueStats)
|
|
53
|
+
.values({ instanceId: id, messagesSent: 0, messagesFailed: 0 })
|
|
54
|
+
.onConflictDoNothing()
|
|
55
|
+
.run();
|
|
56
|
+
return this.getInstanceFromDb(id);
|
|
57
|
+
}
|
|
58
|
+
async connectInstance(id) {
|
|
59
|
+
const row = this.getInstanceFromDb(id);
|
|
60
|
+
let managed = this.adapters.get(id);
|
|
61
|
+
if (!managed) {
|
|
62
|
+
const adapter = this.createAdapter(id, row.channel);
|
|
63
|
+
managed = { adapter, channel: row.channel };
|
|
64
|
+
this.adapters.set(id, managed);
|
|
65
|
+
this.bindAdapterEvents(id, adapter);
|
|
66
|
+
}
|
|
67
|
+
db.update(instances)
|
|
68
|
+
.set({ status: "connecting", updatedAt: Date.now() })
|
|
69
|
+
.where(eq(instances.id, id))
|
|
70
|
+
.run();
|
|
71
|
+
await managed.adapter.connect();
|
|
72
|
+
}
|
|
73
|
+
async disconnectInstance(id) {
|
|
74
|
+
const managed = this.adapters.get(id);
|
|
75
|
+
if (managed) {
|
|
76
|
+
await managed.adapter.disconnect();
|
|
77
|
+
}
|
|
78
|
+
db.update(instances)
|
|
79
|
+
.set({
|
|
80
|
+
status: "disconnected",
|
|
81
|
+
lastDisconnected: Date.now(),
|
|
82
|
+
updatedAt: Date.now(),
|
|
83
|
+
})
|
|
84
|
+
.where(eq(instances.id, id))
|
|
85
|
+
.run();
|
|
86
|
+
}
|
|
87
|
+
async deleteInstance(id) {
|
|
88
|
+
const managed = this.adapters.get(id);
|
|
89
|
+
if (managed) {
|
|
90
|
+
await managed.adapter.disconnect();
|
|
91
|
+
this.adapters.delete(id);
|
|
92
|
+
}
|
|
93
|
+
db.delete(authKeys).where(eq(authKeys.instanceId, id)).run();
|
|
94
|
+
db.delete(instances).where(eq(instances.id, id)).run();
|
|
95
|
+
}
|
|
96
|
+
async restartInstance(id) {
|
|
97
|
+
await this.disconnectInstance(id);
|
|
98
|
+
this.adapters.delete(id);
|
|
99
|
+
await this.connectInstance(id);
|
|
100
|
+
}
|
|
101
|
+
// ---- Adapter access ----
|
|
102
|
+
getAdapter(id) {
|
|
103
|
+
const managed = this.adapters.get(id);
|
|
104
|
+
if (!managed) {
|
|
105
|
+
throw new Error(`Instance ${id} not found or not initialized`);
|
|
106
|
+
}
|
|
107
|
+
return managed.adapter;
|
|
108
|
+
}
|
|
109
|
+
getInstanceChannel(id) {
|
|
110
|
+
const managed = this.adapters.get(id);
|
|
111
|
+
if (managed)
|
|
112
|
+
return managed.channel;
|
|
113
|
+
const row = this.getInstanceFromDb(id);
|
|
114
|
+
return row.channel;
|
|
115
|
+
}
|
|
116
|
+
// ---- DB reads ----
|
|
117
|
+
getAllInstances() {
|
|
118
|
+
return db.select().from(instances).all();
|
|
119
|
+
}
|
|
120
|
+
getInstance(id) {
|
|
121
|
+
return this.getInstanceFromDb(id);
|
|
122
|
+
}
|
|
123
|
+
getInstanceFromDb(id) {
|
|
124
|
+
const row = db.select().from(instances).where(eq(instances.id, id)).get();
|
|
125
|
+
if (!row) {
|
|
126
|
+
throw new Error(`Instance ${id} not found`);
|
|
127
|
+
}
|
|
128
|
+
return row;
|
|
129
|
+
}
|
|
130
|
+
// ---- Private helpers ----
|
|
131
|
+
createAdapter(instanceId, channel) {
|
|
132
|
+
if (channel !== "baileys") {
|
|
133
|
+
throw new Error(`Only baileys channel is supported in wagent`);
|
|
134
|
+
}
|
|
135
|
+
return new BaileysAdapter(instanceId);
|
|
136
|
+
}
|
|
137
|
+
bindAdapterEvents(instanceId, adapter) {
|
|
138
|
+
const events = [
|
|
139
|
+
"message.received",
|
|
140
|
+
"message.updated",
|
|
141
|
+
"message.deleted",
|
|
142
|
+
"message.reaction",
|
|
143
|
+
"message.edited",
|
|
144
|
+
"presence.updated",
|
|
145
|
+
"chat.updated",
|
|
146
|
+
"group.updated",
|
|
147
|
+
"group.participants_changed",
|
|
148
|
+
"contact.updated",
|
|
149
|
+
"connection.changed",
|
|
150
|
+
"call.received",
|
|
151
|
+
];
|
|
152
|
+
for (const event of events) {
|
|
153
|
+
adapter.on(event, (payload) => {
|
|
154
|
+
if (event === "connection.changed") {
|
|
155
|
+
const connPayload = payload;
|
|
156
|
+
const statusMap = {
|
|
157
|
+
open: "connected",
|
|
158
|
+
close: "disconnected",
|
|
159
|
+
connecting: "connecting",
|
|
160
|
+
};
|
|
161
|
+
const dbStatus = statusMap[connPayload.status] ?? "disconnected";
|
|
162
|
+
const updateFields = {
|
|
163
|
+
status: dbStatus,
|
|
164
|
+
updatedAt: Date.now(),
|
|
165
|
+
};
|
|
166
|
+
if (connPayload.status === "open") {
|
|
167
|
+
updateFields.lastConnected = Date.now();
|
|
168
|
+
}
|
|
169
|
+
if (connPayload.status === "close") {
|
|
170
|
+
updateFields.lastDisconnected = Date.now();
|
|
171
|
+
}
|
|
172
|
+
db.update(instances).set(updateFields).where(eq(instances.id, instanceId)).run();
|
|
173
|
+
}
|
|
174
|
+
for (const handler of this.globalHandlers) {
|
|
175
|
+
try {
|
|
176
|
+
handler(event, instanceId, payload);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
logger.error({ err, event, instanceId }, "Error in global event handler");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// WA MCP — Structured Logger with Request Tracing
|
|
3
|
+
// ============================================================
|
|
4
|
+
import pino from "pino";
|
|
5
|
+
import { SERVER_NAME, DEFAULT_LOG_LEVEL } from "../constants.js";
|
|
6
|
+
const logLevel = process.argv.includes("--debug")
|
|
7
|
+
? "debug"
|
|
8
|
+
: process.argv.includes("--verbose")
|
|
9
|
+
? "info"
|
|
10
|
+
: DEFAULT_LOG_LEVEL;
|
|
11
|
+
const isStdio = process.env.WA_TRANSPORT === "stdio";
|
|
12
|
+
export const logger = pino({
|
|
13
|
+
name: SERVER_NAME,
|
|
14
|
+
level: logLevel,
|
|
15
|
+
transport: process.env.NODE_ENV !== "production"
|
|
16
|
+
? { target: "pino-pretty", options: { colorize: true, destination: isStdio ? 2 : 1 } }
|
|
17
|
+
: undefined,
|
|
18
|
+
redact: [
|
|
19
|
+
"headers.authorization",
|
|
20
|
+
"*.accessToken",
|
|
21
|
+
"*.cloudAccessToken",
|
|
22
|
+
"*.cloud_access_token",
|
|
23
|
+
"*.apiKey",
|
|
24
|
+
"params.accessToken",
|
|
25
|
+
"params.apiKey",
|
|
26
|
+
"req.headers.authorization",
|
|
27
|
+
"req.headers['x-api-key']",
|
|
28
|
+
],
|
|
29
|
+
}, pino.destination(isStdio ? 2 : 1));
|
|
30
|
+
export function createChildLogger(context) {
|
|
31
|
+
return logger.child(context);
|
|
32
|
+
}
|
|
33
|
+
export function createRequestLogger(toolName, instanceId) {
|
|
34
|
+
return logger.child({
|
|
35
|
+
requestId: crypto.randomUUID(),
|
|
36
|
+
tool: toolName,
|
|
37
|
+
...(instanceId ? { instanceId } : {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wagent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "WhatsApp Agent — AI-powered WhatsApp assistant using WAMCP + Groq",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"wagent": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["dist", "README.md"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"wagent": "tsx src/index.ts",
|
|
13
|
+
"dev": "tsx watch src/index.ts",
|
|
14
|
+
"typecheck": "tsc --noEmit"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
|
21
|
+
"better-sqlite3": "^11.8.2",
|
|
22
|
+
"drizzle-orm": "^0.39.3",
|
|
23
|
+
"pino": "^9.6.0",
|
|
24
|
+
"pino-pretty": "^13.0.0",
|
|
25
|
+
"zod": "^3.24.2",
|
|
26
|
+
"ai": "^6.0.208",
|
|
27
|
+
"@ai-sdk/google": "^3.0.70",
|
|
28
|
+
"@ai-sdk/openai-compatible": "^2.0.51",
|
|
29
|
+
"qrcode-terminal": "^0.12.0",
|
|
30
|
+
"@hapi/boom": "^10.0.1"
|
|
31
|
+
},
|
|
32
|
+
"pnpm": {
|
|
33
|
+
"onlyBuiltDependencies": ["better-sqlite3", "@whiskeysockets/baileys", "protobufjs", "esbuild"]
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.13.5",
|
|
37
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
38
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
39
|
+
"@types/hapi__boom": "^9.0.1",
|
|
40
|
+
"tsx": "^4.19.3",
|
|
41
|
+
"typescript": "^5.7.3"
|
|
42
|
+
}
|
|
43
|
+
}
|