levix-bot 2.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/LICENSE +21 -0
- package/README.md +105 -0
- package/SETUP.md +242 -0
- package/app.cjs +337 -0
- package/bin/levix.js +7 -0
- package/deploy/install.sh +182 -0
- package/deploy/levix.service +56 -0
- package/deploy/nginx/levix.leviro.net.conf +79 -0
- package/package.json +78 -0
- package/public/brand/apple-touch-icon.png +0 -0
- package/public/brand/banner.jpg +0 -0
- package/public/brand/banner.webp +0 -0
- package/public/brand/icon-16.png +0 -0
- package/public/brand/icon-192.png +0 -0
- package/public/brand/icon-32.png +0 -0
- package/public/brand/icon-512.png +0 -0
- package/public/brand/icon-512.webp +0 -0
- package/public/brand/mark.png +0 -0
- package/public/brand/mark.svg +16 -0
- package/public/brand/mark.webp +0 -0
- package/public/brand/wordmark.png +0 -0
- package/public/brand/wordmark.webp +0 -0
- package/public/dashboard.css +881 -0
- package/public/dashboard.js +1057 -0
- package/public/logo.svg +16 -0
- package/public/qrcode.min.js +1 -0
- package/public/socket.io.min.js +7 -0
- package/scheduler.cjs +197 -0
- package/src/auth/auth-storage.cjs +74 -0
- package/src/auth/use-database-auth-state.js +130 -0
- package/src/bootstrap/core.js +74 -0
- package/src/bootstrap/events.cjs +43 -0
- package/src/bootstrap/panel.js +97 -0
- package/src/cli.js +153 -0
- package/src/commands/autoschedule.cjs +84 -0
- package/src/commands/blacklist.cjs +116 -0
- package/src/commands/block.cjs +70 -0
- package/src/commands/calc.cjs +387 -0
- package/src/commands/debt.cjs +175 -0
- package/src/commands/deleteschedule.cjs +36 -0
- package/src/commands/gemini.cjs +727 -0
- package/src/commands/group/add.cjs +136 -0
- package/src/commands/group/all.cjs +43 -0
- package/src/commands/group/antiSpam.cjs +96 -0
- package/src/commands/group/antilink.cjs +167 -0
- package/src/commands/group/approveall.cjs +54 -0
- package/src/commands/group/clearwarns.cjs +41 -0
- package/src/commands/group/deletenote.cjs +33 -0
- package/src/commands/group/demote.cjs +62 -0
- package/src/commands/group/kick.cjs +85 -0
- package/src/commands/group/media.cjs +120 -0
- package/src/commands/group/members.cjs +59 -0
- package/src/commands/group/note.cjs +32 -0
- package/src/commands/group/promote.cjs +60 -0
- package/src/commands/group/removeall.cjs +42 -0
- package/src/commands/group/rules.cjs +31 -0
- package/src/commands/group/save.cjs +36 -0
- package/src/commands/group/setname.cjs +41 -0
- package/src/commands/group/setpp.cjs +51 -0
- package/src/commands/group/setrules.cjs +46 -0
- package/src/commands/group/setwarn.cjs +58 -0
- package/src/commands/group/tagadmins.cjs +50 -0
- package/src/commands/group/warn.cjs +105 -0
- package/src/commands/group/warns.cjs +66 -0
- package/src/commands/group/welcome.cjs +109 -0
- package/src/commands/group.cjs +160 -0
- package/src/commands/help.cjs +230 -0
- package/src/commands/listschedules.cjs +47 -0
- package/src/commands/loop.cjs +163 -0
- package/src/commands/memory.cjs +201 -0
- package/src/commands/mod.cjs +155 -0
- package/src/commands/notes.cjs +28 -0
- package/src/commands/perm.cjs +236 -0
- package/src/commands/ping.cjs +17 -0
- package/src/commands/poll.cjs +80 -0
- package/src/commands/prayer.cjs +64 -0
- package/src/commands/qr.cjs +39 -0
- package/src/commands/rand.cjs +130 -0
- package/src/commands/restart.cjs +25 -0
- package/src/commands/schedule.cjs +86 -0
- package/src/commands/score.cjs +130 -0
- package/src/commands/setprefix.cjs +40 -0
- package/src/commands/shortlink.cjs +74 -0
- package/src/commands/shutdown.cjs +45 -0
- package/src/commands/status.cjs +75 -0
- package/src/commands/stt.cjs +150 -0
- package/src/commands/todo.cjs +88 -0
- package/src/commands/tts.cjs +170 -0
- package/src/commands/unblock.cjs +57 -0
- package/src/commands/weather.cjs +79 -0
- package/src/config/ai-persona.md +17 -0
- package/src/config/baileys.config.js +51 -0
- package/src/config/brand.cjs +36 -0
- package/src/config/brand.esm.js +17 -0
- package/src/config/constants.js +16 -0
- package/src/config/defaults.cjs +91 -0
- package/src/config/lock.cjs +110 -0
- package/src/config/paths.cjs +90 -0
- package/src/config/runtime-config.cjs +219 -0
- package/src/config/secrets.cjs +130 -0
- package/src/config/settings.cjs +386 -0
- package/src/core/connection.js +199 -0
- package/src/core/events.js +61 -0
- package/src/core/socket.js +64 -0
- package/src/db/db.cjs +282 -0
- package/src/db/store.cjs +837 -0
- package/src/db/store.esm.js +17 -0
- package/src/domain/caddy.js +120 -0
- package/src/domain/command.js +380 -0
- package/src/domain/detect.js +173 -0
- package/src/domain/nginx.js +138 -0
- package/src/domain/system.js +148 -0
- package/src/handlers/command.handler.js +414 -0
- package/src/handlers/group.handler.js +73 -0
- package/src/handlers/message.handler.js +149 -0
- package/src/index.js +217 -0
- package/src/middleware/antispam.middleware.js +68 -0
- package/src/middleware/blacklist.middleware.js +29 -0
- package/src/middleware/forward-tracking.middleware.js +49 -0
- package/src/middleware/permissions.middleware.js +114 -0
- package/src/routes/dashboard.api.esm.js +779 -0
- package/src/services/aiAgent.cjs +317 -0
- package/src/services/aiTools.cjs +635 -0
- package/src/utils/datetime.cjs +61 -0
- package/src/utils/lid-helper.esm.js +143 -0
- package/src/utils/logger.cjs +72 -0
- package/src/utils/memory.cjs +385 -0
- package/src/utils/normalizeJid.cjs +65 -0
- package/src/utils/normalizeJid.esm.js +37 -0
- package/src/utils/openBrowser.cjs +129 -0
- package/src/utils/permissions.cjs +154 -0
- package/src/utils/permissions.esm.js +375 -0
- package/src/utils/recentMessageCache.esm.js +70 -0
- package/src/utils/requestOrigin.cjs +102 -0
- package/src/utils/sendBotMessage.cjs +43 -0
- package/src/utils/sendBotMessage.esm.js +139 -0
- package/src/utils/statusMessage.cjs +179 -0
- package/src/utils/storage-hub.cjs +49 -0
- package/src/utils/storage-hub.esm.js +24 -0
- package/src/utils/storage.cjs +80 -0
- package/src/utils/storage.esm.js +88 -0
- package/src/utils/textDecode.cjs +197 -0
- package/src/utils/thumbnail.cjs +591 -0
- package/src/utils/typing.esm.js +102 -0
- package/views/dashboard.ejs +329 -0
- package/views/login.ejs +105 -0
- package/views/qr.ejs +108 -0
- package/views/setup.ejs +122 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Baileys v7 LID Helper Functions
|
|
2
|
+
// These functions help work with the new LID system
|
|
3
|
+
|
|
4
|
+
import { getLidForPn, getPnForLid } from '../utils/storage.esm.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the preferred identifier for a user (LID if available, otherwise PN)
|
|
8
|
+
* @param {Object} sock - WhatsApp socket instance
|
|
9
|
+
* @param {string} pn - Phone number JID (e.g., "1234567890@s.whatsapp.net")
|
|
10
|
+
* @returns {Promise<string>} - LID if available, otherwise the original PN
|
|
11
|
+
*/
|
|
12
|
+
export async function getPreferredJid(sock, pn) {
|
|
13
|
+
if (!pn || !pn.includes('@s.whatsapp.net')) {
|
|
14
|
+
return pn; // Not a phone number, return as-is
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// First check database
|
|
19
|
+
const lidFromDb = getLidForPn(pn);
|
|
20
|
+
if (lidFromDb) {
|
|
21
|
+
return lidFromDb;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Try to get from Baileys' internal store if available
|
|
25
|
+
if (sock.signalRepository?.lidMapping) {
|
|
26
|
+
const lid = await sock.signalRepository.lidMapping.getLIDForPN(pn);
|
|
27
|
+
if (lid) {
|
|
28
|
+
return lid;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fallback to PN
|
|
33
|
+
return pn;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// If any error, return the original PN
|
|
36
|
+
return pn;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert LID to PN if needed
|
|
42
|
+
* @param {Object} sock - WhatsApp socket instance
|
|
43
|
+
* @param {string} lid - LID JID (e.g., "lid@lid")
|
|
44
|
+
* @returns {Promise<string>} - PN if found, otherwise the original LID
|
|
45
|
+
*/
|
|
46
|
+
export async function lidToPn(sock, lid) {
|
|
47
|
+
if (!lid || !lid.includes('@lid')) {
|
|
48
|
+
return lid; // Not a LID, return as-is
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// First check database
|
|
53
|
+
const pnFromDb = getPnForLid(lid);
|
|
54
|
+
if (pnFromDb) {
|
|
55
|
+
return pnFromDb;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Try to get from Baileys' internal store if available
|
|
59
|
+
if (sock.signalRepository?.lidMapping) {
|
|
60
|
+
const pn = await sock.signalRepository.lidMapping.getPNForLID(lid);
|
|
61
|
+
if (pn) {
|
|
62
|
+
return pn;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Fallback to LID
|
|
67
|
+
return lid;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return lid;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get user identifier from message key
|
|
75
|
+
* Returns LID if available, otherwise PN
|
|
76
|
+
* @param {Object} sock - WhatsApp socket instance
|
|
77
|
+
* @param {Object} msgKey - Message key object
|
|
78
|
+
* @returns {Promise<string>} - User identifier (LID or PN)
|
|
79
|
+
*/
|
|
80
|
+
export async function getUserIdFromKey(sock, msgKey) {
|
|
81
|
+
const isGroup = msgKey.remoteJid?.endsWith('@g.us');
|
|
82
|
+
|
|
83
|
+
if (isGroup) {
|
|
84
|
+
// In groups, use participant field
|
|
85
|
+
const participantId = msgKey.participant || msgKey.remoteJid;
|
|
86
|
+
|
|
87
|
+
// V7: Check for participantAlt (the alternate JID)
|
|
88
|
+
if (msgKey.participantAlt) {
|
|
89
|
+
// If we have participantAlt, the participant is likely a LID
|
|
90
|
+
// Return the LID for privacy
|
|
91
|
+
return participantId;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return participantId;
|
|
95
|
+
} else {
|
|
96
|
+
// In DMs, use remoteJid
|
|
97
|
+
const remoteId = msgKey.remoteJid;
|
|
98
|
+
|
|
99
|
+
// V7: Check for remoteJidAlt
|
|
100
|
+
if (msgKey.remoteJidAlt) {
|
|
101
|
+
// If we have remoteJidAlt, the remoteJid is likely a LID
|
|
102
|
+
return remoteId;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return remoteId;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Format user mention using LID when available
|
|
111
|
+
* @param {Object} sock - WhatsApp socket instance
|
|
112
|
+
* @param {string} jid - User JID (LID or PN)
|
|
113
|
+
* @returns {Promise<Object>} - Object with mention text and mentions array
|
|
114
|
+
*/
|
|
115
|
+
export async function formatUserMention(sock, jid) {
|
|
116
|
+
// Extract the number/ID part for display
|
|
117
|
+
const displayId = jid.split('@')[0];
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
text: `@${displayId}`,
|
|
121
|
+
mentions: [jid]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve multiple JIDs to their preferred format (LID when available)
|
|
127
|
+
* @param {Object} sock - WhatsApp socket instance
|
|
128
|
+
* @param {Array<string>} jids - Array of JIDs
|
|
129
|
+
* @returns {Promise<Array<string>>} - Array of resolved JIDs
|
|
130
|
+
*/
|
|
131
|
+
export async function resolveJids(sock, jids) {
|
|
132
|
+
const resolved = [];
|
|
133
|
+
|
|
134
|
+
for (const jid of jids) {
|
|
135
|
+
if (jid.includes('@s.whatsapp.net')) {
|
|
136
|
+
resolved.push(await getPreferredJid(sock, jid));
|
|
137
|
+
} else {
|
|
138
|
+
resolved.push(jid);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return resolved;
|
|
143
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const pino = require("pino");
|
|
3
|
+
|
|
4
|
+
const { ensureDataDir } = require("../config/paths.cjs");
|
|
5
|
+
|
|
6
|
+
// Logs go in the data directory with everything else the bot writes, so a
|
|
7
|
+
// global install never tries to write next to its own code.
|
|
8
|
+
const logDir = ensureDataDir("logs");
|
|
9
|
+
|
|
10
|
+
// Pino's `transport` option runs each target in a worker thread, and a worker
|
|
11
|
+
// loads its target by module path — which a single-file executable can't
|
|
12
|
+
// provide. Same three destinations either way; packaged builds just wire them
|
|
13
|
+
// as plain streams in this thread.
|
|
14
|
+
function isPackaged() {
|
|
15
|
+
try {
|
|
16
|
+
return require("node:sea").isSea() === true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const base = {
|
|
23
|
+
level: "trace",
|
|
24
|
+
serializers: {
|
|
25
|
+
err: pino.stdSerializers.err,
|
|
26
|
+
error: pino.stdSerializers.err,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const PRETTY = {
|
|
31
|
+
colorize: true,
|
|
32
|
+
translateTime: "SYS:dd-mm-yyyy HH:MM:ss",
|
|
33
|
+
ignore: "pid,hostname",
|
|
34
|
+
messageFormat: "{msg}",
|
|
35
|
+
errorLikeObjectKeys: ["err", "error"],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const combined = path.join(logDir, "combined.log");
|
|
39
|
+
const errors = path.join(logDir, "error.log");
|
|
40
|
+
|
|
41
|
+
const logger = isPackaged()
|
|
42
|
+
? pino(
|
|
43
|
+
base,
|
|
44
|
+
pino.multistream([
|
|
45
|
+
{ level: "trace", stream: pino.destination({ dest: combined, mkdir: true }) },
|
|
46
|
+
{ level: "error", stream: pino.destination({ dest: errors, mkdir: true }) },
|
|
47
|
+
{ level: "trace", stream: require("pino-pretty")(PRETTY) },
|
|
48
|
+
])
|
|
49
|
+
)
|
|
50
|
+
: pino({
|
|
51
|
+
...base,
|
|
52
|
+
transport: {
|
|
53
|
+
targets: [
|
|
54
|
+
// Combined log file (all levels)
|
|
55
|
+
{
|
|
56
|
+
target: "pino/file",
|
|
57
|
+
level: "trace",
|
|
58
|
+
options: { destination: combined, mkdir: true },
|
|
59
|
+
},
|
|
60
|
+
// Error log file (errors only)
|
|
61
|
+
{
|
|
62
|
+
target: "pino/file",
|
|
63
|
+
level: "error",
|
|
64
|
+
options: { destination: errors, mkdir: true },
|
|
65
|
+
},
|
|
66
|
+
// Pretty console output
|
|
67
|
+
{ target: "pino-pretty", level: "trace", options: PRETTY },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
module.exports = logger;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// Long-term memory for the AI, stored as plain Markdown files.
|
|
2
|
+
//
|
|
3
|
+
// Why files and not a table
|
|
4
|
+
// -------------------------
|
|
5
|
+
// The operator asked for "save it to your memory" to end up in a `.md` file
|
|
6
|
+
// per chat (plus one global file) — the same idea as an agent's CLAUDE.md.
|
|
7
|
+
// Markdown keeps the memory:
|
|
8
|
+
// * human readable and hand-editable (open the file, fix a line, done),
|
|
9
|
+
// * diffable / backup-able without a DB dump,
|
|
10
|
+
// * trivially injectable into the model's system prompt.
|
|
11
|
+
//
|
|
12
|
+
// Layout (root overridable with MEMORY_DIR):
|
|
13
|
+
//
|
|
14
|
+
// memory/
|
|
15
|
+
// global.md <- shared by every chat
|
|
16
|
+
// chats/
|
|
17
|
+
// 201234567890@s.whatsapp.net.md
|
|
18
|
+
// 120363xxxxxxxxx@g.us.md
|
|
19
|
+
//
|
|
20
|
+
// File shape:
|
|
21
|
+
//
|
|
22
|
+
// # 🧠 ذاكرة ...
|
|
23
|
+
// <!-- chat: <jid> -->
|
|
24
|
+
// ...free-form prose the human wrote (never touched by the bot)...
|
|
25
|
+
// - fact one <!-- id:m1a2b3 at:2026-08-21T10:00:00.000Z by:فلان -->
|
|
26
|
+
// - fact two
|
|
27
|
+
//
|
|
28
|
+
// Every line starting with `-` (or `*`) is one memory entry; indented lines
|
|
29
|
+
// continue the entry above. Metadata rides in a trailing HTML comment, which
|
|
30
|
+
// Markdown renderers hide and humans can delete without breaking anything —
|
|
31
|
+
// entries without it still load fine.
|
|
32
|
+
//
|
|
33
|
+
// CommonJS because the commands and the AI agent are.
|
|
34
|
+
|
|
35
|
+
const fs = require("fs");
|
|
36
|
+
const path = require("path");
|
|
37
|
+
|
|
38
|
+
const logger = require("./logger.cjs");
|
|
39
|
+
|
|
40
|
+
const { dataPath, PACKAGE_ROOT } = require("../config/paths.cjs");
|
|
41
|
+
|
|
42
|
+
// The memory files live in the data directory. An install that predates that
|
|
43
|
+
// keeps using its existing ./memory folder rather than silently starting empty.
|
|
44
|
+
const LEGACY_ROOT = path.join(PACKAGE_ROOT, "memory");
|
|
45
|
+
const ROOT = fs.existsSync(LEGACY_ROOT) ? LEGACY_ROOT : dataPath("memory");
|
|
46
|
+
const CHATS_DIR = path.join(ROOT, "chats");
|
|
47
|
+
const GLOBAL_FILE = path.join(ROOT, "global.md");
|
|
48
|
+
|
|
49
|
+
// Guardrails so a runaway agent can't grow the prompt without bound.
|
|
50
|
+
const MAX_ENTRY_CHARS = 2000;
|
|
51
|
+
// Read per call so the dashboard can change it without a restart.
|
|
52
|
+
const settings = require("../config/settings.cjs");
|
|
53
|
+
const maxContextChars = () => settings.get("memory_context_chars");
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// paths
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
function ensureDirs() {
|
|
60
|
+
fs.mkdirSync(CHATS_DIR, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `120363@g.us` -> a filename that is safe on every OS we might run on. */
|
|
64
|
+
function safeFileName(chatId) {
|
|
65
|
+
return String(chatId || "unknown")
|
|
66
|
+
.replace(/[^A-Za-z0-9._@-]+/g, "_")
|
|
67
|
+
.slice(0, 120);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeScope(scope) {
|
|
71
|
+
const value = String(scope || "chat").toLowerCase();
|
|
72
|
+
if (["global", "عام", "all", "عالمي", "shared"].includes(value)) return "global";
|
|
73
|
+
return "chat";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Absolute path of the markdown file backing a scope. */
|
|
77
|
+
function memoryFilePath(scope, chatId) {
|
|
78
|
+
if (normalizeScope(scope) === "global") return GLOBAL_FILE;
|
|
79
|
+
if (!chatId) return GLOBAL_FILE;
|
|
80
|
+
return path.join(CHATS_DIR, `${safeFileName(chatId)}.md`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function headerFor(scope, chatId, chatName) {
|
|
84
|
+
if (normalizeScope(scope) === "global") {
|
|
85
|
+
return [
|
|
86
|
+
"# 🧠 الذاكرة العامة",
|
|
87
|
+
"",
|
|
88
|
+
"> معلومات محفوظة لكل المحادثات. ملف Markdown عادي — عدّله بإيدك وقت ما تحب.",
|
|
89
|
+
"> كل سطر بيبدأ بـ `-` هو معلومة محفوظة.",
|
|
90
|
+
"",
|
|
91
|
+
"",
|
|
92
|
+
].join("\n");
|
|
93
|
+
}
|
|
94
|
+
return [
|
|
95
|
+
"# 🧠 ذاكرة المحادثة",
|
|
96
|
+
"",
|
|
97
|
+
`<!-- chat: ${chatId || "unknown"} -->`,
|
|
98
|
+
chatName ? `<!-- name: ${chatName} -->` : null,
|
|
99
|
+
"",
|
|
100
|
+
"> معلومات محفوظة لهذه المحادثة فقط. ملف Markdown عادي — عدّله بإيدك وقت ما تحب.",
|
|
101
|
+
"> كل سطر بيبدأ بـ `-` هو معلومة محفوظة.",
|
|
102
|
+
"",
|
|
103
|
+
"",
|
|
104
|
+
]
|
|
105
|
+
.filter((line) => line !== null)
|
|
106
|
+
.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readRaw(filePath) {
|
|
110
|
+
try {
|
|
111
|
+
return fs.readFileSync(filePath, "utf8");
|
|
112
|
+
} catch {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function writeRaw(filePath, content) {
|
|
118
|
+
ensureDirs();
|
|
119
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// parsing
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const BULLET_RE = /^\s{0,3}[-*]\s+(.*)$/;
|
|
127
|
+
const META_RE = /\s*<!--\s*(id:[^>]*?)\s*-->\s*$/;
|
|
128
|
+
|
|
129
|
+
function parseMeta(line) {
|
|
130
|
+
const match = line.match(META_RE);
|
|
131
|
+
if (!match) return { text: line, meta: {} };
|
|
132
|
+
const meta = {};
|
|
133
|
+
for (const pair of match[1].split(/\s+/)) {
|
|
134
|
+
const idx = pair.indexOf(":");
|
|
135
|
+
if (idx > 0) meta[pair.slice(0, idx)] = pair.slice(idx + 1);
|
|
136
|
+
}
|
|
137
|
+
return { text: line.replace(META_RE, ""), meta };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Every entry in a memory file, in file order.
|
|
142
|
+
* @returns {Array<{id:string, at:string, by:string, content:string, start:number, end:number}>}
|
|
143
|
+
*/
|
|
144
|
+
function parseEntries(raw) {
|
|
145
|
+
const lines = raw.split("\n");
|
|
146
|
+
const entries = [];
|
|
147
|
+
let current = null;
|
|
148
|
+
|
|
149
|
+
const flush = () => {
|
|
150
|
+
if (!current) return;
|
|
151
|
+
current.content = current.content.trim();
|
|
152
|
+
if (current.content) entries.push(current);
|
|
153
|
+
current = null;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
lines.forEach((line, index) => {
|
|
157
|
+
const bullet = line.match(BULLET_RE);
|
|
158
|
+
if (bullet) {
|
|
159
|
+
flush();
|
|
160
|
+
const { text, meta } = parseMeta(bullet[1]);
|
|
161
|
+
current = {
|
|
162
|
+
id: meta.id || `m${index}`,
|
|
163
|
+
at: meta.at || "",
|
|
164
|
+
by: meta.by ? meta.by.replace(/_/g, " ") : "",
|
|
165
|
+
content: text.trim(),
|
|
166
|
+
start: index,
|
|
167
|
+
end: index,
|
|
168
|
+
};
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Continuation of the previous bullet (indented, non-empty).
|
|
172
|
+
if (current && /^\s{2,}\S/.test(line)) {
|
|
173
|
+
const { text } = parseMeta(line);
|
|
174
|
+
current.content += `\n${text.trim()}`;
|
|
175
|
+
current.end = index;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
flush();
|
|
179
|
+
});
|
|
180
|
+
flush();
|
|
181
|
+
|
|
182
|
+
return entries;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// public API
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
function newId() {
|
|
190
|
+
return `m${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** All entries for a scope. Never throws — a missing file is an empty memory. */
|
|
194
|
+
function listMemory({ scope = "chat", chatId } = {}) {
|
|
195
|
+
try {
|
|
196
|
+
return parseEntries(readRaw(memoryFilePath(scope, chatId)));
|
|
197
|
+
} catch (err) {
|
|
198
|
+
logger.error({ err }, "[memory] failed to read memory");
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Append one memory entry. Appending (rather than rewriting the file) is what
|
|
205
|
+
* keeps any prose the human added by hand intact.
|
|
206
|
+
*
|
|
207
|
+
* @returns {{id:string, content:string, scope:string, file:string}}
|
|
208
|
+
*/
|
|
209
|
+
function addMemory({ scope = "chat", chatId, content, by, chatName } = {}) {
|
|
210
|
+
const normalizedScope = normalizeScope(scope);
|
|
211
|
+
const text = String(content || "").trim().slice(0, MAX_ENTRY_CHARS);
|
|
212
|
+
if (!text) throw new Error("مفيش محتوى أحفظه");
|
|
213
|
+
|
|
214
|
+
const filePath = memoryFilePath(normalizedScope, chatId);
|
|
215
|
+
ensureDirs();
|
|
216
|
+
|
|
217
|
+
let raw = readRaw(filePath);
|
|
218
|
+
if (!raw.trim()) raw = headerFor(normalizedScope, chatId, chatName);
|
|
219
|
+
if (!raw.endsWith("\n")) raw += "\n";
|
|
220
|
+
|
|
221
|
+
const id = newId();
|
|
222
|
+
const meta = [
|
|
223
|
+
`id:${id}`,
|
|
224
|
+
`at:${new Date().toISOString()}`,
|
|
225
|
+
by ? `by:${String(by).replace(/\s+/g, "_").slice(0, 40)}` : null,
|
|
226
|
+
]
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
.join(" ");
|
|
229
|
+
|
|
230
|
+
// Multi-line facts stay one Markdown list item: continuation lines are
|
|
231
|
+
// indented so parseEntries() glues them back together.
|
|
232
|
+
const [first, ...rest] = text.split("\n");
|
|
233
|
+
const body = [
|
|
234
|
+
`- ${first.trim()} <!-- ${meta} -->`,
|
|
235
|
+
...rest.map((line) => ` ${line.trim()}`),
|
|
236
|
+
].join("\n");
|
|
237
|
+
|
|
238
|
+
writeRaw(filePath, `${raw}${body}\n`);
|
|
239
|
+
logger.info(
|
|
240
|
+
{ scope: normalizedScope, chatId, id },
|
|
241
|
+
"[memory] saved a new entry"
|
|
242
|
+
);
|
|
243
|
+
return { id, content: text, scope: normalizedScope, file: filePath };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Drop one entry, addressed by id, 1-based index, or a text fragment.
|
|
248
|
+
* @returns {object|null} the removed entry
|
|
249
|
+
*/
|
|
250
|
+
function removeMemory({ scope = "chat", chatId, ref } = {}) {
|
|
251
|
+
const filePath = memoryFilePath(scope, chatId);
|
|
252
|
+
const raw = readRaw(filePath);
|
|
253
|
+
if (!raw) return null;
|
|
254
|
+
|
|
255
|
+
const entries = parseEntries(raw);
|
|
256
|
+
if (!entries.length) return null;
|
|
257
|
+
|
|
258
|
+
const needle = String(ref ?? "").trim();
|
|
259
|
+
if (!needle) return null;
|
|
260
|
+
|
|
261
|
+
let target =
|
|
262
|
+
entries.find((entry) => entry.id === needle) ||
|
|
263
|
+
(/^\d+$/.test(needle) ? entries[Number(needle) - 1] : null);
|
|
264
|
+
|
|
265
|
+
if (!target) {
|
|
266
|
+
const lower = needle.toLowerCase();
|
|
267
|
+
target = entries.find((entry) => entry.content.toLowerCase().includes(lower));
|
|
268
|
+
}
|
|
269
|
+
if (!target) return null;
|
|
270
|
+
|
|
271
|
+
const lines = raw.split("\n");
|
|
272
|
+
lines.splice(target.start, target.end - target.start + 1);
|
|
273
|
+
writeRaw(filePath, lines.join("\n"));
|
|
274
|
+
return target;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Wipe every entry but keep whatever prose/header the file already had. */
|
|
278
|
+
function clearMemory({ scope = "chat", chatId } = {}) {
|
|
279
|
+
const filePath = memoryFilePath(scope, chatId);
|
|
280
|
+
const raw = readRaw(filePath);
|
|
281
|
+
if (!raw) return 0;
|
|
282
|
+
|
|
283
|
+
const entries = parseEntries(raw);
|
|
284
|
+
if (!entries.length) return 0;
|
|
285
|
+
|
|
286
|
+
const drop = new Set();
|
|
287
|
+
for (const entry of entries) {
|
|
288
|
+
for (let i = entry.start; i <= entry.end; i++) drop.add(i);
|
|
289
|
+
}
|
|
290
|
+
const kept = raw.split("\n").filter((_, index) => !drop.has(index));
|
|
291
|
+
writeRaw(filePath, kept.join("\n"));
|
|
292
|
+
return entries.length;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Case-insensitive substring search across one scope. */
|
|
296
|
+
function searchMemory(query, { scope = "chat", chatId } = {}) {
|
|
297
|
+
const needle = String(query || "").trim().toLowerCase();
|
|
298
|
+
const entries = listMemory({ scope, chatId });
|
|
299
|
+
if (!needle) return entries;
|
|
300
|
+
return entries.filter((entry) => entry.content.toLowerCase().includes(needle));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function renderEntries(entries, limitChars) {
|
|
304
|
+
const lines = [];
|
|
305
|
+
let used = 0;
|
|
306
|
+
// Newest facts matter most, so fill from the end and restore order after.
|
|
307
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
308
|
+
const line = `- ${entries[i].content.replace(/\n/g, " ")}`;
|
|
309
|
+
if (used + line.length > limitChars) break;
|
|
310
|
+
used += line.length + 1;
|
|
311
|
+
lines.unshift(line);
|
|
312
|
+
}
|
|
313
|
+
return lines.join("\n");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The memory block injected into the model's system instruction.
|
|
318
|
+
* Returns "" when both files are empty, so a fresh install pays nothing.
|
|
319
|
+
*/
|
|
320
|
+
function buildMemoryContext(chatId, { limitChars = maxContextChars() } = {}) {
|
|
321
|
+
const globalEntries = listMemory({ scope: "global" });
|
|
322
|
+
const chatEntries = chatId ? listMemory({ scope: "chat", chatId }) : [];
|
|
323
|
+
if (!globalEntries.length && !chatEntries.length) return "";
|
|
324
|
+
|
|
325
|
+
const perScope = Math.floor(limitChars / (globalEntries.length && chatEntries.length ? 2 : 1));
|
|
326
|
+
const blocks = [];
|
|
327
|
+
|
|
328
|
+
if (globalEntries.length) {
|
|
329
|
+
blocks.push(
|
|
330
|
+
`### ذاكرة عامة (global.md)\n${renderEntries(globalEntries, perScope)}`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (chatEntries.length) {
|
|
334
|
+
blocks.push(
|
|
335
|
+
`### ذاكرة المحادثة دي (${safeFileName(chatId)}.md)\n${renderEntries(
|
|
336
|
+
chatEntries,
|
|
337
|
+
perScope
|
|
338
|
+
)}`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return [
|
|
343
|
+
"## 🧠 الذاكرة الدائمة",
|
|
344
|
+
"دي معلومات اتحفظت قبل كده. اعتبرها حقيقة واستخدمها من غير ما تقول إنك بتقرأ من ملف.",
|
|
345
|
+
"",
|
|
346
|
+
...blocks,
|
|
347
|
+
].join("\n");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Counts + file paths, for `!memory` and `!status`. */
|
|
351
|
+
function memoryStats(chatId) {
|
|
352
|
+
const globalEntries = listMemory({ scope: "global" });
|
|
353
|
+
const chatEntries = chatId ? listMemory({ scope: "chat", chatId }) : [];
|
|
354
|
+
let chatFiles = 0;
|
|
355
|
+
try {
|
|
356
|
+
chatFiles = fs.readdirSync(CHATS_DIR).filter((f) => f.endsWith(".md")).length;
|
|
357
|
+
} catch {
|
|
358
|
+
chatFiles = 0;
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
root: ROOT,
|
|
362
|
+
globalFile: GLOBAL_FILE,
|
|
363
|
+
chatFile: chatId ? memoryFilePath("chat", chatId) : null,
|
|
364
|
+
globalCount: globalEntries.length,
|
|
365
|
+
chatCount: chatEntries.length,
|
|
366
|
+
chatFiles,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = {
|
|
371
|
+
ROOT,
|
|
372
|
+
CHATS_DIR,
|
|
373
|
+
GLOBAL_FILE,
|
|
374
|
+
ensureDirs,
|
|
375
|
+
normalizeScope,
|
|
376
|
+
memoryFilePath,
|
|
377
|
+
listMemory,
|
|
378
|
+
addMemory,
|
|
379
|
+
removeMemory,
|
|
380
|
+
clearMemory,
|
|
381
|
+
searchMemory,
|
|
382
|
+
buildMemoryContext,
|
|
383
|
+
memoryStats,
|
|
384
|
+
parseEntries,
|
|
385
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// CommonJS wrapper for normalizeJid.esm.js
|
|
2
|
+
// This allows CommonJS files to use the normalizeJid function
|
|
3
|
+
|
|
4
|
+
// We can't directly require ESM from CommonJS, so we use a workaround
|
|
5
|
+
// by creating a simple normalizeJid function that handles both formats
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* V7: Normalize WhatsApp JID to handle both PN (Phone Number) and LID (Local Identifier)
|
|
9
|
+
* @param {string} jid - The JID to normalize
|
|
10
|
+
* @returns {string} - Normalized JID
|
|
11
|
+
*/
|
|
12
|
+
function normalizeJid(jid) {
|
|
13
|
+
if (!jid) return jid;
|
|
14
|
+
|
|
15
|
+
// V7: Handle LID format (@lid)
|
|
16
|
+
if (jid.includes("@lid")) {
|
|
17
|
+
// LID format: keep as-is
|
|
18
|
+
if (jid.includes(":")) {
|
|
19
|
+
return jid.split(":")[0] + "@lid";
|
|
20
|
+
}
|
|
21
|
+
return jid;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle PN format (@s.whatsapp.net)
|
|
25
|
+
if (jid.includes(":")) {
|
|
26
|
+
return jid.split(":")[0] + "@" + jid.split("@")[1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return jid;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* V7: Helper to check if JID is a Phone Number (PN)
|
|
34
|
+
* @param {string} jid - The JID to check
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
function isPnJid(jid) {
|
|
38
|
+
if (!jid) return false;
|
|
39
|
+
return jid.includes("@s.whatsapp.net");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* V7: Helper to check if JID is a LID
|
|
44
|
+
* @param {string} jid - The JID to check
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isLidJid(jid) {
|
|
48
|
+
if (!jid) return false;
|
|
49
|
+
return jid.includes("@lid");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* V7: Helper to check if JID is a user (either PN or LID)
|
|
54
|
+
* @param {string} jid - The JID to check
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
function isUserJid(jid) {
|
|
58
|
+
return isPnJid(jid) || isLidJid(jid);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = normalizeJid;
|
|
62
|
+
module.exports.normalizeJid = normalizeJid;
|
|
63
|
+
module.exports.isPnJid = isPnJid;
|
|
64
|
+
module.exports.isLidJid = isLidJid;
|
|
65
|
+
module.exports.isUserJid = isUserJid;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// V7: Updated to handle both PN (Phone Number) and LID (Local Identifier)
|
|
2
|
+
export default function normalizeJid(jid) {
|
|
3
|
+
if (!jid) return jid;
|
|
4
|
+
|
|
5
|
+
// V7: Handle LID format (@lid)
|
|
6
|
+
if (jid.includes('@lid')) {
|
|
7
|
+
// LID format: keep as-is
|
|
8
|
+
if (jid.includes(':')) {
|
|
9
|
+
return jid.split(':')[0] + '@lid';
|
|
10
|
+
}
|
|
11
|
+
return jid;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Handle PN format (@s.whatsapp.net)
|
|
15
|
+
if (jid.includes(':')) {
|
|
16
|
+
return jid.split(':')[0] + '@' + jid.split('@')[1];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return jid;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// V7: Helper to check if JID is a Phone Number (PN)
|
|
23
|
+
export function isPnJid(jid) {
|
|
24
|
+
if (!jid) return false;
|
|
25
|
+
return jid.includes('@s.whatsapp.net');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// V7: Helper to check if JID is a LID
|
|
29
|
+
export function isLidJid(jid) {
|
|
30
|
+
if (!jid) return false;
|
|
31
|
+
return jid.includes('@lid');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// V7: Helper to check if JID is a user (either PN or LID)
|
|
35
|
+
export function isUserJid(jid) {
|
|
36
|
+
return isPnJid(jid) || isLidJid(jid);
|
|
37
|
+
}
|