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.
@@ -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-W3XGDQ6D.js");
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-EOVQXRON.js");
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-W3XGDQ6D.js");
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-W3XGDQ6D.js");
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 v3.0.0");
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, "..", "..", "app", "src-tauri", "target", "release", "bundle", "macos", "REX.app");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rex-claude",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Claude Code sous steroides — guards, health checks, memory RAG",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- };