rex-claude 3.0.0 → 4.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/dist/gateway-EKMU5D7J.js +784 -0
- package/dist/index.js +13 -5
- package/dist/{init-W3XGDQ6D.js → init-DLFEGD6O.js} +1 -1
- package/dist/prune-2PPIVDXK.js +151 -0
- package/package.json +1 -1
- package/dist/gateway-EOVQXRON.js +0 -198
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/gateway.ts
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
var OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
|
|
9
|
+
var STATE_FILE = join(homedir(), ".rex-memory", "gateway-state.json");
|
|
10
|
+
var LOG_FILE = join(homedir(), ".claude", "rex-gateway-commands.log");
|
|
11
|
+
function loadConfig() {
|
|
12
|
+
const defaults = {
|
|
13
|
+
macTailscaleIp: "100.112.24.122",
|
|
14
|
+
macAddress: "52:f1:cf:b2:a5:32",
|
|
15
|
+
vpsTailscaleIp: "100.86.167.118",
|
|
16
|
+
pollTimeout: 30,
|
|
17
|
+
maxOutputLength: 4e3
|
|
18
|
+
};
|
|
19
|
+
try {
|
|
20
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
21
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
22
|
+
const gw = settings.env || {};
|
|
23
|
+
return {
|
|
24
|
+
macTailscaleIp: gw.REX_MAC_TAILSCALE_IP || defaults.macTailscaleIp,
|
|
25
|
+
macAddress: gw.REX_MAC_ADDRESS || defaults.macAddress,
|
|
26
|
+
vpsTailscaleIp: gw.REX_VPS_TAILSCALE_IP || defaults.vpsTailscaleIp,
|
|
27
|
+
pollTimeout: parseInt(gw.REX_POLL_TIMEOUT || "") || defaults.pollTimeout,
|
|
28
|
+
maxOutputLength: parseInt(gw.REX_MAX_OUTPUT || "") || defaults.maxOutputLength
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return defaults;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function loadState() {
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(STATE_FILE)) {
|
|
37
|
+
return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
return { mode: "qwen", lastActivity: (/* @__PURE__ */ new Date()).toISOString(), sessionsCount: 0 };
|
|
42
|
+
}
|
|
43
|
+
function saveState(state2) {
|
|
44
|
+
try {
|
|
45
|
+
const dir = join(homedir(), ".rex-memory");
|
|
46
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
47
|
+
writeFileSync(STATE_FILE, JSON.stringify(state2, null, 2));
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
var state = loadState();
|
|
52
|
+
var config = loadConfig();
|
|
53
|
+
function logCommand(from, command, result) {
|
|
54
|
+
try {
|
|
55
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
56
|
+
const entry = `[${ts}] @${from}: ${command} -> ${result.slice(0, 200)}
|
|
57
|
+
`;
|
|
58
|
+
const dir = join(homedir(), ".claude");
|
|
59
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
60
|
+
writeFileSync(LOG_FILE, entry, { flag: "a" });
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
var COLORS = {
|
|
65
|
+
reset: "\x1B[0m",
|
|
66
|
+
green: "\x1B[32m",
|
|
67
|
+
yellow: "\x1B[33m",
|
|
68
|
+
red: "\x1B[31m",
|
|
69
|
+
dim: "\x1B[2m",
|
|
70
|
+
bold: "\x1B[1m",
|
|
71
|
+
cyan: "\x1B[36m"
|
|
72
|
+
};
|
|
73
|
+
function getCredentials() {
|
|
74
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
75
|
+
try {
|
|
76
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
77
|
+
const token2 = settings.env?.REX_TELEGRAM_BOT_TOKEN;
|
|
78
|
+
const chatId2 = settings.env?.REX_TELEGRAM_CHAT_ID;
|
|
79
|
+
if (token2 && chatId2) return { token: token2, chatId: chatId2 };
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
const token = process.env.REX_TELEGRAM_BOT_TOKEN;
|
|
83
|
+
const chatId = process.env.REX_TELEGRAM_CHAT_ID;
|
|
84
|
+
if (token && chatId) return { token, chatId };
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
async function tg(token, method, body) {
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(`https://api.telegram.org/bot${token}/${method}`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify(body)
|
|
93
|
+
});
|
|
94
|
+
return await res.json();
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function send(token, chatId, text, keyboard) {
|
|
100
|
+
const body = { chat_id: chatId, text, parse_mode: "Markdown" };
|
|
101
|
+
if (keyboard) {
|
|
102
|
+
body.reply_markup = { inline_keyboard: keyboard };
|
|
103
|
+
}
|
|
104
|
+
return tg(token, "sendMessage", body);
|
|
105
|
+
}
|
|
106
|
+
async function editMessage(token, chatId, messageId, text, keyboard) {
|
|
107
|
+
const body = { chat_id: chatId, message_id: messageId, text, parse_mode: "Markdown" };
|
|
108
|
+
if (keyboard) {
|
|
109
|
+
body.reply_markup = { inline_keyboard: keyboard };
|
|
110
|
+
}
|
|
111
|
+
return tg(token, "editMessageText", body);
|
|
112
|
+
}
|
|
113
|
+
async function answerCallback(token, callbackId, text) {
|
|
114
|
+
return tg(token, "answerCallbackQuery", { callback_query_id: callbackId, text });
|
|
115
|
+
}
|
|
116
|
+
function isAuthorized(msgChatId, authorizedChatId) {
|
|
117
|
+
return String(msgChatId) === String(authorizedChatId);
|
|
118
|
+
}
|
|
119
|
+
function mainMenu() {
|
|
120
|
+
return [
|
|
121
|
+
[
|
|
122
|
+
{ text: "\u{1F4CA} Status", callback_data: "status" },
|
|
123
|
+
{ text: "\u{1FA7A} Doctor", callback_data: "doctor" },
|
|
124
|
+
{ text: "\u{1F50D} Memory", callback_data: "memory_menu" }
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
{ text: "\u{1F5A5} Git", callback_data: "git" },
|
|
128
|
+
{ text: "\u26A1 Optimize", callback_data: "optimize" },
|
|
129
|
+
{ text: "\u{1F4E5} Ingest", callback_data: "ingest" }
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
{ text: `\u{1F916} Mode: ${state.mode === "qwen" ? "Qwen (local)" : "Claude"}`, callback_data: "switch_mode" },
|
|
133
|
+
{ text: "\u{1F9F9} Prune", callback_data: "prune" }
|
|
134
|
+
],
|
|
135
|
+
[
|
|
136
|
+
{ text: "\u{1F4A4} Wake Mac", callback_data: "wake_mac" },
|
|
137
|
+
{ text: "\u{1F50C} Mac Status", callback_data: "mac_status" }
|
|
138
|
+
],
|
|
139
|
+
[
|
|
140
|
+
{ text: "\u{1F4CB} Sessions", callback_data: "sessions" },
|
|
141
|
+
{ text: "\u{1F4DD} Logs", callback_data: "logs" }
|
|
142
|
+
]
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
function backButton() {
|
|
146
|
+
return [[{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }]];
|
|
147
|
+
}
|
|
148
|
+
function claudeMenu() {
|
|
149
|
+
return [
|
|
150
|
+
[
|
|
151
|
+
{ text: "\u{1F4AC} New Session", callback_data: "claude_new" },
|
|
152
|
+
{ text: "\u{1F4C2} Continue Last", callback_data: "claude_continue" }
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
{ text: "\u{1F4CB} List Sessions", callback_data: "claude_sessions" },
|
|
156
|
+
{ text: "\u{1F504} Resume #", callback_data: "claude_resume" }
|
|
157
|
+
],
|
|
158
|
+
[{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }]
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
function run(cmd, timeout = 3e4) {
|
|
162
|
+
try {
|
|
163
|
+
return execSync(cmd, { timeout, encoding: "utf-8" }).trim();
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return e.stderr?.trim() || e.message || "Command failed";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function strip(text) {
|
|
169
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
170
|
+
}
|
|
171
|
+
function truncate(text, max) {
|
|
172
|
+
const limit = max || config.maxOutputLength;
|
|
173
|
+
if (text.length <= limit) return text;
|
|
174
|
+
return text.slice(0, limit) + "\n\n... (truncated)";
|
|
175
|
+
}
|
|
176
|
+
async function wakeMac() {
|
|
177
|
+
try {
|
|
178
|
+
const mac = config.macAddress.replace(/:/g, "");
|
|
179
|
+
const pyCmd = `python3 -c "
|
|
180
|
+
import socket, struct
|
|
181
|
+
mac = bytes.fromhex('${mac}')
|
|
182
|
+
pkt = b'\\xff'*6 + mac*16
|
|
183
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
184
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
185
|
+
s.sendto(pkt, ('255.255.255.255', 9))
|
|
186
|
+
s.sendto(pkt, ('${config.macTailscaleIp}', 9))
|
|
187
|
+
s.close()
|
|
188
|
+
print('Magic packet sent')
|
|
189
|
+
"`;
|
|
190
|
+
const out = run(pyCmd, 5e3);
|
|
191
|
+
if (out.includes("Magic packet sent")) return "\u2705 Magic packet sent to Mac";
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const ping = run(`ping -c 1 -W 2 ${config.macTailscaleIp} 2>/dev/null`, 5e3);
|
|
196
|
+
if (ping.includes("1 packets received") || ping.includes("1 received")) {
|
|
197
|
+
return "\u2705 Mac is already awake (responds to ping)";
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
return "\u26A0\uFE0F Magic packet sent \u2014 Mac may take 30s to wake";
|
|
202
|
+
}
|
|
203
|
+
async function checkMacStatus() {
|
|
204
|
+
try {
|
|
205
|
+
const ping = run(`ping -c 1 -W 3 ${config.macTailscaleIp} 2>/dev/null`, 5e3);
|
|
206
|
+
const online = ping.includes("1 packets received") || ping.includes("1 received");
|
|
207
|
+
if (online) {
|
|
208
|
+
const ts = run("tailscale status 2>/dev/null | head -5", 5e3);
|
|
209
|
+
return { online: true, details: ts };
|
|
210
|
+
}
|
|
211
|
+
return { online: false, details: "Mac not responding to ping" };
|
|
212
|
+
} catch {
|
|
213
|
+
return { online: false, details: "Ping failed" };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function askLLM(prompt) {
|
|
217
|
+
if (state.mode === "qwen") {
|
|
218
|
+
return askQwen(prompt);
|
|
219
|
+
} else {
|
|
220
|
+
return askClaude(prompt);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function askQwen(prompt) {
|
|
224
|
+
try {
|
|
225
|
+
const check = await fetch(`${OLLAMA_URL}/api/tags`);
|
|
226
|
+
if (!check.ok) return "\u26A0\uFE0F Ollama not running. /wake to wake Mac first.";
|
|
227
|
+
} catch {
|
|
228
|
+
return "\u26A0\uFE0F Ollama not running. Wake Mac first.";
|
|
229
|
+
}
|
|
230
|
+
const out = run(`rex llm "${prompt.replace(/"/g, '\\"').replace(/`/g, "\\`")}"`, 6e4);
|
|
231
|
+
if (!out || out.includes("rex-claude") || out.includes("Commands:")) {
|
|
232
|
+
return "\u26A0\uFE0F LLM returned no useful response";
|
|
233
|
+
}
|
|
234
|
+
return truncate(out);
|
|
235
|
+
}
|
|
236
|
+
async function askClaude(prompt) {
|
|
237
|
+
try {
|
|
238
|
+
const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/`/g, "\\`");
|
|
239
|
+
const out = run(`claude -p "${escapedPrompt}" 2>/dev/null`, 12e4);
|
|
240
|
+
if (out) return truncate(out);
|
|
241
|
+
return "\u26A0\uFE0F Claude CLI not available or returned empty";
|
|
242
|
+
} catch {
|
|
243
|
+
return "\u26A0\uFE0F Claude CLI error";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async function claudeSession(prompt, resume) {
|
|
247
|
+
try {
|
|
248
|
+
const flag = resume ? "--continue" : "";
|
|
249
|
+
const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/`/g, "\\`");
|
|
250
|
+
const out = run(`claude ${flag} -p "${escapedPrompt}" 2>/dev/null`, 18e4);
|
|
251
|
+
state.sessionsCount++;
|
|
252
|
+
saveState(state);
|
|
253
|
+
if (out) return truncate(out);
|
|
254
|
+
return "\u26A0\uFE0F No response from Claude session";
|
|
255
|
+
} catch {
|
|
256
|
+
return "\u26A0\uFE0F Claude session error";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function handleCallback(token, chatId, messageId, callbackId, data, from) {
|
|
260
|
+
await answerCallback(token, callbackId);
|
|
261
|
+
logCommand(from, `[btn] ${data}`, "ok");
|
|
262
|
+
switch (data) {
|
|
263
|
+
case "menu":
|
|
264
|
+
await editMessage(
|
|
265
|
+
token,
|
|
266
|
+
chatId,
|
|
267
|
+
messageId,
|
|
268
|
+
"\u{1F996} *REX Gateway v3*\nChoisis une action :",
|
|
269
|
+
mainMenu()
|
|
270
|
+
);
|
|
271
|
+
break;
|
|
272
|
+
case "status": {
|
|
273
|
+
const out = strip(run("rex status"));
|
|
274
|
+
await editMessage(
|
|
275
|
+
token,
|
|
276
|
+
chatId,
|
|
277
|
+
messageId,
|
|
278
|
+
`\u{1F4CA} *Status*
|
|
279
|
+
${out}`,
|
|
280
|
+
backButton()
|
|
281
|
+
);
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case "doctor": {
|
|
285
|
+
await editMessage(token, chatId, messageId, "\u{1FA7A} _Running diagnostics..._");
|
|
286
|
+
const out = truncate(strip(run("rex doctor")));
|
|
287
|
+
await editMessage(
|
|
288
|
+
token,
|
|
289
|
+
chatId,
|
|
290
|
+
messageId,
|
|
291
|
+
`\u{1FA7A} *Doctor*
|
|
292
|
+
\`\`\`
|
|
293
|
+
${out}
|
|
294
|
+
\`\`\``,
|
|
295
|
+
backButton()
|
|
296
|
+
);
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case "git": {
|
|
300
|
+
const branch = run('git branch --show-current 2>/dev/null || echo "n/a"');
|
|
301
|
+
const status = run("git status --short 2>/dev/null | head -15");
|
|
302
|
+
const lastCommit = run('git log -1 --format="%s" 2>/dev/null || echo "n/a"');
|
|
303
|
+
await editMessage(
|
|
304
|
+
token,
|
|
305
|
+
chatId,
|
|
306
|
+
messageId,
|
|
307
|
+
`\u{1F5A5} *Git*
|
|
308
|
+
Branch: \`${branch}\`
|
|
309
|
+
Last: ${lastCommit}
|
|
310
|
+
\`\`\`
|
|
311
|
+
${status || "Clean"}
|
|
312
|
+
\`\`\``,
|
|
313
|
+
backButton()
|
|
314
|
+
);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
case "optimize": {
|
|
318
|
+
await editMessage(token, chatId, messageId, "\u26A1 _Analyzing CLAUDE.md..._");
|
|
319
|
+
const out = truncate(strip(run("rex optimize", 6e4)), 3500);
|
|
320
|
+
await editMessage(
|
|
321
|
+
token,
|
|
322
|
+
chatId,
|
|
323
|
+
messageId,
|
|
324
|
+
`\u26A1 *Optimize*
|
|
325
|
+
\`\`\`
|
|
326
|
+
${out}
|
|
327
|
+
\`\`\``,
|
|
328
|
+
[[
|
|
329
|
+
{ text: "\u{1F527} Apply", callback_data: "optimize_apply" },
|
|
330
|
+
{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }
|
|
331
|
+
]]
|
|
332
|
+
);
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
case "optimize_apply": {
|
|
336
|
+
await editMessage(token, chatId, messageId, "\u{1F527} _Applying optimizations..._");
|
|
337
|
+
const out = truncate(strip(run("rex optimize --apply", 12e4)), 3500);
|
|
338
|
+
await editMessage(
|
|
339
|
+
token,
|
|
340
|
+
chatId,
|
|
341
|
+
messageId,
|
|
342
|
+
`\u{1F527} *Applied*
|
|
343
|
+
\`\`\`
|
|
344
|
+
${out}
|
|
345
|
+
\`\`\``,
|
|
346
|
+
backButton()
|
|
347
|
+
);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case "ingest": {
|
|
351
|
+
await editMessage(token, chatId, messageId, "\u{1F4E5} _Ingesting sessions..._");
|
|
352
|
+
const out = truncate(strip(run("rex ingest", 12e4)), 3500);
|
|
353
|
+
await editMessage(
|
|
354
|
+
token,
|
|
355
|
+
chatId,
|
|
356
|
+
messageId,
|
|
357
|
+
`\u{1F4E5} *Ingest*
|
|
358
|
+
\`\`\`
|
|
359
|
+
${out}
|
|
360
|
+
\`\`\``,
|
|
361
|
+
backButton()
|
|
362
|
+
);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case "prune": {
|
|
366
|
+
await editMessage(token, chatId, messageId, "\u{1F9F9} _Pruning old memories..._");
|
|
367
|
+
const out = truncate(strip(run("rex prune", 6e4)));
|
|
368
|
+
await editMessage(
|
|
369
|
+
token,
|
|
370
|
+
chatId,
|
|
371
|
+
messageId,
|
|
372
|
+
`\u{1F9F9} *Prune*
|
|
373
|
+
\`\`\`
|
|
374
|
+
${out}
|
|
375
|
+
\`\`\``,
|
|
376
|
+
backButton()
|
|
377
|
+
);
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
case "memory_menu":
|
|
381
|
+
await editMessage(
|
|
382
|
+
token,
|
|
383
|
+
chatId,
|
|
384
|
+
messageId,
|
|
385
|
+
"\u{1F50D} *Memory*\nEnvoie ta recherche en texte ou :",
|
|
386
|
+
[
|
|
387
|
+
[
|
|
388
|
+
{ text: "\u{1F4E5} Ingest Now", callback_data: "ingest" },
|
|
389
|
+
{ text: "\u{1F9F9} Prune", callback_data: "prune" }
|
|
390
|
+
],
|
|
391
|
+
[
|
|
392
|
+
{ text: "\u{1F4CA} Stats", callback_data: "memory_stats" },
|
|
393
|
+
{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }
|
|
394
|
+
]
|
|
395
|
+
]
|
|
396
|
+
);
|
|
397
|
+
break;
|
|
398
|
+
case "memory_stats": {
|
|
399
|
+
const out = run("rex prune --stats", 1e4);
|
|
400
|
+
await editMessage(
|
|
401
|
+
token,
|
|
402
|
+
chatId,
|
|
403
|
+
messageId,
|
|
404
|
+
`\u{1F4CA} *Memory Stats*
|
|
405
|
+
\`\`\`
|
|
406
|
+
${strip(out)}
|
|
407
|
+
\`\`\``,
|
|
408
|
+
backButton()
|
|
409
|
+
);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case "switch_mode":
|
|
413
|
+
state.mode = state.mode === "qwen" ? "claude" : "qwen";
|
|
414
|
+
state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
415
|
+
saveState(state);
|
|
416
|
+
await editMessage(
|
|
417
|
+
token,
|
|
418
|
+
chatId,
|
|
419
|
+
messageId,
|
|
420
|
+
`\u{1F916} Mode switched to *${state.mode === "qwen" ? "Qwen (local LLM)" : "Claude (CLI)"}*`,
|
|
421
|
+
mainMenu()
|
|
422
|
+
);
|
|
423
|
+
break;
|
|
424
|
+
case "wake_mac": {
|
|
425
|
+
await editMessage(token, chatId, messageId, "\u{1F4A4} _Sending wake signal..._");
|
|
426
|
+
const result = await wakeMac();
|
|
427
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
428
|
+
const status = await checkMacStatus();
|
|
429
|
+
await editMessage(
|
|
430
|
+
token,
|
|
431
|
+
chatId,
|
|
432
|
+
messageId,
|
|
433
|
+
`\u{1F4A4} *Wake Mac*
|
|
434
|
+
${result}
|
|
435
|
+
|
|
436
|
+
\u{1F50C} ${status.online ? "\u{1F7E2} Online" : "\u{1F534} Offline"}
|
|
437
|
+
\`${status.details}\``,
|
|
438
|
+
[[
|
|
439
|
+
{ text: "\u{1F504} Check Again", callback_data: "mac_status" },
|
|
440
|
+
{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }
|
|
441
|
+
]]
|
|
442
|
+
);
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
case "mac_status": {
|
|
446
|
+
await editMessage(token, chatId, messageId, "\u{1F50C} _Checking Mac..._");
|
|
447
|
+
const status = await checkMacStatus();
|
|
448
|
+
await editMessage(
|
|
449
|
+
token,
|
|
450
|
+
chatId,
|
|
451
|
+
messageId,
|
|
452
|
+
`\u{1F50C} *Mac Status*
|
|
453
|
+
${status.online ? "\u{1F7E2} Online" : "\u{1F534} Offline"}
|
|
454
|
+
\`\`\`
|
|
455
|
+
${status.details}
|
|
456
|
+
\`\`\``,
|
|
457
|
+
[[
|
|
458
|
+
{ text: "\u{1F4A4} Wake", callback_data: "wake_mac" },
|
|
459
|
+
{ text: "\u{1F504} Refresh", callback_data: "mac_status" },
|
|
460
|
+
{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }
|
|
461
|
+
]]
|
|
462
|
+
);
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case "sessions": {
|
|
466
|
+
await editMessage(
|
|
467
|
+
token,
|
|
468
|
+
chatId,
|
|
469
|
+
messageId,
|
|
470
|
+
`\u{1F4CB} *Claude Sessions*
|
|
471
|
+
Mode: *${state.mode}*
|
|
472
|
+
Sessions: ${state.sessionsCount}
|
|
473
|
+
Last: ${state.lastActivity}`,
|
|
474
|
+
claudeMenu()
|
|
475
|
+
);
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case "claude_new":
|
|
479
|
+
await editMessage(
|
|
480
|
+
token,
|
|
481
|
+
chatId,
|
|
482
|
+
messageId,
|
|
483
|
+
"\u{1F4AC} *New Claude Session*\nEnvoie ta question/tache en texte. Claude va la traiter en mode session.",
|
|
484
|
+
backButton()
|
|
485
|
+
);
|
|
486
|
+
break;
|
|
487
|
+
case "claude_continue": {
|
|
488
|
+
await editMessage(token, chatId, messageId, "\u{1F4C2} _Continuing last session..._");
|
|
489
|
+
const out = await claudeSession("Continue the previous task. What was I working on?", true);
|
|
490
|
+
await editMessage(
|
|
491
|
+
token,
|
|
492
|
+
chatId,
|
|
493
|
+
messageId,
|
|
494
|
+
`\u{1F4C2} *Session Continued*
|
|
495
|
+
${out}`,
|
|
496
|
+
[[
|
|
497
|
+
{ text: "\u{1F4AC} Reply", callback_data: "claude_new" },
|
|
498
|
+
{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }
|
|
499
|
+
]]
|
|
500
|
+
);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
case "claude_sessions": {
|
|
504
|
+
const sessions = run("ls -lt ~/.claude/projects/ 2>/dev/null | head -10");
|
|
505
|
+
await editMessage(
|
|
506
|
+
token,
|
|
507
|
+
chatId,
|
|
508
|
+
messageId,
|
|
509
|
+
`\u{1F4CB} *Recent Sessions*
|
|
510
|
+
\`\`\`
|
|
511
|
+
${strip(sessions)}
|
|
512
|
+
\`\`\``,
|
|
513
|
+
claudeMenu()
|
|
514
|
+
);
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case "claude_resume": {
|
|
518
|
+
await editMessage(
|
|
519
|
+
token,
|
|
520
|
+
chatId,
|
|
521
|
+
messageId,
|
|
522
|
+
"\u{1F504} *Resume Session*\nEnvoie le chemin du projet pour reprendre la session.",
|
|
523
|
+
backButton()
|
|
524
|
+
);
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
case "logs": {
|
|
528
|
+
let logs = "No logs yet";
|
|
529
|
+
try {
|
|
530
|
+
if (existsSync(LOG_FILE)) {
|
|
531
|
+
logs = run(`tail -20 "${LOG_FILE}"`);
|
|
532
|
+
}
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
await editMessage(
|
|
536
|
+
token,
|
|
537
|
+
chatId,
|
|
538
|
+
messageId,
|
|
539
|
+
`\u{1F4DD} *Recent Logs*
|
|
540
|
+
\`\`\`
|
|
541
|
+
${truncate(strip(logs), 3e3)}
|
|
542
|
+
\`\`\``,
|
|
543
|
+
backButton()
|
|
544
|
+
);
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
var BLOCKED_COMMANDS = [
|
|
550
|
+
"rm -rf",
|
|
551
|
+
"rm -r /",
|
|
552
|
+
"mkfs",
|
|
553
|
+
"dd if=",
|
|
554
|
+
":(){",
|
|
555
|
+
"chmod -R 777",
|
|
556
|
+
"git push --force main",
|
|
557
|
+
"git push --force master",
|
|
558
|
+
"sudo rm",
|
|
559
|
+
"sudo chmod",
|
|
560
|
+
"eval ",
|
|
561
|
+
"curl | sh",
|
|
562
|
+
"curl | bash",
|
|
563
|
+
"wget | sh",
|
|
564
|
+
"> /dev/sd",
|
|
565
|
+
"shutdown",
|
|
566
|
+
"reboot",
|
|
567
|
+
"init 0"
|
|
568
|
+
];
|
|
569
|
+
async function handleText(token, chatId, text, from) {
|
|
570
|
+
const cmd = text.trim().toLowerCase();
|
|
571
|
+
if (cmd === "/start" || cmd === "/menu" || cmd === "/help" || cmd === "/h") {
|
|
572
|
+
await send(token, chatId, "\u{1F996} *REX Gateway v3*\nChoisis une action :", mainMenu());
|
|
573
|
+
logCommand(from, cmd, "menu");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (cmd === "/status" || cmd === "/s") {
|
|
577
|
+
const out = strip(run("rex status"));
|
|
578
|
+
await send(token, chatId, `\u{1F4CA} ${out}`, backButton());
|
|
579
|
+
logCommand(from, "/status", out);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (cmd === "/wake" || cmd === "/w") {
|
|
583
|
+
await send(token, chatId, "\u{1F4A4} _Sending wake signal..._");
|
|
584
|
+
const result = await wakeMac();
|
|
585
|
+
await send(token, chatId, result, backButton());
|
|
586
|
+
logCommand(from, "/wake", result);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (cmd === "/doctor" || cmd === "/d") {
|
|
590
|
+
await send(token, chatId, "\u{1FA7A} _Running diagnostics..._");
|
|
591
|
+
const out = truncate(strip(run("rex doctor")));
|
|
592
|
+
await send(token, chatId, `\u{1FA7A}
|
|
593
|
+
\`\`\`
|
|
594
|
+
${out}
|
|
595
|
+
\`\`\``, backButton());
|
|
596
|
+
logCommand(from, "/doctor", "done");
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (cmd === "/ingest" || cmd === "/i") {
|
|
600
|
+
await send(token, chatId, "\u{1F4E5} _Ingesting..._");
|
|
601
|
+
const out = truncate(strip(run("rex ingest", 12e4)));
|
|
602
|
+
await send(token, chatId, `\u{1F4E5}
|
|
603
|
+
\`\`\`
|
|
604
|
+
${out}
|
|
605
|
+
\`\`\``, backButton());
|
|
606
|
+
logCommand(from, "/ingest", "done");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (cmd === "/prune") {
|
|
610
|
+
await send(token, chatId, "\u{1F9F9} _Pruning..._");
|
|
611
|
+
const out = truncate(strip(run("rex prune", 6e4)));
|
|
612
|
+
await send(token, chatId, `\u{1F9F9}
|
|
613
|
+
\`\`\`
|
|
614
|
+
${out}
|
|
615
|
+
\`\`\``, backButton());
|
|
616
|
+
logCommand(from, "/prune", "done");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (cmd.startsWith("/search ") || cmd.startsWith("/q ")) {
|
|
620
|
+
const query = text.replace(/^\/(search|q)\s+/i, "");
|
|
621
|
+
if (!query) {
|
|
622
|
+
await send(token, chatId, "Usage: /search <query>");
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const out = run(`rex search ${query}`);
|
|
626
|
+
await send(
|
|
627
|
+
token,
|
|
628
|
+
chatId,
|
|
629
|
+
out ? `\u{1F50D} *Search:* ${query}
|
|
630
|
+
\`\`\`
|
|
631
|
+
${truncate(out, 3e3)}
|
|
632
|
+
\`\`\`` : "No results",
|
|
633
|
+
backButton()
|
|
634
|
+
);
|
|
635
|
+
logCommand(from, `/search ${query}`, out ? "found" : "empty");
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (cmd.startsWith("/sh ") || cmd.startsWith("/run ")) {
|
|
639
|
+
const shellCmd = text.replace(/^\/(sh|run)\s+/i, "");
|
|
640
|
+
if (BLOCKED_COMMANDS.some((b) => shellCmd.toLowerCase().includes(b))) {
|
|
641
|
+
await send(token, chatId, "\u{1F6AB} Blocked: dangerous command");
|
|
642
|
+
logCommand(from, `/sh ${shellCmd}`, "BLOCKED");
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const out = run(shellCmd);
|
|
646
|
+
await send(token, chatId, `\`$ ${shellCmd}\`
|
|
647
|
+
\`\`\`
|
|
648
|
+
${truncate(out, 3500)}
|
|
649
|
+
\`\`\``, backButton());
|
|
650
|
+
logCommand(from, `/sh ${shellCmd}`, out.slice(0, 100));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (cmd.startsWith("/mode")) {
|
|
654
|
+
state.mode = state.mode === "qwen" ? "claude" : "qwen";
|
|
655
|
+
state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
656
|
+
saveState(state);
|
|
657
|
+
await send(
|
|
658
|
+
token,
|
|
659
|
+
chatId,
|
|
660
|
+
`\u{1F916} Switched to *${state.mode === "qwen" ? "Qwen (local)" : "Claude"}*`,
|
|
661
|
+
mainMenu()
|
|
662
|
+
);
|
|
663
|
+
logCommand(from, "/mode", state.mode);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (cmd === "/claude" || cmd === "/c") {
|
|
667
|
+
await send(token, chatId, "\u{1F916} *Claude Remote*\nGere tes sessions Claude a distance :", claudeMenu());
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (cmd.startsWith("/claude ") || cmd.startsWith("/c ")) {
|
|
671
|
+
const prompt = text.replace(/^\/(claude|c)\s+/i, "");
|
|
672
|
+
await send(token, chatId, "\u{1F916} _Claude is thinking..._");
|
|
673
|
+
const out = await claudeSession(prompt);
|
|
674
|
+
await send(token, chatId, out, [
|
|
675
|
+
[
|
|
676
|
+
{ text: "\u{1F4AC} Continue", callback_data: "claude_continue" },
|
|
677
|
+
{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }
|
|
678
|
+
]
|
|
679
|
+
]);
|
|
680
|
+
logCommand(from, `/claude ${prompt.slice(0, 50)}`, out.slice(0, 100));
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (text.length > 2) {
|
|
684
|
+
const modeLabel = state.mode === "qwen" ? "\u{1F9E0} Qwen" : "\u{1F916} Claude";
|
|
685
|
+
await send(token, chatId, `${modeLabel} _thinking..._`);
|
|
686
|
+
state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
687
|
+
saveState(state);
|
|
688
|
+
let response;
|
|
689
|
+
if (state.mode === "claude") {
|
|
690
|
+
response = await claudeSession(text);
|
|
691
|
+
} else {
|
|
692
|
+
response = await askLLM(text);
|
|
693
|
+
}
|
|
694
|
+
await send(token, chatId, response, [
|
|
695
|
+
[
|
|
696
|
+
{ text: `Mode: ${state.mode}`, callback_data: "switch_mode" },
|
|
697
|
+
{ text: state.mode === "claude" ? "\u{1F4AC} Continue" : "\u25C0\uFE0F Menu", callback_data: state.mode === "claude" ? "claude_continue" : "menu" }
|
|
698
|
+
]
|
|
699
|
+
]);
|
|
700
|
+
logCommand(from, text.slice(0, 80), response.slice(0, 100));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
await send(token, chatId, "\u{1F996} *REX*\nEnvoie un message ou appuie sur Menu :", mainMenu());
|
|
704
|
+
}
|
|
705
|
+
async function gateway() {
|
|
706
|
+
const creds = getCredentials();
|
|
707
|
+
if (!creds) {
|
|
708
|
+
console.error(`${COLORS.red}No Telegram credentials found.${COLORS.reset}`);
|
|
709
|
+
console.error(`Run ${COLORS.cyan}rex setup${COLORS.reset} to configure Telegram gateway.`);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
const { token, chatId } = creds;
|
|
713
|
+
config = loadConfig();
|
|
714
|
+
state = loadState();
|
|
715
|
+
console.log(`${COLORS.bold}REX Gateway v3${COLORS.reset} \u2014 Interactive Telegram bot`);
|
|
716
|
+
console.log(`${COLORS.dim}Chat: ${chatId} | Mode: ${state.mode} | Sessions: ${state.sessionsCount}${COLORS.reset}`);
|
|
717
|
+
console.log(`${COLORS.dim}Auth: restricted to chat_id ${chatId}${COLORS.reset}`);
|
|
718
|
+
console.log(`${COLORS.dim}Ctrl+C to stop${COLORS.reset}
|
|
719
|
+
`);
|
|
720
|
+
let offset = 0;
|
|
721
|
+
try {
|
|
722
|
+
const flush = await fetch(`https://api.telegram.org/bot${token}/getUpdates?offset=-1`);
|
|
723
|
+
const flushData = await flush.json();
|
|
724
|
+
if (flushData.result?.length) {
|
|
725
|
+
offset = flushData.result[flushData.result.length - 1].update_id + 1;
|
|
726
|
+
}
|
|
727
|
+
} catch {
|
|
728
|
+
}
|
|
729
|
+
await send(token, chatId, `\u{1F7E2} *REX Gateway v3* started
|
|
730
|
+
Mode: ${state.mode} | Sessions: ${state.sessionsCount}`, mainMenu());
|
|
731
|
+
process.on("SIGINT", async () => {
|
|
732
|
+
console.log(`
|
|
733
|
+
${COLORS.dim}Shutting down...${COLORS.reset}`);
|
|
734
|
+
state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
735
|
+
saveState(state);
|
|
736
|
+
await send(token, chatId, "\u{1F534} *REX Gateway* stopped");
|
|
737
|
+
process.exit(0);
|
|
738
|
+
});
|
|
739
|
+
process.on("SIGTERM", async () => {
|
|
740
|
+
state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
741
|
+
saveState(state);
|
|
742
|
+
await send(token, chatId, "\u{1F534} *REX Gateway* stopped (SIGTERM)");
|
|
743
|
+
process.exit(0);
|
|
744
|
+
});
|
|
745
|
+
while (true) {
|
|
746
|
+
try {
|
|
747
|
+
const res = await fetch(
|
|
748
|
+
`https://api.telegram.org/bot${token}/getUpdates?offset=${offset}&timeout=${config.pollTimeout}&allowed_updates=["message","callback_query"]`
|
|
749
|
+
);
|
|
750
|
+
const data = await res.json();
|
|
751
|
+
if (!data.ok || !data.result?.length) continue;
|
|
752
|
+
for (const update of data.result) {
|
|
753
|
+
offset = update.update_id + 1;
|
|
754
|
+
if (update.callback_query) {
|
|
755
|
+
const cb = update.callback_query;
|
|
756
|
+
const cbChatId = String(cb.message?.chat?.id);
|
|
757
|
+
if (!isAuthorized(cbChatId, chatId)) {
|
|
758
|
+
await answerCallback(token, cb.id, "\u{1F6AB} Unauthorized");
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
const from2 = cb.from?.username ?? "?";
|
|
762
|
+
console.log(`${COLORS.cyan}@${from2}${COLORS.reset} [btn] ${cb.data}`);
|
|
763
|
+
await handleCallback(token, chatId, cb.message.message_id, cb.id, cb.data, from2);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const msg = update.message;
|
|
767
|
+
if (!msg?.text) continue;
|
|
768
|
+
if (!isAuthorized(msg.chat.id, chatId)) {
|
|
769
|
+
await send(token, String(msg.chat.id), "\u{1F6AB} Unauthorized. This REX instance is private.");
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const from = msg.from?.username ?? "?";
|
|
773
|
+
console.log(`${COLORS.cyan}@${from}${COLORS.reset}: ${msg.text}`);
|
|
774
|
+
await handleText(token, chatId, msg.text, from);
|
|
775
|
+
}
|
|
776
|
+
} catch (err) {
|
|
777
|
+
console.error(`${COLORS.red}Poll error:${COLORS.reset} ${err.message}`);
|
|
778
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
export {
|
|
783
|
+
gateway
|
|
784
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -371,7 +371,7 @@ async function main() {
|
|
|
371
371
|
break;
|
|
372
372
|
}
|
|
373
373
|
case "init": {
|
|
374
|
-
const { init } = await import("./init-
|
|
374
|
+
const { init } = await import("./init-DLFEGD6O.js");
|
|
375
375
|
await init();
|
|
376
376
|
break;
|
|
377
377
|
}
|
|
@@ -416,6 +416,12 @@ async function main() {
|
|
|
416
416
|
await optimize(applyFlag);
|
|
417
417
|
break;
|
|
418
418
|
}
|
|
419
|
+
case "prune": {
|
|
420
|
+
const { prune } = await import("./prune-2PPIVDXK.js");
|
|
421
|
+
const statsFlag = process.argv.includes("--stats");
|
|
422
|
+
await prune(statsFlag);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
419
425
|
case "setup": {
|
|
420
426
|
const { setup } = await import("./setup-AO3MW46W.js");
|
|
421
427
|
await setup();
|
|
@@ -439,23 +445,23 @@ async function main() {
|
|
|
439
445
|
break;
|
|
440
446
|
}
|
|
441
447
|
case "gateway": {
|
|
442
|
-
const { gateway } = await import("./gateway-
|
|
448
|
+
const { gateway } = await import("./gateway-EKMU5D7J.js");
|
|
443
449
|
await gateway();
|
|
444
450
|
break;
|
|
445
451
|
}
|
|
446
452
|
case "startup": {
|
|
447
|
-
const { installStartup } = await import("./init-
|
|
453
|
+
const { installStartup } = await import("./init-DLFEGD6O.js");
|
|
448
454
|
installStartup();
|
|
449
455
|
break;
|
|
450
456
|
}
|
|
451
457
|
case "startup-remove": {
|
|
452
|
-
const { uninstallStartup } = await import("./init-
|
|
458
|
+
const { uninstallStartup } = await import("./init-DLFEGD6O.js");
|
|
453
459
|
uninstallStartup();
|
|
454
460
|
break;
|
|
455
461
|
}
|
|
456
462
|
case "--version":
|
|
457
463
|
case "-v":
|
|
458
|
-
console.log("rex-claude
|
|
464
|
+
console.log("rex-claude v4.0.0");
|
|
459
465
|
break;
|
|
460
466
|
case "help":
|
|
461
467
|
default:
|
|
@@ -474,6 +480,8 @@ ${COLORS.bold}Memory (requires Ollama):${COLORS.reset}
|
|
|
474
480
|
rex search <query> Semantic search across past sessions
|
|
475
481
|
rex optimize Analyze CLAUDE.md with local LLM
|
|
476
482
|
rex optimize --apply Apply optimizations (with backup)
|
|
483
|
+
rex prune Cleanup old/duplicate memories
|
|
484
|
+
rex prune --stats Show memory database stats
|
|
477
485
|
|
|
478
486
|
${COLORS.bold}LLM & Context:${COLORS.reset}
|
|
479
487
|
rex setup Install Ollama + models + Telegram gateway
|
|
@@ -213,7 +213,7 @@ function uninstallGatewayAgent() {
|
|
|
213
213
|
function installApp() {
|
|
214
214
|
if (process.platform !== "darwin") return;
|
|
215
215
|
const thisDir = new URL(".", import.meta.url).pathname;
|
|
216
|
-
const buildApp = join(thisDir, "..", "..", "
|
|
216
|
+
const buildApp = join(thisDir, "..", "..", "flutter_app", "build", "macos", "Build", "Products", "Release", "rex_app.app");
|
|
217
217
|
const installedApp = "/Applications/REX.app";
|
|
218
218
|
if (existsSync(installedApp)) {
|
|
219
219
|
skip("REX.app already in /Applications");
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/prune.ts
|
|
4
|
+
import { existsSync, statSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
var COLORS = {
|
|
8
|
+
reset: "\x1B[0m",
|
|
9
|
+
green: "\x1B[32m",
|
|
10
|
+
yellow: "\x1B[33m",
|
|
11
|
+
red: "\x1B[31m",
|
|
12
|
+
bold: "\x1B[1m",
|
|
13
|
+
dim: "\x1B[2m",
|
|
14
|
+
cyan: "\x1B[36m"
|
|
15
|
+
};
|
|
16
|
+
var DB_PATH = join(homedir(), ".rex-memory", "db", "rex.sqlite");
|
|
17
|
+
var MAX_AGE_DAYS = 180;
|
|
18
|
+
var MAX_DB_SIZE_MB = 50;
|
|
19
|
+
function formatSize(bytes) {
|
|
20
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
21
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
22
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
23
|
+
}
|
|
24
|
+
async function prune(statsOnly = false) {
|
|
25
|
+
const line = "\u2550".repeat(45);
|
|
26
|
+
console.log(`
|
|
27
|
+
${line}`);
|
|
28
|
+
console.log(`${COLORS.bold} REX ${statsOnly ? "MEMORY STATS" : "PRUNE"}${COLORS.reset}`);
|
|
29
|
+
console.log(`${line}
|
|
30
|
+
`);
|
|
31
|
+
if (!existsSync(DB_PATH)) {
|
|
32
|
+
console.log(` ${COLORS.yellow}No memory database found.${COLORS.reset}`);
|
|
33
|
+
console.log(` Run ${COLORS.cyan}rex ingest${COLORS.reset} first.
|
|
34
|
+
`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
let Database;
|
|
38
|
+
let sqliteVec;
|
|
39
|
+
try {
|
|
40
|
+
Database = (await import("better-sqlite3")).default;
|
|
41
|
+
sqliteVec = await import("sqlite-vec");
|
|
42
|
+
} catch {
|
|
43
|
+
const memDir = join(homedir(), ".rex-memory");
|
|
44
|
+
if (!existsSync(join(memDir, "node_modules", "better-sqlite3"))) {
|
|
45
|
+
console.log(` ${COLORS.yellow}better-sqlite3 not available.${COLORS.reset}`);
|
|
46
|
+
console.log(` Ensure @rex/memory is installed: ${COLORS.cyan}cd ~/.rex-memory && npm install${COLORS.reset}
|
|
47
|
+
`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
Database = (await import(join(memDir, "node_modules", "better-sqlite3", "lib", "index.js"))).default;
|
|
52
|
+
sqliteVec = await import(join(memDir, "node_modules", "sqlite-vec", "src", "index.js"));
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.log(` ${COLORS.red}Cannot load database modules:${COLORS.reset} ${err.message}
|
|
55
|
+
`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const db = new Database(DB_PATH);
|
|
60
|
+
try {
|
|
61
|
+
sqliteVec.load(db);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
db.pragma("journal_mode = WAL");
|
|
65
|
+
const totalMemories = db.prepare("SELECT COUNT(*) as c FROM memories").get().c;
|
|
66
|
+
const categories = db.prepare("SELECT category, COUNT(*) as c FROM memories GROUP BY category ORDER BY c DESC").all();
|
|
67
|
+
const projects = db.prepare("SELECT project, COUNT(*) as c FROM memories GROUP BY project ORDER BY c DESC LIMIT 10").all();
|
|
68
|
+
const ingestFiles = db.prepare("SELECT COUNT(*) as c FROM ingest_log").get().c;
|
|
69
|
+
const dbSize = statSync(DB_PATH).size;
|
|
70
|
+
console.log(` ${COLORS.bold}Database:${COLORS.reset} ${formatSize(dbSize)}`);
|
|
71
|
+
console.log(` ${COLORS.bold}Total memories:${COLORS.reset} ${totalMemories}`);
|
|
72
|
+
console.log(` ${COLORS.bold}Ingested files:${COLORS.reset} ${ingestFiles}`);
|
|
73
|
+
console.log();
|
|
74
|
+
console.log(` ${COLORS.bold}By category:${COLORS.reset}`);
|
|
75
|
+
for (const cat of categories) {
|
|
76
|
+
console.log(` ${COLORS.cyan}${cat.category}${COLORS.reset}: ${cat.c}`);
|
|
77
|
+
}
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(` ${COLORS.bold}Top projects:${COLORS.reset}`);
|
|
80
|
+
for (const proj of projects) {
|
|
81
|
+
console.log(` ${COLORS.dim}${proj.project || "(none)"}${COLORS.reset}: ${proj.c}`);
|
|
82
|
+
}
|
|
83
|
+
if (statsOnly) {
|
|
84
|
+
console.log();
|
|
85
|
+
db.close();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
89
|
+
cutoffDate.setDate(cutoffDate.getDate() - MAX_AGE_DAYS);
|
|
90
|
+
const cutoff = cutoffDate.toISOString();
|
|
91
|
+
const oldCount = db.prepare("SELECT COUNT(*) as c FROM memories WHERE created_at < ?").get(cutoff).c;
|
|
92
|
+
if (oldCount === 0 && dbSize < MAX_DB_SIZE_MB * 1024 * 1024) {
|
|
93
|
+
console.log(`
|
|
94
|
+
${COLORS.green}\u2713${COLORS.reset} Nothing to prune (all memories < ${MAX_AGE_DAYS} days, DB < ${MAX_DB_SIZE_MB}MB)`);
|
|
95
|
+
db.close();
|
|
96
|
+
console.log();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
console.log(`
|
|
100
|
+
${COLORS.bold}Pruning:${COLORS.reset}`);
|
|
101
|
+
if (oldCount > 0) {
|
|
102
|
+
const oldIds = db.prepare("SELECT id FROM memories WHERE created_at < ?").all(cutoff);
|
|
103
|
+
const deleteVec = db.prepare("DELETE FROM memory_vec WHERE rowid = ?");
|
|
104
|
+
const deleteMem = db.prepare("DELETE FROM memories WHERE id = ?");
|
|
105
|
+
const tx = db.transaction(() => {
|
|
106
|
+
for (const row of oldIds) {
|
|
107
|
+
try {
|
|
108
|
+
deleteVec.run(row.id);
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
deleteMem.run(row.id);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
tx();
|
|
115
|
+
console.log(` ${COLORS.green}\u2713${COLORS.reset} Removed ${oldCount} memories older than ${MAX_AGE_DAYS} days`);
|
|
116
|
+
}
|
|
117
|
+
const dupes = db.prepare(`
|
|
118
|
+
SELECT id FROM memories WHERE id NOT IN (
|
|
119
|
+
SELECT MIN(id) FROM memories GROUP BY content
|
|
120
|
+
)
|
|
121
|
+
`).all();
|
|
122
|
+
if (dupes.length > 0) {
|
|
123
|
+
const deleteVec = db.prepare("DELETE FROM memory_vec WHERE rowid = ?");
|
|
124
|
+
const deleteMem = db.prepare("DELETE FROM memories WHERE id = ?");
|
|
125
|
+
const tx = db.transaction(() => {
|
|
126
|
+
for (const row of dupes) {
|
|
127
|
+
try {
|
|
128
|
+
deleteVec.run(row.id);
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
deleteMem.run(row.id);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
tx();
|
|
135
|
+
console.log(` ${COLORS.green}\u2713${COLORS.reset} Removed ${dupes.length} duplicate memories`);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
db.exec("VACUUM");
|
|
139
|
+
const newSize = statSync(DB_PATH).size;
|
|
140
|
+
console.log(` ${COLORS.green}\u2713${COLORS.reset} Compacted: ${formatSize(dbSize)} -> ${formatSize(newSize)}`);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
const remaining = db.prepare("SELECT COUNT(*) as c FROM memories").get().c;
|
|
144
|
+
console.log(`
|
|
145
|
+
${COLORS.bold}Remaining:${COLORS.reset} ${remaining} memories`);
|
|
146
|
+
db.close();
|
|
147
|
+
console.log();
|
|
148
|
+
}
|
|
149
|
+
export {
|
|
150
|
+
prune
|
|
151
|
+
};
|
package/package.json
CHANGED
package/dist/gateway-EOVQXRON.js
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/gateway.ts
|
|
4
|
-
import { homedir } from "os";
|
|
5
|
-
import { readFileSync } from "fs";
|
|
6
|
-
import { join } from "path";
|
|
7
|
-
import { execSync } from "child_process";
|
|
8
|
-
var COLORS = {
|
|
9
|
-
reset: "\x1B[0m",
|
|
10
|
-
green: "\x1B[32m",
|
|
11
|
-
yellow: "\x1B[33m",
|
|
12
|
-
red: "\x1B[31m",
|
|
13
|
-
dim: "\x1B[2m",
|
|
14
|
-
bold: "\x1B[1m",
|
|
15
|
-
cyan: "\x1B[36m"
|
|
16
|
-
};
|
|
17
|
-
function getCredentials() {
|
|
18
|
-
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
19
|
-
try {
|
|
20
|
-
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
21
|
-
const token2 = settings.env?.REX_TELEGRAM_BOT_TOKEN;
|
|
22
|
-
const chatId2 = settings.env?.REX_TELEGRAM_CHAT_ID;
|
|
23
|
-
if (token2 && chatId2) return { token: token2, chatId: chatId2 };
|
|
24
|
-
} catch {
|
|
25
|
-
}
|
|
26
|
-
const token = process.env.REX_TELEGRAM_BOT_TOKEN;
|
|
27
|
-
const chatId = process.env.REX_TELEGRAM_CHAT_ID;
|
|
28
|
-
if (token && chatId) return { token, chatId };
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
async function sendMessage(token, chatId, text, parseMode = "Markdown") {
|
|
32
|
-
try {
|
|
33
|
-
await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
|
34
|
-
method: "POST",
|
|
35
|
-
headers: { "Content-Type": "application/json" },
|
|
36
|
-
body: JSON.stringify({ chat_id: chatId, text, parse_mode: parseMode })
|
|
37
|
-
});
|
|
38
|
-
} catch {
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function runCommand(cmd) {
|
|
42
|
-
try {
|
|
43
|
-
return execSync(cmd, { timeout: 3e4, encoding: "utf-8" }).trim();
|
|
44
|
-
} catch (e) {
|
|
45
|
-
return e.stderr?.trim() || e.message || "Command failed";
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
async function handleCommand(text) {
|
|
49
|
-
const cmd = text.trim().toLowerCase();
|
|
50
|
-
if (cmd === "/status" || cmd === "/s") {
|
|
51
|
-
const out = runCommand("rex status");
|
|
52
|
-
return out || "REX status unavailable";
|
|
53
|
-
}
|
|
54
|
-
if (cmd === "/doctor" || cmd === "/d") {
|
|
55
|
-
const out = runCommand("rex doctor");
|
|
56
|
-
return out.replace(/\x1b\[[0-9;]*m/g, "").slice(0, 4e3);
|
|
57
|
-
}
|
|
58
|
-
if (cmd === "/ingest" || cmd === "/i") {
|
|
59
|
-
const out = runCommand("rex ingest");
|
|
60
|
-
return `*Ingest*
|
|
61
|
-
\`\`\`
|
|
62
|
-
${out.slice(0, 3e3)}
|
|
63
|
-
\`\`\``;
|
|
64
|
-
}
|
|
65
|
-
if (cmd.startsWith("/search ") || cmd.startsWith("/q ")) {
|
|
66
|
-
const query = text.replace(/^\/(search|q)\s+/i, "");
|
|
67
|
-
if (!query) return "Usage: /search <query>";
|
|
68
|
-
const out = runCommand(`rex search ${query}`);
|
|
69
|
-
return out ? `*Search:* ${query}
|
|
70
|
-
\`\`\`
|
|
71
|
-
${out.slice(0, 3e3)}
|
|
72
|
-
\`\`\`` : "No results";
|
|
73
|
-
}
|
|
74
|
-
if (cmd.startsWith("/llm ") || cmd.startsWith("/ask ")) {
|
|
75
|
-
const prompt = text.replace(/^\/(llm|ask)\s+/i, "");
|
|
76
|
-
if (!prompt) return "Usage: /llm <prompt>";
|
|
77
|
-
const out = runCommand(`rex llm "${prompt.replace(/"/g, '\\"')}"`);
|
|
78
|
-
return out.slice(0, 4e3);
|
|
79
|
-
}
|
|
80
|
-
if (cmd === "/optimize" || cmd === "/o") {
|
|
81
|
-
const out = runCommand("rex optimize");
|
|
82
|
-
return `*Optimize*
|
|
83
|
-
\`\`\`
|
|
84
|
-
${out.replace(/\x1b\[[0-9;]*m/g, "").slice(0, 3e3)}
|
|
85
|
-
\`\`\``;
|
|
86
|
-
}
|
|
87
|
-
if (cmd === "/git" || cmd === "/g") {
|
|
88
|
-
const branch = runCommand('git branch --show-current 2>/dev/null || echo "n/a"');
|
|
89
|
-
const status = runCommand("git status --short 2>/dev/null | head -15");
|
|
90
|
-
const lastCommit = runCommand('git log -1 --format="%s" 2>/dev/null || echo "n/a"');
|
|
91
|
-
return `*Git*
|
|
92
|
-
Branch: \`${branch}\`
|
|
93
|
-
Last: ${lastCommit}
|
|
94
|
-
\`\`\`
|
|
95
|
-
${status || "Clean"}
|
|
96
|
-
\`\`\``;
|
|
97
|
-
}
|
|
98
|
-
if (cmd.startsWith("/sh ") || cmd.startsWith("/run ")) {
|
|
99
|
-
const shellCmd = text.replace(/^\/(sh|run)\s+/i, "");
|
|
100
|
-
const blocked = ["rm -rf", "rm -r /", "mkfs", "dd if=", ":(){", "chmod -R 777", "git push --force main", "git push --force master"];
|
|
101
|
-
if (blocked.some((b) => shellCmd.toLowerCase().includes(b))) {
|
|
102
|
-
return "Blocked: dangerous command";
|
|
103
|
-
}
|
|
104
|
-
const out = runCommand(shellCmd);
|
|
105
|
-
return `\`$ ${shellCmd}\`
|
|
106
|
-
\`\`\`
|
|
107
|
-
${out.slice(0, 3500)}
|
|
108
|
-
\`\`\``;
|
|
109
|
-
}
|
|
110
|
-
if (cmd === "/help" || cmd === "/start" || cmd === "/h") {
|
|
111
|
-
return `*REX Gateway*
|
|
112
|
-
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
113
|
-
/status \u2014 Quick health check
|
|
114
|
-
/doctor \u2014 Full diagnostics
|
|
115
|
-
/ingest \u2014 Sync sessions to memory
|
|
116
|
-
/search <q> \u2014 Semantic search
|
|
117
|
-
/llm <prompt> \u2014 Ask local LLM
|
|
118
|
-
/optimize \u2014 Analyze CLAUDE.md
|
|
119
|
-
/git \u2014 Git status
|
|
120
|
-
/sh <cmd> \u2014 Run shell command
|
|
121
|
-
/help \u2014 This message`;
|
|
122
|
-
}
|
|
123
|
-
if (text.length > 3) {
|
|
124
|
-
try {
|
|
125
|
-
const check = await fetch("http://localhost:11434/api/tags");
|
|
126
|
-
if (check.ok) {
|
|
127
|
-
const out = runCommand(`rex llm "${text.replace(/"/g, '\\"')}"`);
|
|
128
|
-
if (out && !out.includes("rex-claude") && !out.includes("Commands:")) {
|
|
129
|
-
return out.slice(0, 4e3);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
} catch {
|
|
133
|
-
}
|
|
134
|
-
return `Unknown command: "${text.slice(0, 50)}"
|
|
135
|
-
Send /help for available commands.`;
|
|
136
|
-
}
|
|
137
|
-
return "Send /help for available commands.";
|
|
138
|
-
}
|
|
139
|
-
async function gateway() {
|
|
140
|
-
const creds = getCredentials();
|
|
141
|
-
if (!creds) {
|
|
142
|
-
console.error(`${COLORS.red}No Telegram credentials found.${COLORS.reset}`);
|
|
143
|
-
console.error(`Run ${COLORS.cyan}rex setup${COLORS.reset} to configure Telegram gateway.`);
|
|
144
|
-
process.exit(1);
|
|
145
|
-
}
|
|
146
|
-
const { token, chatId } = creds;
|
|
147
|
-
console.log(`${COLORS.bold}REX Gateway${COLORS.reset} \u2014 Telegram long-polling active`);
|
|
148
|
-
console.log(`${COLORS.dim}Bot token: ...${token.slice(-8)}${COLORS.reset}`);
|
|
149
|
-
console.log(`${COLORS.dim}Chat ID: ${chatId}${COLORS.reset}`);
|
|
150
|
-
console.log(`${COLORS.dim}Press Ctrl+C to stop${COLORS.reset}
|
|
151
|
-
`);
|
|
152
|
-
let offset = 0;
|
|
153
|
-
try {
|
|
154
|
-
const flush = await fetch(`https://api.telegram.org/bot${token}/getUpdates?offset=-1`);
|
|
155
|
-
const flushData = await flush.json();
|
|
156
|
-
if (flushData.result?.length) {
|
|
157
|
-
offset = flushData.result[flushData.result.length - 1].update_id + 1;
|
|
158
|
-
}
|
|
159
|
-
} catch {
|
|
160
|
-
}
|
|
161
|
-
await sendMessage(token, chatId, "\u{1F7E2} *REX Gateway* started\nSend /help for commands.");
|
|
162
|
-
const POLL_TIMEOUT = 30;
|
|
163
|
-
process.on("SIGINT", async () => {
|
|
164
|
-
console.log(`
|
|
165
|
-
${COLORS.dim}Shutting down...${COLORS.reset}`);
|
|
166
|
-
await sendMessage(token, chatId, "\u{1F534} *REX Gateway* stopped");
|
|
167
|
-
process.exit(0);
|
|
168
|
-
});
|
|
169
|
-
while (true) {
|
|
170
|
-
try {
|
|
171
|
-
const res = await fetch(
|
|
172
|
-
`https://api.telegram.org/bot${token}/getUpdates?offset=${offset}&timeout=${POLL_TIMEOUT}&allowed_updates=["message"]`
|
|
173
|
-
);
|
|
174
|
-
const data = await res.json();
|
|
175
|
-
if (!data.ok || !data.result?.length) continue;
|
|
176
|
-
for (const update of data.result) {
|
|
177
|
-
offset = update.update_id + 1;
|
|
178
|
-
const msg = update.message;
|
|
179
|
-
if (!msg?.text) continue;
|
|
180
|
-
if (String(msg.chat.id) !== chatId) {
|
|
181
|
-
console.log(`${COLORS.yellow}Ignored message from chat ${msg.chat.id}${COLORS.reset}`);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
const from = msg.from?.username ?? "?";
|
|
185
|
-
console.log(`${COLORS.cyan}@${from}${COLORS.reset}: ${msg.text}`);
|
|
186
|
-
const reply = await handleCommand(msg.text);
|
|
187
|
-
await sendMessage(token, chatId, reply);
|
|
188
|
-
console.log(`${COLORS.green}\u2192${COLORS.reset} ${reply.slice(0, 80)}...`);
|
|
189
|
-
}
|
|
190
|
-
} catch (err) {
|
|
191
|
-
console.error(`${COLORS.red}Poll error:${COLORS.reset} ${err.message}`);
|
|
192
|
-
await new Promise((r) => setTimeout(r, 5e3));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
export {
|
|
197
|
-
gateway
|
|
198
|
-
};
|