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,317 @@
|
|
|
1
|
+
// The AI as an agent, not a single API call.
|
|
2
|
+
//
|
|
3
|
+
// Old flow: prompt -> one generateContent -> send the text. Anything the model
|
|
4
|
+
// didn't already know was a guess.
|
|
5
|
+
// New flow: prompt -> model may call tools (search, open a url, save to
|
|
6
|
+
// memory, hand out roles) -> we run them, feed the results back, and
|
|
7
|
+
// let it decide again -> ... -> final answer.
|
|
8
|
+
//
|
|
9
|
+
// The whole run is narrated in ONE WhatsApp message that gets edited as the
|
|
10
|
+
// agent works ("🤖 بفكر..." -> "🔍 ببحث عن ..." -> the answer), which is what
|
|
11
|
+
// the operator asked for: no chain of throwaway status messages.
|
|
12
|
+
//
|
|
13
|
+
// System prompt: read from config/ai-persona.md (hot-reloaded), plus a small
|
|
14
|
+
// runtime block and whatever is in the long-term memory files.
|
|
15
|
+
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const { GoogleGenerativeAI } = require("@google/generative-ai");
|
|
19
|
+
|
|
20
|
+
const logger = require("../utils/logger.cjs");
|
|
21
|
+
const brand = require("../config/brand.cjs");
|
|
22
|
+
const settings = require("../config/settings.cjs");
|
|
23
|
+
const memory = require("../utils/memory.cjs");
|
|
24
|
+
const { toolDeclarations, describeCall, runTool } = require("./aiTools.cjs");
|
|
25
|
+
|
|
26
|
+
// Model, key, tool budget and history length all come from config/settings.cjs
|
|
27
|
+
// (what the dashboard saved, else the default) and are read per call, so changing
|
|
28
|
+
// any of them from the dashboard takes effect on the next message instead of
|
|
29
|
+
// after a restart.
|
|
30
|
+
|
|
31
|
+
// One client per key. The dashboard can set a key on a bot that started
|
|
32
|
+
// without one, so the client is built lazily and rebuilt when the key changes.
|
|
33
|
+
let clientCache = { key: null, client: null };
|
|
34
|
+
|
|
35
|
+
function geminiClient() {
|
|
36
|
+
const key = settings.get("gemini_api_key");
|
|
37
|
+
if (!key) return null;
|
|
38
|
+
if (clientCache.key !== key) {
|
|
39
|
+
clientCache = { key, client: new GoogleGenerativeAI(key) };
|
|
40
|
+
}
|
|
41
|
+
return clientCache.client;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The persona is editable — from the dashboard, or in a text editor — so it
|
|
45
|
+
// lives in the data directory. The copy that ships with the bot is a template:
|
|
46
|
+
// it is copied over on first start and never written to again, which is what
|
|
47
|
+
// lets a global install or a read-only image still edit its own prompt.
|
|
48
|
+
const { dataPath, assetPath } = require("../config/paths.cjs");
|
|
49
|
+
const PERSONA_TEMPLATE = assetPath("src", "config", "ai-persona.md");
|
|
50
|
+
const PERSONA_FILE = dataPath("ai-persona.md");
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (!fs.existsSync(PERSONA_FILE) && fs.existsSync(PERSONA_TEMPLATE)) {
|
|
54
|
+
fs.copyFileSync(PERSONA_TEMPLATE, PERSONA_FILE);
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Not fatal: loadPersona() falls back to DEFAULT_PERSONA below.
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fallback only — the real persona is config/ai-persona.md (editable from the
|
|
61
|
+
// dashboard). No name in here: identity comes from brand.cjs and is prepended
|
|
62
|
+
// separately so it survives any edit to the persona.
|
|
63
|
+
const DEFAULT_PERSONA = `بوت واتساب ذكي وخفيف الظل.
|
|
64
|
+
اتكلم مصري عامي، ردودك قصيرة ومباشرة، واستخدم أدواتك بدل التخمين.`;
|
|
65
|
+
|
|
66
|
+
let personaCache = { text: null, mtime: 0 };
|
|
67
|
+
|
|
68
|
+
/** The persona file, re-read whenever it changes on disk. */
|
|
69
|
+
function loadPersona() {
|
|
70
|
+
try {
|
|
71
|
+
const { mtimeMs } = fs.statSync(PERSONA_FILE);
|
|
72
|
+
if (personaCache.text && personaCache.mtime === mtimeMs) {
|
|
73
|
+
return personaCache.text;
|
|
74
|
+
}
|
|
75
|
+
const raw = fs.readFileSync(PERSONA_FILE, "utf8").trim();
|
|
76
|
+
// Everything above the `---` separator is a note to the human editing the
|
|
77
|
+
// file, not part of the prompt.
|
|
78
|
+
const body = raw.includes("\n---") ? raw.split(/\n---\s*\n/).slice(1).join("\n---\n") : raw;
|
|
79
|
+
personaCache = { text: body.trim() || DEFAULT_PERSONA, mtime: mtimeMs };
|
|
80
|
+
return personaCache.text;
|
|
81
|
+
} catch {
|
|
82
|
+
return DEFAULT_PERSONA;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function nowLabel() {
|
|
87
|
+
try {
|
|
88
|
+
return new Intl.DateTimeFormat("ar-EG", {
|
|
89
|
+
timeZone: settings.get("bot_timezone"),
|
|
90
|
+
dateStyle: "full",
|
|
91
|
+
timeStyle: "short",
|
|
92
|
+
}).format(new Date());
|
|
93
|
+
} catch {
|
|
94
|
+
return new Date().toISOString();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* frozen identity + persona + who/where/when + long-term memory.
|
|
100
|
+
*
|
|
101
|
+
* The identity block goes FIRST and does not come from the persona file: the
|
|
102
|
+
* operator can rewrite the persona from the dashboard, but not the bot's name
|
|
103
|
+
* or who wrote it.
|
|
104
|
+
* Deliberately short: the old prompt was a page of rules, and the operator
|
|
105
|
+
* asked for it to go away.
|
|
106
|
+
*/
|
|
107
|
+
function buildSystemInstruction(context = {}) {
|
|
108
|
+
const {
|
|
109
|
+
chatId,
|
|
110
|
+
senderName,
|
|
111
|
+
senderId,
|
|
112
|
+
isOwner,
|
|
113
|
+
isAdmin,
|
|
114
|
+
isGroup,
|
|
115
|
+
chatName,
|
|
116
|
+
} = context;
|
|
117
|
+
|
|
118
|
+
const runtime = [
|
|
119
|
+
"## السياق الحالي",
|
|
120
|
+
`- المتكلم: ${senderName || "غير معروف"}${senderId ? ` (${senderId})` : ""}`,
|
|
121
|
+
`- صلاحيته: ${isOwner ? "مالك البوت" : isAdmin ? "أدمن" : "مستخدم عادي"}`,
|
|
122
|
+
`- المكان: ${isGroup ? `جروب${chatName ? ` اسمه "${chatName}"` : ""}` : "محادثة خاصة"}`,
|
|
123
|
+
`- الوقت دلوقتي: ${nowLabel()}`,
|
|
124
|
+
"- في جروب: الكل شايف ردك، فخلي بالك.",
|
|
125
|
+
].join("\n");
|
|
126
|
+
|
|
127
|
+
const memoryBlock = memory.buildMemoryContext(chatId);
|
|
128
|
+
|
|
129
|
+
return [brand.identityPrompt, loadPersona(), runtime, memoryBlock]
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.join("\n\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A turn the conversation can legally start from. */
|
|
135
|
+
function isOpeningTurn(entry) {
|
|
136
|
+
if (entry?.role !== "user") return false;
|
|
137
|
+
// A functionResponse whose matching functionCall was trimmed away is not a
|
|
138
|
+
// valid opening — Gemini rejects the whole history for it.
|
|
139
|
+
return !entry.parts?.some((part) => part?.functionResponse);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Keep the stored history bounded, and make sure it still starts on a real
|
|
144
|
+
* user turn: Gemini rejects a history that opens with a model or function
|
|
145
|
+
* entry.
|
|
146
|
+
*/
|
|
147
|
+
function trimHistory(history) {
|
|
148
|
+
const maxEntries = settings.get("ai_max_history");
|
|
149
|
+
const out = Array.isArray(history) ? [...history] : [];
|
|
150
|
+
while (out.length > maxEntries) out.shift();
|
|
151
|
+
while (out.length && !isOpeningTurn(out[0])) out.shift();
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Gemini file URIs live ~48h; after that every call fails on the old parts. */
|
|
156
|
+
function isFileReferenceError(error) {
|
|
157
|
+
if (!error) return false;
|
|
158
|
+
const status =
|
|
159
|
+
error.status ||
|
|
160
|
+
error.statusCode ||
|
|
161
|
+
error.response?.status ||
|
|
162
|
+
error.error?.code;
|
|
163
|
+
const message = `${error.message || ""} ${error.details || ""}`.toLowerCase();
|
|
164
|
+
|
|
165
|
+
if ([403, 404, 410].includes(Number(status))) return true;
|
|
166
|
+
if (message.includes("permission_denied")) return true;
|
|
167
|
+
if (message.includes("failed_precondition")) return true;
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
message.includes("file") &&
|
|
171
|
+
/not found|not exist|expired|not in an active|not active|permission to access|forbidden|file_?uri/.test(
|
|
172
|
+
message
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Drop every uploaded-media part so a stale file URI can't poison the chat. */
|
|
178
|
+
function sanitizeHistoryForFiles(history) {
|
|
179
|
+
if (!Array.isArray(history)) return [];
|
|
180
|
+
return history
|
|
181
|
+
.map((turn) => {
|
|
182
|
+
if (!turn?.parts) return turn;
|
|
183
|
+
const parts = turn.parts.filter((part) => !part?.fileData && !part?.inlineData);
|
|
184
|
+
if (!parts.length) return null;
|
|
185
|
+
return { ...turn, parts };
|
|
186
|
+
})
|
|
187
|
+
.filter(Boolean);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function withTimeout(promise, ms, label) {
|
|
191
|
+
let timer;
|
|
192
|
+
return Promise.race([
|
|
193
|
+
promise,
|
|
194
|
+
new Promise((_, reject) => {
|
|
195
|
+
timer = setTimeout(
|
|
196
|
+
() => reject(new Error(`${label} تأخرت أكتر من ${Math.round(ms / 1000)} ثانية`)),
|
|
197
|
+
ms
|
|
198
|
+
);
|
|
199
|
+
}),
|
|
200
|
+
]).finally(() => clearTimeout(timer));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isAgentEnabled() {
|
|
204
|
+
return Boolean(geminiClient()) && settings.get("ai_agent");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Run one agent turn.
|
|
209
|
+
*
|
|
210
|
+
* @param {object} options
|
|
211
|
+
* @param {Array} options.parts - the Gemini parts for this message
|
|
212
|
+
* @param {Array} [options.history]- prior conversation
|
|
213
|
+
* @param {object} [options.status] - live status message (see utils/statusMessage.cjs)
|
|
214
|
+
* @param {object} [options.context]- { chatId, senderId, senderName, isOwner, isAdmin, isGroup, chatName }
|
|
215
|
+
* @param {boolean} [options.useTools=true]
|
|
216
|
+
* @returns {Promise<{text: string, history: Array, toolCalls: Array, steps: number}>}
|
|
217
|
+
*/
|
|
218
|
+
async function runAgent({
|
|
219
|
+
parts,
|
|
220
|
+
history = [],
|
|
221
|
+
status = null,
|
|
222
|
+
context = {},
|
|
223
|
+
useTools = true,
|
|
224
|
+
maxSteps = null,
|
|
225
|
+
} = {}) {
|
|
226
|
+
const genAI = geminiClient();
|
|
227
|
+
if (!genAI) throw new Error("GEMINI_API_KEY غير معرف");
|
|
228
|
+
|
|
229
|
+
const stepBudget = maxSteps ?? settings.get("ai_max_tool_steps");
|
|
230
|
+
|
|
231
|
+
const model = genAI.getGenerativeModel({
|
|
232
|
+
model: settings.get("gemini_model"),
|
|
233
|
+
systemInstruction: buildSystemInstruction(context),
|
|
234
|
+
...(useTools ? { tools: toolDeclarations() } : {}),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const chat = model.startChat({ history: trimHistory(history) });
|
|
238
|
+
const toolCalls = [];
|
|
239
|
+
|
|
240
|
+
let result = await chat.sendMessage(parts);
|
|
241
|
+
let steps = 0;
|
|
242
|
+
|
|
243
|
+
while (steps < stepBudget) {
|
|
244
|
+
const calls =
|
|
245
|
+
(typeof result.response?.functionCalls === "function"
|
|
246
|
+
? result.response.functionCalls()
|
|
247
|
+
: null) || [];
|
|
248
|
+
if (!calls.length) break;
|
|
249
|
+
|
|
250
|
+
steps += 1;
|
|
251
|
+
|
|
252
|
+
// Narrate what is about to happen in the same message the user is already
|
|
253
|
+
// looking at.
|
|
254
|
+
if (status) {
|
|
255
|
+
const line = calls.map((call) => describeCall(call.name, call.args)).join("\n");
|
|
256
|
+
await status.update(line);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const responses = [];
|
|
260
|
+
for (const call of calls) {
|
|
261
|
+
toolCalls.push({ name: call.name, args: call.args });
|
|
262
|
+
logger.info(
|
|
263
|
+
{ tool: call.name, chatId: context.chatId },
|
|
264
|
+
"[aiAgent] tool call"
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
let response;
|
|
268
|
+
try {
|
|
269
|
+
response = await withTimeout(
|
|
270
|
+
runTool(call.name, call.args, context),
|
|
271
|
+
settings.get("ai_tool_timeout_ms"),
|
|
272
|
+
call.name
|
|
273
|
+
);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
response = { error: err?.message || String(err) };
|
|
276
|
+
}
|
|
277
|
+
responses.push({
|
|
278
|
+
functionResponse: { name: call.name, response },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (status) await status.update("🤖 بجهّز الرد...");
|
|
283
|
+
result = await chat.sendMessage(responses);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let text = "";
|
|
287
|
+
try {
|
|
288
|
+
text = result.response.text();
|
|
289
|
+
} catch (err) {
|
|
290
|
+
logger.warn({ err: err?.message }, "[aiAgent] response had no text part");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!text && steps >= stepBudget) {
|
|
294
|
+
text =
|
|
295
|
+
"شغّلت الأدوات المتاحة بس مقدرتش أوصل لإجابة نهائية. جرّب تسأل بصيغة أوضح.";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let newHistory = [];
|
|
299
|
+
try {
|
|
300
|
+
newHistory = trimHistory(await chat.getHistory());
|
|
301
|
+
} catch (err) {
|
|
302
|
+
logger.warn({ err: err?.message }, "[aiAgent] could not read chat history");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { text, history: newHistory, toolCalls, steps };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
runAgent,
|
|
310
|
+
buildSystemInstruction,
|
|
311
|
+
loadPersona,
|
|
312
|
+
isAgentEnabled,
|
|
313
|
+
isFileReferenceError,
|
|
314
|
+
sanitizeHistoryForFiles,
|
|
315
|
+
trimHistory,
|
|
316
|
+
PERSONA_FILE,
|
|
317
|
+
};
|