rex-claude 4.0.0 → 6.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.
Files changed (38) hide show
  1. package/dist/agents-JIZXXASP.js +853 -0
  2. package/dist/app-3VWDSH5F.js +248 -0
  3. package/dist/audio-US2J627E.js +196 -0
  4. package/dist/audit-ZVTGE4L4.js +8 -0
  5. package/dist/call-AQZ3Z5SE.js +143 -0
  6. package/dist/chunk-5ND7JYY3.js +62 -0
  7. package/dist/chunk-6SRV2I2H.js +56 -0
  8. package/dist/{setup-AO3MW46W.js → chunk-A7ZLQUOX.js} +93 -16
  9. package/dist/chunk-E5UYN3W7.js +105 -0
  10. package/dist/chunk-HAHJD3QH.js +147 -0
  11. package/dist/{init-DLFEGD6O.js → chunk-KR7ISYZH.js} +328 -29
  12. package/dist/chunk-LTOM55UV.js +154 -0
  13. package/dist/chunk-PDX44BCA.js +11 -0
  14. package/dist/chunk-PPGYFMU5.js +67 -0
  15. package/dist/{chunk-7AGI43F5.js → chunk-WBMVBMWB.js} +4 -2
  16. package/dist/{context-FN5O5YBI.js → context-XNCG2M5Q.js} +2 -1
  17. package/dist/daemon-5KNSNFTD.js +208 -0
  18. package/dist/gateway-YLP66MCQ.js +2273 -0
  19. package/dist/hammerspoon/rex-call-watcher.lua +186 -0
  20. package/dist/index.js +309 -15
  21. package/dist/init-RDZFIBLA.js +30 -0
  22. package/dist/install-63JBDPRU.js +41 -0
  23. package/dist/{llm-YRORUH7E.js → llm-RALIPIMI.js} +2 -1
  24. package/dist/mcp_registry-DX4GGSP6.js +514 -0
  25. package/dist/migrate-GDO37TI5.js +87 -0
  26. package/dist/{optimize-UKMAGQQE.js → optimize-5TE5RKZV.js} +2 -1
  27. package/dist/paths-4SECM6E6.js +38 -0
  28. package/dist/preload-I3MYBVNU.js +78 -0
  29. package/dist/projects-V6TSLO7E.js +17 -0
  30. package/dist/{prune-2PPIVDXK.js → prune-B7F5B5OF.js} +2 -1
  31. package/dist/recategorize-YXYIMQLZ.js +155 -0
  32. package/dist/router-2JD34COX.js +12 -0
  33. package/dist/self-improve-YK7RCYF4.js +197 -0
  34. package/dist/setup-KNDTVFO6.js +8 -0
  35. package/dist/skills-AIWFY5NH.js +374 -0
  36. package/dist/voice-RITC3EVC.js +248 -0
  37. package/package.json +12 -3
  38. package/dist/gateway-EKMU5D7J.js +0 -784
@@ -0,0 +1,2273 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ __require
4
+ } from "./chunk-PDX44BCA.js";
5
+
6
+ // src/gateway.ts
7
+ import { homedir } from "os";
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "fs";
9
+ import { join, basename, extname } from "path";
10
+ import { execSync, execFileSync } from "child_process";
11
+ var LOCK_FILE = join(homedir(), ".rex-memory", "gateway.lock");
12
+ function acquireLock() {
13
+ const pid = process.pid;
14
+ try {
15
+ if (existsSync(LOCK_FILE)) {
16
+ const existing = parseInt(readFileSync(LOCK_FILE, "utf-8").trim(), 10);
17
+ if (existing && existing !== pid) {
18
+ try {
19
+ process.kill(existing, 0);
20
+ return false;
21
+ } catch {
22
+ }
23
+ }
24
+ }
25
+ const dir = join(homedir(), ".rex-memory");
26
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
27
+ writeFileSync(LOCK_FILE, String(pid));
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+ function releaseLock() {
34
+ try {
35
+ if (existsSync(LOCK_FILE)) {
36
+ const content = readFileSync(LOCK_FILE, "utf-8").trim();
37
+ if (parseInt(content, 10) === process.pid) {
38
+ unlinkSync(LOCK_FILE);
39
+ }
40
+ }
41
+ } catch {
42
+ }
43
+ }
44
+ var OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
45
+ var STATE_FILE = join(homedir(), ".rex-memory", "gateway-state.json");
46
+ var LOG_FILE = join(homedir(), ".claude", "rex-gateway-commands.log");
47
+ var UPLOADS_ROOT = join(homedir(), ".rex-memory", "gateway-uploads");
48
+ var NOTIFS_FILE = join(homedir(), ".rex-memory", "notifications.json");
49
+ var MAX_UPLOADS = 200;
50
+ var UPLOAD_RETENTION_DAYS = Math.max(1, parseInt(process.env.REX_UPLOAD_RETENTION_DAYS || "10", 10) || 10);
51
+ var MAX_MEDIA_MB = Math.max(1, parseInt(process.env.REX_GATEWAY_MAX_MEDIA_MB || "20", 10) || 20);
52
+ var MAX_MEDIA_BYTES = MAX_MEDIA_MB * 1024 * 1024;
53
+ var AUTO_UPLOAD_ANALYZE = process.env.REX_GATEWAY_AUTO_ANALYZE_UPLOADS !== "0";
54
+ var AUTO_UPLOAD_MODE = (process.env.REX_GATEWAY_AUTO_ANALYZE_MODE || "claude").toLowerCase() === "qwen" ? "qwen" : "claude";
55
+ var AUTO_UPLOAD_TASK = process.env.REX_GATEWAY_AUTO_ANALYZE_TASK || "Parse this uploaded file and produce a concise coding-oriented brief with next actions.";
56
+ function loadConfig() {
57
+ const defaults = {
58
+ macTailscaleIp: "100.112.24.122",
59
+ macAddress: "52:f1:cf:b2:a5:32",
60
+ vpsTailscaleIp: "100.86.167.118",
61
+ pollTimeout: 30,
62
+ maxOutputLength: 4e3
63
+ };
64
+ try {
65
+ const settingsPath = join(homedir(), ".claude", "settings.json");
66
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
67
+ const gw = settings.env || {};
68
+ return {
69
+ macTailscaleIp: gw.REX_MAC_TAILSCALE_IP || defaults.macTailscaleIp,
70
+ macAddress: gw.REX_MAC_ADDRESS || defaults.macAddress,
71
+ vpsTailscaleIp: gw.REX_VPS_TAILSCALE_IP || defaults.vpsTailscaleIp,
72
+ pollTimeout: parseInt(gw.REX_POLL_TIMEOUT || "") || defaults.pollTimeout,
73
+ maxOutputLength: parseInt(gw.REX_MAX_OUTPUT || "") || defaults.maxOutputLength
74
+ };
75
+ } catch {
76
+ return defaults;
77
+ }
78
+ }
79
+ function loadState() {
80
+ try {
81
+ if (existsSync(STATE_FILE)) {
82
+ const parsed = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
83
+ return {
84
+ mode: parsed.mode === "claude" ? "claude" : "qwen",
85
+ localModel: parsed.localModel || null,
86
+ claudeModel: parsed.claudeModel || null,
87
+ lastActivity: parsed.lastActivity || (/* @__PURE__ */ new Date()).toISOString(),
88
+ sessionsCount: typeof parsed.sessionsCount === "number" ? parsed.sessionsCount : 0,
89
+ uploads: Array.isArray(parsed.uploads) ? parsed.uploads : []
90
+ };
91
+ }
92
+ } catch {
93
+ }
94
+ return { mode: "qwen", localModel: null, claudeModel: null, lastActivity: (/* @__PURE__ */ new Date()).toISOString(), sessionsCount: 0, uploads: [] };
95
+ }
96
+ function saveState(state2) {
97
+ try {
98
+ const dir = join(homedir(), ".rex-memory");
99
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
100
+ writeFileSync(STATE_FILE, JSON.stringify(state2, null, 2));
101
+ } catch {
102
+ }
103
+ }
104
+ var state = loadState();
105
+ var config = loadConfig();
106
+ var activeStreamController = null;
107
+ function loadNotifications() {
108
+ try {
109
+ return JSON.parse(readFileSync(NOTIFS_FILE, "utf-8"));
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+ function saveNotifications(notifs) {
115
+ try {
116
+ const dir = join(homedir(), ".rex-memory");
117
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
118
+ writeFileSync(NOTIFS_FILE, JSON.stringify(notifs.slice(-500), null, 2));
119
+ } catch {
120
+ }
121
+ }
122
+ function addNotification(project, title, message, priority = "normal") {
123
+ const notifs = loadNotifications();
124
+ notifs.push({
125
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
126
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
127
+ project,
128
+ title,
129
+ message,
130
+ priority,
131
+ read: false
132
+ });
133
+ saveNotifications(notifs);
134
+ }
135
+ function priorityEmoji(p) {
136
+ return p === "urgent" ? "\u{1F6A8}" : p === "high" ? "\u{1F534}" : p === "low" ? "\u{1F535}" : "\u{1F514}";
137
+ }
138
+ function timeAgo(ts) {
139
+ const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1e3);
140
+ if (diff < 60) return `${diff}s`;
141
+ if (diff < 3600) return `${Math.floor(diff / 60)}m`;
142
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
143
+ return `${Math.floor(diff / 86400)}j`;
144
+ }
145
+ function buildNotifsMessage(project, page) {
146
+ const allNotifs = loadNotifications();
147
+ const filtered = project && project !== "all" ? allNotifs.filter((n) => n.project === project) : allNotifs;
148
+ const sorted = [...filtered].reverse();
149
+ const PAGE_SIZE = 5;
150
+ const total = sorted.length;
151
+ const start = page * PAGE_SIZE;
152
+ const items = sorted.slice(start, start + PAGE_SIZE);
153
+ const projects = [...new Set(allNotifs.map((n) => n.project))].sort();
154
+ const header = project && project !== "all" ? `\u{1F514} *Notifs \u2014 ${project}*` : `\u{1F514} *Notifications* (${total})`;
155
+ const unread = filtered.filter((n) => !n.read).length;
156
+ const subtitle = unread > 0 ? `_${unread} non lue(s)_` : "_Tout lu_";
157
+ if (items.length === 0) {
158
+ return {
159
+ text: `${header}
160
+
161
+ _Aucune notification_`,
162
+ buttons: [[{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }]]
163
+ };
164
+ }
165
+ const lines = items.map((n) => {
166
+ const status = n.read ? "\u2705" : priorityEmoji(n.priority);
167
+ const proj = n.project;
168
+ const msg = n.message ? `
169
+ _${n.message.slice(0, 100)}_` : "";
170
+ return `${status} *[${proj}]* ${n.title} \u2014 _${timeAgo(n.ts)} ago_${msg}`;
171
+ });
172
+ const text = `${header}
173
+ ${subtitle}
174
+
175
+ ${lines.join("\n\n")}`;
176
+ const filterBtns = [{ text: project === "all" || !project ? "\u2022 Toutes \u2022" : "Toutes", callback_data: "notif_filter_all" }];
177
+ for (const p of projects.slice(0, 5)) {
178
+ filterBtns.push({ text: p === project ? `\u2022 ${p} \u2022` : p, callback_data: `notif_filter_${p}` });
179
+ }
180
+ const navBtns = [];
181
+ if (start > 0) navBtns.push({ text: "\u25C0", callback_data: `notif_page_${page - 1}_${project || "all"}` });
182
+ if (start + PAGE_SIZE < total) navBtns.push({ text: "\u25B6", callback_data: `notif_page_${page + 1}_${project || "all"}` });
183
+ if (unread > 0) navBtns.push({ text: "\u2705 Tout lire", callback_data: `notif_markall_${project || "all"}` });
184
+ const buttons = [filterBtns];
185
+ if (navBtns.length) buttons.push(navBtns);
186
+ buttons.push([{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }]);
187
+ return { text, buttons };
188
+ }
189
+ function ensureUploadsDir() {
190
+ if (!existsSync(UPLOADS_ROOT)) mkdirSync(UPLOADS_ROOT, { recursive: true });
191
+ }
192
+ function slugFileName(name) {
193
+ return name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "upload";
194
+ }
195
+ function rememberUpload(entry) {
196
+ state.uploads.push(entry);
197
+ if (state.uploads.length > MAX_UPLOADS) {
198
+ state.uploads = state.uploads.slice(-MAX_UPLOADS);
199
+ }
200
+ saveState(state);
201
+ }
202
+ function recentUploads(chatId, limit = 10) {
203
+ return state.uploads.filter((u) => u.chatId === chatId).slice(-limit).reverse();
204
+ }
205
+ function latestUpload(chatId) {
206
+ const list = recentUploads(chatId, 1);
207
+ return list.length ? list[0] : null;
208
+ }
209
+ function cleanupOldUploads() {
210
+ ensureUploadsDir();
211
+ const now = Date.now();
212
+ const maxAgeMs = UPLOAD_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
213
+ try {
214
+ const files = readdirSync(UPLOADS_ROOT);
215
+ for (const file of files) {
216
+ const full = join(UPLOADS_ROOT, file);
217
+ let st;
218
+ try {
219
+ st = statSync(full);
220
+ } catch {
221
+ continue;
222
+ }
223
+ if (!st.isFile()) continue;
224
+ if (now - st.mtimeMs > maxAgeMs) {
225
+ try {
226
+ unlinkSync(full);
227
+ } catch {
228
+ }
229
+ }
230
+ }
231
+ } catch {
232
+ }
233
+ const existing = /* @__PURE__ */ new Set();
234
+ try {
235
+ for (const file of readdirSync(UPLOADS_ROOT)) {
236
+ existing.add(join(UPLOADS_ROOT, file));
237
+ }
238
+ } catch {
239
+ }
240
+ state.uploads = state.uploads.filter((u) => existing.has(u.filePath));
241
+ saveState(state);
242
+ }
243
+ function logCommand(from, command, result) {
244
+ try {
245
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
246
+ const entry = `[${ts}] @${from}: ${command} -> ${result.slice(0, 200)}
247
+ `;
248
+ const dir = join(homedir(), ".claude");
249
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
250
+ writeFileSync(LOG_FILE, entry, { flag: "a" });
251
+ } catch {
252
+ }
253
+ }
254
+ var COLORS = {
255
+ reset: "\x1B[0m",
256
+ green: "\x1B[32m",
257
+ yellow: "\x1B[33m",
258
+ red: "\x1B[31m",
259
+ dim: "\x1B[2m",
260
+ bold: "\x1B[1m",
261
+ cyan: "\x1B[36m"
262
+ };
263
+ function getCredentials() {
264
+ const settingsPath = join(homedir(), ".claude", "settings.json");
265
+ try {
266
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
267
+ const token2 = settings.env?.REX_TELEGRAM_BOT_TOKEN;
268
+ const chatId2 = settings.env?.REX_TELEGRAM_CHAT_ID;
269
+ if (token2 && chatId2) return { token: token2, chatId: chatId2 };
270
+ } catch {
271
+ }
272
+ const token = process.env.REX_TELEGRAM_BOT_TOKEN;
273
+ const chatId = process.env.REX_TELEGRAM_CHAT_ID;
274
+ if (token && chatId) return { token, chatId };
275
+ return null;
276
+ }
277
+ async function tg(token, method, body) {
278
+ try {
279
+ const res = await fetch(`https://api.telegram.org/bot${token}/${method}`, {
280
+ method: "POST",
281
+ headers: { "Content-Type": "application/json" },
282
+ body: JSON.stringify(body)
283
+ });
284
+ const json = await res.json();
285
+ if (!json?.ok && method !== "getUpdates") {
286
+ console.error(`${COLORS.dim}TG ${method} failed: ${json?.description || res.status}${COLORS.reset}`);
287
+ }
288
+ return json;
289
+ } catch (e) {
290
+ console.error(`${COLORS.dim}TG ${method} error: ${e?.message || e}${COLORS.reset}`);
291
+ return null;
292
+ }
293
+ }
294
+ async function send(token, chatId, text, keyboard) {
295
+ const body = { chat_id: chatId, text, parse_mode: "Markdown" };
296
+ if (keyboard) {
297
+ body.reply_markup = { inline_keyboard: keyboard };
298
+ }
299
+ return tg(token, "sendMessage", body);
300
+ }
301
+ async function editMessage(token, chatId, messageId, text, keyboard) {
302
+ const body = { chat_id: chatId, message_id: messageId, text, parse_mode: "Markdown" };
303
+ if (keyboard) {
304
+ body.reply_markup = { inline_keyboard: keyboard };
305
+ }
306
+ return tg(token, "editMessageText", body);
307
+ }
308
+ async function answerCallback(token, callbackId, text) {
309
+ return tg(token, "answerCallbackQuery", { callback_query_id: callbackId, text });
310
+ }
311
+ function isAuthorized(msgChatId, authorizedChatId) {
312
+ return String(msgChatId) === String(authorizedChatId);
313
+ }
314
+ function mainMenu() {
315
+ return [
316
+ [
317
+ { text: "\u{1F4CA} Status", callback_data: "status" },
318
+ { text: "\u{1FA7A} Doctor", callback_data: "doctor" },
319
+ { text: "\u{1F50D} Memory", callback_data: "memory_menu" }
320
+ ],
321
+ [
322
+ { text: "\u{1F5A5} Git", callback_data: "git" },
323
+ { text: "\u26A1 Optimize", callback_data: "optimize" },
324
+ { text: "\u{1F4E5} Ingest", callback_data: "ingest" }
325
+ ],
326
+ [
327
+ { text: `\u{1F916} Mode: ${state.mode === "qwen" ? "Qwen (local)" : "Claude"}`, callback_data: "switch_mode" },
328
+ { text: "\u{1F9F9} Prune", callback_data: "prune" }
329
+ ],
330
+ [
331
+ { text: "\u{1F4A4} Wake Mac", callback_data: "wake_mac" },
332
+ { text: "\u{1F50C} Mac Status", callback_data: "mac_status" }
333
+ ],
334
+ [
335
+ { text: "\u{1F4CB} Sessions", callback_data: "sessions" },
336
+ { text: "\u{1F4DD} Logs", callback_data: "logs" },
337
+ { text: "\u{1F4CE} Files", callback_data: "files_menu" }
338
+ ],
339
+ [
340
+ { text: "\u{1F514} Notifs", callback_data: "notifs" },
341
+ { text: "\u{1F9ED} Advanced", callback_data: "advanced_menu" }
342
+ ]
343
+ ];
344
+ }
345
+ function backButton() {
346
+ return [[{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }]];
347
+ }
348
+ function claudeMenu() {
349
+ return [
350
+ [
351
+ { text: "\u{1F4AC} New Session", callback_data: "claude_new" },
352
+ { text: "\u{1F4C2} Continue Last", callback_data: "claude_continue" }
353
+ ],
354
+ [
355
+ { text: "\u{1F4CB} List Sessions", callback_data: "claude_sessions" },
356
+ { text: "\u{1F504} Resume #", callback_data: "claude_resume" }
357
+ ],
358
+ [{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }]
359
+ ];
360
+ }
361
+ function advancedMenu() {
362
+ return [
363
+ [
364
+ { text: "\u{1F9E0} Agents", callback_data: "agents_menu" },
365
+ { text: "\u{1F50C} MCP", callback_data: "mcp_menu" }
366
+ ],
367
+ [
368
+ { text: "\u{1F9EA} Audit", callback_data: "audit" },
369
+ { text: "\u{1F4DD} Logs", callback_data: "logs" }
370
+ ],
371
+ [{ text: "\u{1F39B} Mod\xE8les", callback_data: "models_menu" }],
372
+ [{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }]
373
+ ];
374
+ }
375
+ function modelsMenu() {
376
+ const local = state.localModel || "auto";
377
+ const claude = state.claudeModel || "sonnet-4-6";
378
+ return [
379
+ [{ text: `\u{1F9E0} Local: ${local}`, callback_data: "models_local_menu" }],
380
+ [
381
+ { text: `qwen2.5:1.5b`, callback_data: "set_local_qwen2.5:1.5b" },
382
+ { text: `qwen3.5:4b`, callback_data: "set_local_qwen3.5:4b" },
383
+ { text: `qwen3.5:9b`, callback_data: "set_local_qwen3.5:9b" },
384
+ { text: `auto`, callback_data: "set_local_auto" }
385
+ ],
386
+ [{ text: `\u{1F916} Claude: ${claude}`, callback_data: "models_claude_menu" }],
387
+ [
388
+ { text: `haiku-4-5`, callback_data: "set_claude_claude-haiku-4-5-20251001" },
389
+ { text: `sonnet-4-6`, callback_data: "set_claude_claude-sonnet-4-6" },
390
+ { text: `opus-4-6`, callback_data: "set_claude_claude-opus-4-6" }
391
+ ],
392
+ [{ text: "\u25C0\uFE0F Advanced", callback_data: "advanced_menu" }]
393
+ ];
394
+ }
395
+ function agentsMenu() {
396
+ return [
397
+ [
398
+ { text: "\u{1F504} Refresh", callback_data: "agents_menu" },
399
+ { text: "\u{1F4E6} Profiles", callback_data: "agents_profiles" }
400
+ ],
401
+ [
402
+ { text: "\u2795 Read", callback_data: "agents_create_read" },
403
+ { text: "\u2795 Review", callback_data: "agents_create_review" }
404
+ ],
405
+ [
406
+ { text: "\u25B6\uFE0F Start all", callback_data: "agents_start_all" },
407
+ { text: "\u23F9 Stop all", callback_data: "agents_stop_all" }
408
+ ],
409
+ [{ text: "\u25C0\uFE0F Advanced", callback_data: "advanced_menu" }]
410
+ ];
411
+ }
412
+ function mcpMenu() {
413
+ return [
414
+ [
415
+ { text: "\u{1F504} Refresh", callback_data: "mcp_menu" },
416
+ { text: "\u{1F501} Sync Claude", callback_data: "mcp_sync" }
417
+ ],
418
+ [
419
+ { text: "\u2705 Check enabled", callback_data: "mcp_check_enabled" },
420
+ { text: "\u{1F4E4} Export", callback_data: "mcp_export" }
421
+ ],
422
+ [{ text: "\u25C0\uFE0F Advanced", callback_data: "advanced_menu" }]
423
+ ];
424
+ }
425
+ function filesMenu() {
426
+ return [
427
+ [
428
+ { text: "\u{1F195} Last file", callback_data: "file_last" },
429
+ { text: "\u{1F4DA} List", callback_data: "files_list" }
430
+ ],
431
+ [
432
+ { text: "\u{1F916} Analyze Claude", callback_data: "file_analyze_claude" },
433
+ { text: "\u{1F9E0} Analyze Qwen", callback_data: "file_analyze_qwen" }
434
+ ],
435
+ [{ text: "\u25C0\uFE0F Menu", callback_data: "menu" }]
436
+ ];
437
+ }
438
+ function run(cmd, timeout = 3e4) {
439
+ try {
440
+ return execSync(cmd, { timeout, encoding: "utf-8" }).trim();
441
+ } catch (e) {
442
+ return e.stderr?.trim() || e.message || "Command failed";
443
+ }
444
+ }
445
+ function runRex(args, timeout = 3e4) {
446
+ try {
447
+ return execFileSync("rex", args, { timeout, encoding: "utf-8" }).trim();
448
+ } catch (e) {
449
+ const stderr = e?.stderr?.toString?.().trim?.() || "";
450
+ const stdout = e?.stdout?.toString?.().trim?.() || "";
451
+ return stderr || stdout || e.message || "Command failed";
452
+ }
453
+ }
454
+ function runRexJson(args, timeout = 3e4) {
455
+ const raw = strip(runRex(args, timeout)).trim();
456
+ if (!raw) return null;
457
+ try {
458
+ return JSON.parse(raw);
459
+ } catch {
460
+ return null;
461
+ }
462
+ }
463
+ function strip(text) {
464
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
465
+ }
466
+ function truncate(text, max) {
467
+ const limit = max || config.maxOutputLength;
468
+ if (text.length <= limit) return text;
469
+ return text.slice(0, limit) + "\n\n... (truncated)";
470
+ }
471
+ function sanitizeToken(input) {
472
+ const value = input.trim();
473
+ if (!value) return null;
474
+ if (!/^[a-zA-Z0-9._:-]+$/.test(value)) return null;
475
+ return value;
476
+ }
477
+ function loadAgents() {
478
+ const parsed = runRexJson(["agents", "list", "--json"], 15e3);
479
+ const agents = parsed?.agents;
480
+ if (!Array.isArray(agents)) return [];
481
+ return agents;
482
+ }
483
+ function loadMcpServers() {
484
+ const parsed = runRexJson(["mcp", "list", "--json"], 15e3);
485
+ const servers = parsed?.servers;
486
+ if (!Array.isArray(servers)) return [];
487
+ return servers;
488
+ }
489
+ function renderAgentsSummary(max = 8) {
490
+ const agents = loadAgents();
491
+ if (agents.length === 0) {
492
+ return [
493
+ "\u{1F9E0} *Agents*",
494
+ "No agents configured.",
495
+ "",
496
+ "Create quickly:",
497
+ "`/agent_create read`",
498
+ "`/agent_create code-review`"
499
+ ].join("\n");
500
+ }
501
+ const lines = agents.slice(0, max).map((a) => {
502
+ const icon = a.running ? "\u{1F7E2}" : a.enabled ? "\u{1F7E1}" : "\u26AB\uFE0F";
503
+ return `${icon} \`${a.id}\` \u2022 ${a.profile} \u2022 ${a.running ? "running" : "stopped"}`;
504
+ });
505
+ const more = agents.length > max ? `
506
+ ... +${agents.length - max} more` : "";
507
+ return [
508
+ `\u{1F9E0} *Agents* (${agents.length})`,
509
+ "",
510
+ ...lines,
511
+ more,
512
+ "",
513
+ "Commands:",
514
+ "`/agent_start <id>` `/agent_stop <id>` `/agent_run <id>`"
515
+ ].filter(Boolean).join("\n");
516
+ }
517
+ function renderMcpSummary(max = 8) {
518
+ const servers = loadMcpServers();
519
+ if (servers.length === 0) {
520
+ return [
521
+ "\u{1F50C} *MCP Registry*",
522
+ "No servers configured.",
523
+ "",
524
+ "Add one:",
525
+ "`rex mcp add <name> --command <cmd>`"
526
+ ].join("\n");
527
+ }
528
+ const lines = servers.slice(0, max).map((s) => {
529
+ const icon = s.enabled ? "\u{1F7E2}" : "\u26AB\uFE0F";
530
+ const target = s.type === "stdio" ? `${s.command || "n/a"} ${(s.args || []).join(" ")}`.trim() : s.url || "n/a";
531
+ return `${icon} \`${s.id}\` \u2022 ${s.type} \u2022 ${target}`;
532
+ });
533
+ const more = servers.length > max ? `
534
+ ... +${servers.length - max} more` : "";
535
+ return [
536
+ `\u{1F50C} *MCP Registry* (${servers.length})`,
537
+ "",
538
+ ...lines,
539
+ more,
540
+ "",
541
+ "Commands:",
542
+ "`/mcp_check <id>` `/mcp_sync`"
543
+ ].filter(Boolean).join("\n");
544
+ }
545
+ function pickAttachment(msg) {
546
+ const caption = String(msg?.caption || "").trim();
547
+ if (msg?.document?.file_id) {
548
+ return {
549
+ fileId: String(msg.document.file_id),
550
+ kind: "document",
551
+ fileName: String(msg.document.file_name || `document-${Date.now()}`),
552
+ mimeType: String(msg.document.mime_type || "application/octet-stream"),
553
+ size: Number(msg.document.file_size || 0),
554
+ caption
555
+ };
556
+ }
557
+ if (Array.isArray(msg?.photo) && msg.photo.length > 0) {
558
+ const sorted = [...msg.photo].sort((a, b) => Number(a?.file_size || 0) - Number(b?.file_size || 0));
559
+ const best = sorted[sorted.length - 1] || {};
560
+ return {
561
+ fileId: String(best.file_id),
562
+ kind: "photo",
563
+ fileName: `photo-${Date.now()}.jpg`,
564
+ mimeType: "image/jpeg",
565
+ size: Number(best.file_size || 0),
566
+ caption
567
+ };
568
+ }
569
+ if (msg?.audio?.file_id) {
570
+ const ext = extname(String(msg.audio.file_name || "")).replace(".", "") || "mp3";
571
+ return {
572
+ fileId: String(msg.audio.file_id),
573
+ kind: "audio",
574
+ fileName: String(msg.audio.file_name || `audio-${Date.now()}.${ext}`),
575
+ mimeType: String(msg.audio.mime_type || "audio/mpeg"),
576
+ size: Number(msg.audio.file_size || 0),
577
+ caption
578
+ };
579
+ }
580
+ if (msg?.voice?.file_id) {
581
+ return {
582
+ fileId: String(msg.voice.file_id),
583
+ kind: "voice",
584
+ fileName: `voice-${Date.now()}.ogg`,
585
+ mimeType: String(msg.voice.mime_type || "audio/ogg"),
586
+ size: Number(msg.voice.file_size || 0),
587
+ caption
588
+ };
589
+ }
590
+ if (msg?.video?.file_id) {
591
+ const ext = extname(String(msg.video.file_name || "")).replace(".", "") || "mp4";
592
+ return {
593
+ fileId: String(msg.video.file_id),
594
+ kind: "video",
595
+ fileName: String(msg.video.file_name || `video-${Date.now()}.${ext}`),
596
+ mimeType: String(msg.video.mime_type || "video/mp4"),
597
+ size: Number(msg.video.file_size || 0),
598
+ caption
599
+ };
600
+ }
601
+ return null;
602
+ }
603
+ async function downloadTelegramFile(token, fileId, fileName) {
604
+ const info = await tg(token, "getFile", { file_id: fileId });
605
+ const tgPath = info?.result?.file_path;
606
+ if (!tgPath) throw new Error("Telegram getFile failed");
607
+ const res = await fetch(`https://api.telegram.org/file/bot${token}/${tgPath}`);
608
+ if (!res.ok) throw new Error(`Download failed (${res.status})`);
609
+ const ab = await res.arrayBuffer();
610
+ const buffer = Buffer.from(ab);
611
+ if (buffer.byteLength > MAX_MEDIA_BYTES) {
612
+ throw new Error(`File too large (${Math.round(buffer.byteLength / 1024 / 1024)}MB > ${MAX_MEDIA_MB}MB)`);
613
+ }
614
+ ensureUploadsDir();
615
+ const base = slugFileName(fileName || basename(tgPath));
616
+ const ext = extname(base) || extname(tgPath) || "";
617
+ const stem = ext ? base.replace(new RegExp(`${ext.replace(".", "\\.")}$`), "") : base;
618
+ const finalName = `${stem}-${Date.now()}${ext}`;
619
+ const full = join(UPLOADS_ROOT, finalName);
620
+ writeFileSync(full, buffer);
621
+ return { path: full, size: buffer.byteLength };
622
+ }
623
+ function commandExists(cmd) {
624
+ return run(`command -v ${cmd} >/dev/null 2>&1 && echo ok`).includes("ok");
625
+ }
626
+ function shellQuote(value) {
627
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
628
+ }
629
+ function extractFilePreview(upload, maxChars = 9e3) {
630
+ const path = upload.filePath;
631
+ const qPath = shellQuote(path);
632
+ const name = upload.fileName.toLowerCase();
633
+ const mime = upload.mimeType.toLowerCase();
634
+ if (mime.includes("pdf") || name.endsWith(".pdf")) {
635
+ if (commandExists("pdftotext")) {
636
+ const out = run(`pdftotext -layout ${qPath} - 2>/dev/null | head -c ${maxChars}`, 3e4);
637
+ if (out.trim()) return out.trim();
638
+ }
639
+ if (commandExists("python3")) {
640
+ const py = run(
641
+ `python3 -c "import sys
642
+ p=sys.argv[1]
643
+ t=''
644
+ try:
645
+ import pypdf
646
+ r=pypdf.PdfReader(p)
647
+ t='\\n'.join([(pg.extract_text() or '') for pg in r.pages])
648
+ except Exception:
649
+ try:
650
+ import PyPDF2
651
+ r=PyPDF2.PdfReader(p)
652
+ t='\\n'.join([(pg.extract_text() or '') for pg in r.pages])
653
+ except Exception:
654
+ pass
655
+ print(t[:${maxChars}])" ${qPath}`,
656
+ 45e3
657
+ );
658
+ if (py.trim()) return py.trim();
659
+ }
660
+ const raw = run(`strings ${qPath} 2>/dev/null | head -c ${maxChars}`, 1e4);
661
+ return raw.trim();
662
+ }
663
+ const textLike = mime.startsWith("text/") || name.endsWith(".md") || name.endsWith(".txt") || name.endsWith(".json") || name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".yml") || name.endsWith(".yaml") || name.endsWith(".toml") || name.endsWith(".csv");
664
+ if (textLike) {
665
+ const out = run(`head -c ${maxChars} ${qPath}`, 1e4);
666
+ return out.trim();
667
+ }
668
+ if (upload.kind === "photo" && commandExists("tesseract")) {
669
+ const out = run(`tesseract ${qPath} stdout 2>/dev/null | head -c ${maxChars}`, 3e4);
670
+ return out.trim();
671
+ }
672
+ return "";
673
+ }
674
+ async function analyzeUpload(upload, task, mode) {
675
+ const preview = extractFilePreview(upload);
676
+ const activeMode = mode || state.mode;
677
+ const prompt = [
678
+ "You are processing a file uploaded from Telegram to REX.",
679
+ `File path: ${upload.filePath}`,
680
+ `File name: ${upload.fileName}`,
681
+ `Kind: ${upload.kind}`,
682
+ `Mime type: ${upload.mimeType}`,
683
+ `Size: ${upload.size} bytes`,
684
+ `User task: ${task}`,
685
+ "",
686
+ preview ? `Extracted preview:\\n${preview}` : "No text preview available from this file type. Explain what can be done next.",
687
+ "",
688
+ "Return:",
689
+ "1) concise summary",
690
+ "2) useful action plan for coding/ops"
691
+ ].join("\n");
692
+ if (activeMode === "claude") {
693
+ return askClaude(prompt);
694
+ }
695
+ return askLLM(prompt);
696
+ }
697
+ function renderUpload(u) {
698
+ const mb = (u.size / 1024 / 1024).toFixed(2);
699
+ return [
700
+ `id: ${u.id}`,
701
+ `type: ${u.kind}`,
702
+ `name: ${u.fileName}`,
703
+ `mime: ${u.mimeType}`,
704
+ `size: ${mb} MB`,
705
+ `path: ${u.filePath}`,
706
+ `time: ${u.uploadedAt}`
707
+ ].join("\n");
708
+ }
709
+ function renderUploadsList(chatId) {
710
+ const uploads = recentUploads(chatId, 10);
711
+ if (uploads.length === 0) {
712
+ return "\u{1F4CE} *Files*\nNo uploads yet.\n\nSend an image, PDF, audio, or document to this bot.";
713
+ }
714
+ const lines = uploads.map((u) => {
715
+ const mb = (u.size / 1024 / 1024).toFixed(2);
716
+ return `\u2022 \`${u.id}\` ${u.kind} ${u.fileName} (${mb}MB)`;
717
+ });
718
+ return ["\u{1F4CE} *Recent uploads*", "", ...lines, "", "Auto-analysis is ON. Optional override: `/file_analyze <prompt>`"].join("\n");
719
+ }
720
+ async function handleAttachment(token, chatId, msg, from) {
721
+ const attachment = pickAttachment(msg);
722
+ if (!attachment) return "";
723
+ if (attachment.size > MAX_MEDIA_BYTES) {
724
+ return `\u26A0\uFE0F File too large (${Math.round(attachment.size / 1024 / 1024)}MB). Limit is ${MAX_MEDIA_MB}MB.`;
725
+ }
726
+ const dl = await downloadTelegramFile(token, attachment.fileId, attachment.fileName);
727
+ const entry = {
728
+ id: `up-${Date.now().toString(36)}`,
729
+ chatId,
730
+ from,
731
+ kind: attachment.kind,
732
+ filePath: dl.path,
733
+ fileName: attachment.fileName,
734
+ mimeType: attachment.mimeType,
735
+ size: dl.size,
736
+ caption: attachment.caption || void 0,
737
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
738
+ };
739
+ rememberUpload(entry);
740
+ let text = `\u{1F4CE} *File received*
741
+ \`\`\`
742
+ ${renderUpload(entry)}
743
+ \`\`\``;
744
+ if (AUTO_UPLOAD_ANALYZE) {
745
+ const mode = AUTO_UPLOAD_MODE;
746
+ const task = attachment.caption && attachment.caption.length > 2 ? attachment.caption : AUTO_UPLOAD_TASK;
747
+ try {
748
+ const out = await analyzeUpload(entry, task, mode);
749
+ text += `
750
+
751
+ ${mode === "claude" ? "\u{1F916}" : "\u{1F9E0}"} *Auto analysis* (${mode})
752
+ ${truncate(out, 2800)}`;
753
+ } catch (e) {
754
+ const err = e instanceof Error ? e.message : String(e);
755
+ text += `
756
+
757
+ \u26A0\uFE0F Auto analysis failed: ${err}`;
758
+ }
759
+ } else {
760
+ text += "\n\nAuto analysis disabled. Use `/file_analyze <prompt>`.";
761
+ }
762
+ logCommand(from, `[upload] ${attachment.kind}:${attachment.fileName}`, "saved");
763
+ return text;
764
+ }
765
+ async function wakeMac() {
766
+ try {
767
+ const mac = config.macAddress.replace(/:/g, "");
768
+ const pyCmd = `python3 -c "
769
+ import socket, struct
770
+ mac = bytes.fromhex('${mac}')
771
+ pkt = b'\\xff'*6 + mac*16
772
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
773
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
774
+ s.sendto(pkt, ('255.255.255.255', 9))
775
+ s.sendto(pkt, ('${config.macTailscaleIp}', 9))
776
+ s.close()
777
+ print('Magic packet sent')
778
+ "`;
779
+ const out = run(pyCmd, 5e3);
780
+ if (out.includes("Magic packet sent")) return "\u2705 Magic packet sent to Mac";
781
+ } catch {
782
+ }
783
+ try {
784
+ const ping = run(`ping -c 1 -W 2 ${config.macTailscaleIp} 2>/dev/null`, 5e3);
785
+ if (ping.includes("1 packets received") || ping.includes("1 received")) {
786
+ return "\u2705 Mac is already awake (responds to ping)";
787
+ }
788
+ } catch {
789
+ }
790
+ return "\u26A0\uFE0F Magic packet sent \u2014 Mac may take 30s to wake";
791
+ }
792
+ async function checkMacStatus() {
793
+ try {
794
+ const ping = run(`ping -c 1 -W 3 ${config.macTailscaleIp} 2>/dev/null`, 5e3);
795
+ const online = ping.includes("1 packets received") || ping.includes("1 received");
796
+ if (online) {
797
+ const ts = run("tailscale status 2>/dev/null | head -5", 5e3);
798
+ return { online: true, details: ts };
799
+ }
800
+ return { online: false, details: "Mac not responding to ping" };
801
+ } catch {
802
+ return { online: false, details: "Ping failed" };
803
+ }
804
+ }
805
+ async function askLLM(prompt) {
806
+ if (state.mode === "qwen") {
807
+ return askQwen(prompt);
808
+ } else {
809
+ return askClaude(prompt);
810
+ }
811
+ }
812
+ async function askQwen(prompt) {
813
+ try {
814
+ const check = await fetch(`${OLLAMA_URL}/api/tags`);
815
+ if (!check.ok) return "\u26A0\uFE0F Ollama not running. /wake to wake Mac first.";
816
+ } catch {
817
+ return "\u26A0\uFE0F Ollama not running. Wake Mac first.";
818
+ }
819
+ const out = runRex(["llm", prompt], 6e4);
820
+ if (!out || out.includes("rex-claude") || out.includes("Commands:")) {
821
+ return "\u26A0\uFE0F LLM returned no useful response";
822
+ }
823
+ return truncate(out);
824
+ }
825
+ async function askQwenStream(token, chatId, prompt) {
826
+ let model = state.localModel || "qwen3.5:4b";
827
+ if (!state.localModel) {
828
+ try {
829
+ const tags = await fetch(`${OLLAMA_URL}/api/tags`);
830
+ if (!tags.ok) return "\u26A0\uFE0F Ollama not running.";
831
+ const data = await tags.json();
832
+ const names = data.models.map((m) => m.name);
833
+ for (const pref of ["qwen3.5:9b", "qwen3.5:4b", "qwen2.5:1.5b"]) {
834
+ const base = pref.split(":")[0];
835
+ const match = names.find((n) => n.includes(base));
836
+ if (match) {
837
+ model = match;
838
+ break;
839
+ }
840
+ }
841
+ } catch {
842
+ return "\u26A0\uFE0F Ollama not running.";
843
+ }
844
+ } else {
845
+ try {
846
+ const check = await fetch(`${OLLAMA_URL}/api/tags`);
847
+ if (!check.ok) return "\u26A0\uFE0F Ollama not running.";
848
+ } catch {
849
+ return "\u26A0\uFE0F Ollama not running.";
850
+ }
851
+ }
852
+ const initMsg = await tg(token, "sendMessage", {
853
+ chat_id: chatId,
854
+ text: "\u{1F9E0} _Qwen thinking..._",
855
+ parse_mode: "Markdown"
856
+ });
857
+ const msgId = initMsg?.result?.message_id;
858
+ if (!msgId) return "\u26A0\uFE0F Failed to send initial message";
859
+ const controller = new AbortController();
860
+ activeStreamController = controller;
861
+ const streamTimeout = setTimeout(() => controller.abort(), 12e4);
862
+ const res = await fetch(`${OLLAMA_URL}/api/chat`, {
863
+ method: "POST",
864
+ headers: { "Content-Type": "application/json" },
865
+ body: JSON.stringify({
866
+ model,
867
+ messages: [{ role: "user", content: prompt }],
868
+ stream: true,
869
+ think: false
870
+ }),
871
+ signal: controller.signal
872
+ });
873
+ if (!res.ok || !res.body) {
874
+ await editMessage(token, chatId, msgId, "\u26A0\uFE0F Ollama stream failed");
875
+ return "\u26A0\uFE0F Ollama stream failed";
876
+ }
877
+ function stripThinkBlocks(raw) {
878
+ let clean = raw.replace(/<think>[\s\S]*?<\/think>/g, "");
879
+ clean = clean.replace(/<think>[\s\S]*$/, "");
880
+ return clean.trim();
881
+ }
882
+ let rawFull = "";
883
+ let lastEdit = 0;
884
+ let wasThinking = false;
885
+ const EDIT_INTERVAL = 800;
886
+ const reader = res.body.getReader();
887
+ const decoder = new TextDecoder();
888
+ let buffer = "";
889
+ try {
890
+ while (true) {
891
+ const { done, value } = await reader.read();
892
+ if (done) break;
893
+ buffer += decoder.decode(value, { stream: true });
894
+ const lines = buffer.split("\n");
895
+ buffer = lines.pop() || "";
896
+ for (const line of lines) {
897
+ if (!line.trim()) continue;
898
+ try {
899
+ const chunk = JSON.parse(line);
900
+ if (chunk.message?.content) {
901
+ rawFull += chunk.message.content;
902
+ }
903
+ } catch {
904
+ }
905
+ }
906
+ const now = Date.now();
907
+ if (now - lastEdit > EDIT_INTERVAL) {
908
+ const visible2 = stripThinkBlocks(rawFull);
909
+ const isThinking = rawFull.includes("<think>") && !rawFull.includes("</think>");
910
+ if (isThinking && !wasThinking) {
911
+ try {
912
+ await editMessage(token, chatId, msgId, "\u{1F9E0} _R\xE9flexion en cours..._");
913
+ } catch {
914
+ }
915
+ wasThinking = true;
916
+ } else if (visible2.length > 0) {
917
+ wasThinking = false;
918
+ const display = visible2.length > 4e3 ? visible2.slice(-4e3) : visible2;
919
+ try {
920
+ await editMessage(token, chatId, msgId, `\u{1F9E0} ${display}`);
921
+ } catch {
922
+ }
923
+ }
924
+ lastEdit = now;
925
+ }
926
+ }
927
+ if (buffer.trim()) {
928
+ try {
929
+ const chunk = JSON.parse(buffer);
930
+ if (chunk.message?.content) rawFull += chunk.message.content;
931
+ } catch {
932
+ }
933
+ }
934
+ } catch {
935
+ } finally {
936
+ clearTimeout(streamTimeout);
937
+ activeStreamController = null;
938
+ }
939
+ const visible = stripThinkBlocks(rawFull);
940
+ const finalText = truncate(visible || rawFull || "\u26A0\uFE0F Empty response");
941
+ try {
942
+ await editMessage(token, chatId, msgId, `\u{1F9E0} ${finalText}`, [
943
+ [
944
+ { text: "Mode: qwen", callback_data: "switch_mode" },
945
+ { text: "\u25C0\uFE0F Menu", callback_data: "menu" }
946
+ ]
947
+ ]);
948
+ } catch {
949
+ }
950
+ return finalText;
951
+ }
952
+ function claudeEnv() {
953
+ const env = { ...process.env };
954
+ delete env.CLAUDECODE;
955
+ return env;
956
+ }
957
+ async function runClaudeAsync(args, timeoutMs, onProgress) {
958
+ const { spawn } = __require("child_process");
959
+ const modelArgs = state.claudeModel ? ["--model", state.claudeModel, ...args] : args;
960
+ return new Promise((resolve) => {
961
+ let stdout = "";
962
+ let stderr = "";
963
+ let settled = false;
964
+ let child;
965
+ try {
966
+ child = spawn("claude", modelArgs, { env: claudeEnv() });
967
+ } catch (e) {
968
+ resolve({ stdout: "", stderr: e?.message || "spawn failed" });
969
+ return;
970
+ }
971
+ child.stdout?.on("data", (d) => {
972
+ stdout += d.toString();
973
+ });
974
+ child.stderr?.on("data", (d) => {
975
+ stderr += d.toString();
976
+ });
977
+ let frameIdx = 0;
978
+ const timer = onProgress ? setInterval(() => {
979
+ if (!settled) onProgress(frameIdx++).catch(() => {
980
+ });
981
+ }, 3e3) : null;
982
+ const done = () => {
983
+ if (settled) return;
984
+ settled = true;
985
+ if (timer) clearInterval(timer);
986
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
987
+ };
988
+ child.on("close", done);
989
+ child.on("error", (e) => {
990
+ if (settled) return;
991
+ settled = true;
992
+ if (timer) clearInterval(timer);
993
+ const code = e.code;
994
+ resolve({ stdout: "", stderr: code === "ENOENT" ? "claude CLI not found in PATH" : e.message });
995
+ });
996
+ const to = setTimeout(() => {
997
+ if (!settled) {
998
+ settled = true;
999
+ if (timer) clearInterval(timer);
1000
+ try {
1001
+ child.kill();
1002
+ } catch {
1003
+ }
1004
+ resolve({ stdout: "", stderr: `Claude CLI timed out after ${Math.round(timeoutMs / 1e3)}s` });
1005
+ }
1006
+ }, timeoutMs);
1007
+ child.on("close", () => clearTimeout(to));
1008
+ });
1009
+ }
1010
+ function parseClaudeError(stderr) {
1011
+ const s = stderr.toLowerCase();
1012
+ if (s.includes("nested session") || s.includes("claudecode") || s.includes("cannot be launched inside")) {
1013
+ return "\u26A0\uFE0F Claude: nested session conflict \u2014 restart gateway outside Claude Code";
1014
+ }
1015
+ if (s.includes("not logged in") || s.includes("authenticate") || s.includes("unauthorized") || s.includes("401")) {
1016
+ return "\u26A0\uFE0F Claude: not authenticated \u2014 run `claude auth login`";
1017
+ }
1018
+ if (s.includes("rate limit") || s.includes("429") || s.includes("quota exceeded")) {
1019
+ return "\u26A0\uFE0F Claude: rate limit \u2014 r\xE9essaie dans quelques minutes";
1020
+ }
1021
+ if (s.includes("timed out")) {
1022
+ return "\u26A0\uFE0F Claude: timeout \u2014 requ\xEAte trop longue";
1023
+ }
1024
+ if (s.includes("not found in path") || s.includes("enoent")) {
1025
+ return "\u26A0\uFE0F Claude CLI introuvable \u2014 v\xE9rifie l'installation";
1026
+ }
1027
+ if (s.includes("network") || s.includes("econnrefused") || s.includes("fetch failed")) {
1028
+ return "\u26A0\uFE0F Claude: erreur r\xE9seau \u2014 v\xE9rifie la connexion";
1029
+ }
1030
+ return `\u26A0\uFE0F Claude: ${stderr.slice(0, 400)}`;
1031
+ }
1032
+ async function askClaude(prompt) {
1033
+ const { stdout, stderr } = await runClaudeAsync(["-p", prompt], 12e4);
1034
+ if (stdout) return truncate(stdout);
1035
+ if (stderr) {
1036
+ console.error(`Claude CLI stderr: ${stderr.slice(0, 300)}`);
1037
+ return parseClaudeError(stderr);
1038
+ }
1039
+ return "\u26A0\uFE0F Claude CLI returned empty";
1040
+ }
1041
+ async function askClaudeWithProgress(token, chatId, msgId, args) {
1042
+ const frames = [
1043
+ "\u{1F916} _Claude r\xE9fl\xE9chit..._",
1044
+ "\u{1F916} _Claude r\xE9fl\xE9chit.._",
1045
+ "\u{1F916} _Claude r\xE9fl\xE9chit._",
1046
+ "\u{1F916} _Claude r\xE9fl\xE9chit..._"
1047
+ ];
1048
+ const { stdout, stderr } = await runClaudeAsync(args, 18e4, async (idx) => {
1049
+ try {
1050
+ await editMessage(token, chatId, msgId, frames[idx % frames.length]);
1051
+ } catch {
1052
+ }
1053
+ });
1054
+ if (stdout) return truncate(stdout);
1055
+ if (stderr) {
1056
+ console.error(`Claude session stderr: ${stderr.slice(0, 300)}`);
1057
+ return parseClaudeError(stderr);
1058
+ }
1059
+ return "\u26A0\uFE0F No response from Claude";
1060
+ }
1061
+ async function claudeSession(token, chatId, msgId, prompt, resume) {
1062
+ const args = resume ? ["--continue", "-p", prompt] : ["-p", prompt];
1063
+ const result = await askClaudeWithProgress(token, chatId, msgId, args);
1064
+ if (!result.startsWith("\u26A0\uFE0F")) {
1065
+ state.sessionsCount++;
1066
+ saveState(state);
1067
+ }
1068
+ return result;
1069
+ }
1070
+ async function handleCallback(token, chatId, messageId, callbackId, data, from) {
1071
+ await answerCallback(token, callbackId);
1072
+ logCommand(from, `[btn] ${data}`, "ok");
1073
+ state = loadState();
1074
+ switch (data) {
1075
+ case "menu":
1076
+ await editMessage(
1077
+ token,
1078
+ chatId,
1079
+ messageId,
1080
+ "\u{1F996} *REX Gateway v3*\nChoisis une action :",
1081
+ mainMenu()
1082
+ );
1083
+ break;
1084
+ case "status": {
1085
+ const out = strip(run("rex status"));
1086
+ await editMessage(
1087
+ token,
1088
+ chatId,
1089
+ messageId,
1090
+ `\u{1F4CA} *Status*
1091
+ ${out}`,
1092
+ backButton()
1093
+ );
1094
+ break;
1095
+ }
1096
+ case "doctor": {
1097
+ await editMessage(token, chatId, messageId, "\u{1FA7A} _Running diagnostics..._");
1098
+ const out = truncate(strip(run("rex doctor")));
1099
+ await editMessage(
1100
+ token,
1101
+ chatId,
1102
+ messageId,
1103
+ `\u{1FA7A} *Doctor*
1104
+ \`\`\`
1105
+ ${out}
1106
+ \`\`\``,
1107
+ backButton()
1108
+ );
1109
+ break;
1110
+ }
1111
+ case "git": {
1112
+ const branch = run('git branch --show-current 2>/dev/null || echo "n/a"');
1113
+ const status = run("git status --short 2>/dev/null | head -15");
1114
+ const lastCommit = run('git log -1 --format="%s" 2>/dev/null || echo "n/a"');
1115
+ await editMessage(
1116
+ token,
1117
+ chatId,
1118
+ messageId,
1119
+ `\u{1F5A5} *Git*
1120
+ Branch: \`${branch}\`
1121
+ Last: ${lastCommit}
1122
+ \`\`\`
1123
+ ${status || "Clean"}
1124
+ \`\`\``,
1125
+ backButton()
1126
+ );
1127
+ break;
1128
+ }
1129
+ case "optimize": {
1130
+ await editMessage(token, chatId, messageId, "\u26A1 _Analyzing CLAUDE.md..._");
1131
+ const out = truncate(strip(run("rex optimize", 6e4)), 3500);
1132
+ await editMessage(
1133
+ token,
1134
+ chatId,
1135
+ messageId,
1136
+ `\u26A1 *Optimize*
1137
+ \`\`\`
1138
+ ${out}
1139
+ \`\`\``,
1140
+ [[
1141
+ { text: "\u{1F527} Apply", callback_data: "optimize_apply" },
1142
+ { text: "\u25C0\uFE0F Menu", callback_data: "menu" }
1143
+ ]]
1144
+ );
1145
+ break;
1146
+ }
1147
+ case "optimize_apply": {
1148
+ await editMessage(token, chatId, messageId, "\u{1F527} _Applying optimizations..._");
1149
+ const out = truncate(strip(run("rex optimize --apply", 12e4)), 3500);
1150
+ await editMessage(
1151
+ token,
1152
+ chatId,
1153
+ messageId,
1154
+ `\u{1F527} *Applied*
1155
+ \`\`\`
1156
+ ${out}
1157
+ \`\`\``,
1158
+ backButton()
1159
+ );
1160
+ break;
1161
+ }
1162
+ case "ingest": {
1163
+ await editMessage(token, chatId, messageId, "\u{1F4E5} _Ingesting sessions..._");
1164
+ const out = truncate(strip(run("rex ingest", 12e4)), 3500);
1165
+ await editMessage(
1166
+ token,
1167
+ chatId,
1168
+ messageId,
1169
+ `\u{1F4E5} *Ingest*
1170
+ \`\`\`
1171
+ ${out}
1172
+ \`\`\``,
1173
+ backButton()
1174
+ );
1175
+ break;
1176
+ }
1177
+ case "prune": {
1178
+ await editMessage(token, chatId, messageId, "\u{1F9F9} _Pruning old memories..._");
1179
+ const out = truncate(strip(run("rex prune", 6e4)));
1180
+ await editMessage(
1181
+ token,
1182
+ chatId,
1183
+ messageId,
1184
+ `\u{1F9F9} *Prune*
1185
+ \`\`\`
1186
+ ${out}
1187
+ \`\`\``,
1188
+ backButton()
1189
+ );
1190
+ break;
1191
+ }
1192
+ case "memory_menu":
1193
+ await editMessage(
1194
+ token,
1195
+ chatId,
1196
+ messageId,
1197
+ "\u{1F50D} *Memory*\nEnvoie ta recherche en texte ou :",
1198
+ [
1199
+ [
1200
+ { text: "\u{1F4E5} Ingest Now", callback_data: "ingest" },
1201
+ { text: "\u{1F9F9} Prune", callback_data: "prune" }
1202
+ ],
1203
+ [
1204
+ { text: "\u{1F4CA} Stats", callback_data: "memory_stats" },
1205
+ { text: "\u25C0\uFE0F Menu", callback_data: "menu" }
1206
+ ]
1207
+ ]
1208
+ );
1209
+ break;
1210
+ case "memory_stats": {
1211
+ const out = run("rex prune --stats", 1e4);
1212
+ await editMessage(
1213
+ token,
1214
+ chatId,
1215
+ messageId,
1216
+ `\u{1F4CA} *Memory Stats*
1217
+ \`\`\`
1218
+ ${strip(out)}
1219
+ \`\`\``,
1220
+ backButton()
1221
+ );
1222
+ break;
1223
+ }
1224
+ case "switch_mode":
1225
+ state.mode = state.mode === "qwen" ? "claude" : "qwen";
1226
+ state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
1227
+ saveState(state);
1228
+ await editMessage(
1229
+ token,
1230
+ chatId,
1231
+ messageId,
1232
+ `\u{1F916} Mode switched to *${state.mode === "qwen" ? "Qwen (local LLM)" : "Claude (CLI)"}*`,
1233
+ mainMenu()
1234
+ );
1235
+ break;
1236
+ case "wake_mac": {
1237
+ await editMessage(token, chatId, messageId, "\u{1F4A4} _Sending wake signal..._");
1238
+ const result = await wakeMac();
1239
+ await new Promise((r) => setTimeout(r, 3e3));
1240
+ const status = await checkMacStatus();
1241
+ await editMessage(
1242
+ token,
1243
+ chatId,
1244
+ messageId,
1245
+ `\u{1F4A4} *Wake Mac*
1246
+ ${result}
1247
+
1248
+ \u{1F50C} ${status.online ? "\u{1F7E2} Online" : "\u{1F534} Offline"}
1249
+ \`${status.details}\``,
1250
+ [[
1251
+ { text: "\u{1F504} Check Again", callback_data: "mac_status" },
1252
+ { text: "\u25C0\uFE0F Menu", callback_data: "menu" }
1253
+ ]]
1254
+ );
1255
+ break;
1256
+ }
1257
+ case "mac_status": {
1258
+ await editMessage(token, chatId, messageId, "\u{1F50C} _Checking Mac..._");
1259
+ const status = await checkMacStatus();
1260
+ await editMessage(
1261
+ token,
1262
+ chatId,
1263
+ messageId,
1264
+ `\u{1F50C} *Mac Status*
1265
+ ${status.online ? "\u{1F7E2} Online" : "\u{1F534} Offline"}
1266
+ \`\`\`
1267
+ ${status.details}
1268
+ \`\`\``,
1269
+ [[
1270
+ { text: "\u{1F4A4} Wake", callback_data: "wake_mac" },
1271
+ { text: "\u{1F504} Refresh", callback_data: "mac_status" },
1272
+ { text: "\u25C0\uFE0F Menu", callback_data: "menu" }
1273
+ ]]
1274
+ );
1275
+ break;
1276
+ }
1277
+ case "sessions": {
1278
+ await editMessage(
1279
+ token,
1280
+ chatId,
1281
+ messageId,
1282
+ `\u{1F4CB} *Claude Sessions*
1283
+ Mode: *${state.mode}*
1284
+ Sessions: ${state.sessionsCount}
1285
+ Last: ${state.lastActivity}`,
1286
+ claudeMenu()
1287
+ );
1288
+ break;
1289
+ }
1290
+ case "claude_new":
1291
+ await editMessage(
1292
+ token,
1293
+ chatId,
1294
+ messageId,
1295
+ "\u{1F4AC} *New Claude Session*\nEnvoie ta question/tache en texte. Claude va la traiter en mode session.",
1296
+ backButton()
1297
+ );
1298
+ break;
1299
+ case "claude_continue": {
1300
+ await editMessage(token, chatId, messageId, "\u{1F4C2} _Continuing last session..._");
1301
+ const out = await claudeSession(token, chatId, messageId, "Continue the previous task. What was I working on?", true);
1302
+ await editMessage(
1303
+ token,
1304
+ chatId,
1305
+ messageId,
1306
+ `\u{1F4C2} *Session Continued*
1307
+ ${out}`,
1308
+ [[
1309
+ { text: "\u{1F4AC} Reply", callback_data: "claude_new" },
1310
+ { text: "\u25C0\uFE0F Menu", callback_data: "menu" }
1311
+ ]]
1312
+ );
1313
+ break;
1314
+ }
1315
+ case "claude_sessions": {
1316
+ const sessions = run("ls -lt ~/.claude/projects/ 2>/dev/null | head -10");
1317
+ await editMessage(
1318
+ token,
1319
+ chatId,
1320
+ messageId,
1321
+ `\u{1F4CB} *Recent Sessions*
1322
+ \`\`\`
1323
+ ${strip(sessions)}
1324
+ \`\`\``,
1325
+ claudeMenu()
1326
+ );
1327
+ break;
1328
+ }
1329
+ case "claude_resume": {
1330
+ await editMessage(
1331
+ token,
1332
+ chatId,
1333
+ messageId,
1334
+ "\u{1F504} *Resume Session*\nEnvoie le chemin du projet pour reprendre la session.",
1335
+ backButton()
1336
+ );
1337
+ break;
1338
+ }
1339
+ case "advanced_menu":
1340
+ await editMessage(
1341
+ token,
1342
+ chatId,
1343
+ messageId,
1344
+ "\u{1F9ED} *Advanced*\nAgents autonomes, registry MCP et audit.",
1345
+ advancedMenu()
1346
+ );
1347
+ break;
1348
+ case "audit": {
1349
+ await editMessage(token, chatId, messageId, "\u{1F9EA} _Running strict audit..._");
1350
+ const out = truncate(strip(run("rex audit --strict", 12e4)), 3500);
1351
+ await editMessage(
1352
+ token,
1353
+ chatId,
1354
+ messageId,
1355
+ `\u{1F9EA} *Audit*
1356
+ \`\`\`
1357
+ ${out}
1358
+ \`\`\``,
1359
+ advancedMenu()
1360
+ );
1361
+ break;
1362
+ }
1363
+ case "agents_menu":
1364
+ await editMessage(token, chatId, messageId, renderAgentsSummary(), agentsMenu());
1365
+ break;
1366
+ case "agents_profiles": {
1367
+ const parsed = runRexJson(["agents", "profiles", "--json"], 15e3);
1368
+ const rows = Array.isArray(parsed?.profiles) ? parsed.profiles : [];
1369
+ const body = rows.length ? rows.slice(0, 8).map((p) => `\u2022 ${p.name} \u2022 model=${p.model} \u2022 every=${p.intervalSec}s`).join("\n") : "No profile data.";
1370
+ await editMessage(
1371
+ token,
1372
+ chatId,
1373
+ messageId,
1374
+ `\u{1F4E6} *Agent Profiles*
1375
+ ${body}`,
1376
+ agentsMenu()
1377
+ );
1378
+ break;
1379
+ }
1380
+ case "agents_create_read": {
1381
+ const out = truncate(strip(runRex(["agents", "create", "read"], 2e4)), 3e3);
1382
+ await editMessage(
1383
+ token,
1384
+ chatId,
1385
+ messageId,
1386
+ `\u2795 *Agent Created (read)*
1387
+ \`\`\`
1388
+ ${out}
1389
+ \`\`\``,
1390
+ agentsMenu()
1391
+ );
1392
+ break;
1393
+ }
1394
+ case "agents_create_review": {
1395
+ const out = truncate(strip(runRex(["agents", "create", "code-review"], 2e4)), 3e3);
1396
+ await editMessage(
1397
+ token,
1398
+ chatId,
1399
+ messageId,
1400
+ `\u2795 *Agent Created (code-review)*
1401
+ \`\`\`
1402
+ ${out}
1403
+ \`\`\``,
1404
+ agentsMenu()
1405
+ );
1406
+ break;
1407
+ }
1408
+ case "agents_start_all": {
1409
+ const targets = loadAgents().filter((a) => a.enabled);
1410
+ if (targets.length === 0) {
1411
+ await editMessage(token, chatId, messageId, "\u{1F9E0} *Agents*\nNo enabled agents to start.", agentsMenu());
1412
+ break;
1413
+ }
1414
+ const lines = targets.slice(0, 10).map((a) => {
1415
+ const out = runRex(["agents", "run", a.id], 2e4);
1416
+ const ok = out.includes('"ok"') || out.includes("alreadyRunning");
1417
+ return `${ok ? "\u2705" : "\u26A0\uFE0F"} ${a.id}`;
1418
+ });
1419
+ const more = targets.length > 10 ? `
1420
+ ... +${targets.length - 10} more` : "";
1421
+ await editMessage(
1422
+ token,
1423
+ chatId,
1424
+ messageId,
1425
+ `\u25B6\uFE0F *Start enabled agents*
1426
+ ${lines.join("\n")}${more}`,
1427
+ agentsMenu()
1428
+ );
1429
+ break;
1430
+ }
1431
+ case "agents_stop_all": {
1432
+ const targets = loadAgents().filter((a) => a.running);
1433
+ if (targets.length === 0) {
1434
+ await editMessage(token, chatId, messageId, "\u{1F9E0} *Agents*\nNo running agents to stop.", agentsMenu());
1435
+ break;
1436
+ }
1437
+ const lines = targets.slice(0, 10).map((a) => {
1438
+ const out = runRex(["agents", "stop", a.id], 2e4);
1439
+ const ok = out.includes('"ok"') || out.includes('"stopped"');
1440
+ return `${ok ? "\u2705" : "\u26A0\uFE0F"} ${a.id}`;
1441
+ });
1442
+ const more = targets.length > 10 ? `
1443
+ ... +${targets.length - 10} more` : "";
1444
+ await editMessage(
1445
+ token,
1446
+ chatId,
1447
+ messageId,
1448
+ `\u23F9 *Stop running agents*
1449
+ ${lines.join("\n")}${more}`,
1450
+ agentsMenu()
1451
+ );
1452
+ break;
1453
+ }
1454
+ case "mcp_menu":
1455
+ await editMessage(token, chatId, messageId, renderMcpSummary(), mcpMenu());
1456
+ break;
1457
+ case "mcp_sync": {
1458
+ const out = truncate(strip(runRex(["mcp", "sync-claude"], 2e4)), 3e3);
1459
+ await editMessage(
1460
+ token,
1461
+ chatId,
1462
+ messageId,
1463
+ `\u{1F501} *MCP Sync Claude*
1464
+ \`\`\`
1465
+ ${out}
1466
+ \`\`\``,
1467
+ mcpMenu()
1468
+ );
1469
+ break;
1470
+ }
1471
+ case "mcp_check_enabled": {
1472
+ const servers = loadMcpServers().filter((s) => s.enabled);
1473
+ if (servers.length === 0) {
1474
+ await editMessage(token, chatId, messageId, "\u{1F50C} *MCP*\nNo enabled servers to check.", mcpMenu());
1475
+ break;
1476
+ }
1477
+ const lines = servers.slice(0, 8).map((s) => {
1478
+ const checked = runRexJson(["mcp", "check", s.id], 15e3);
1479
+ return `${checked?.ok ? "\u2705" : "\u274C"} ${s.id} (${s.type})`;
1480
+ });
1481
+ const more = servers.length > 8 ? `
1482
+ ... +${servers.length - 8} more` : "";
1483
+ await editMessage(
1484
+ token,
1485
+ chatId,
1486
+ messageId,
1487
+ `\u2705 *MCP check (enabled)*
1488
+ ${lines.join("\n")}${more}`,
1489
+ mcpMenu()
1490
+ );
1491
+ break;
1492
+ }
1493
+ case "mcp_export": {
1494
+ const out = truncate(strip(runRex(["mcp", "export"], 15e3)), 3e3);
1495
+ await editMessage(
1496
+ token,
1497
+ chatId,
1498
+ messageId,
1499
+ `\u{1F4E4} *MCP Export*
1500
+ \`\`\`
1501
+ ${out}
1502
+ \`\`\``,
1503
+ mcpMenu()
1504
+ );
1505
+ break;
1506
+ }
1507
+ case "files_menu":
1508
+ await editMessage(token, chatId, messageId, renderUploadsList(chatId), filesMenu());
1509
+ break;
1510
+ case "files_list":
1511
+ await editMessage(token, chatId, messageId, renderUploadsList(chatId), filesMenu());
1512
+ break;
1513
+ case "file_last": {
1514
+ const upload = latestUpload(chatId);
1515
+ if (!upload) {
1516
+ await editMessage(token, chatId, messageId, "\u{1F4CE} *Files*\nNo uploaded file found yet.", filesMenu());
1517
+ break;
1518
+ }
1519
+ await editMessage(
1520
+ token,
1521
+ chatId,
1522
+ messageId,
1523
+ `\u{1F4CE} *Last File*
1524
+ \`\`\`
1525
+ ${renderUpload(upload)}
1526
+ \`\`\``,
1527
+ filesMenu()
1528
+ );
1529
+ break;
1530
+ }
1531
+ case "file_analyze_claude": {
1532
+ const upload = latestUpload(chatId);
1533
+ if (!upload) {
1534
+ await editMessage(token, chatId, messageId, "\u{1F4CE} *Files*\nNo uploaded file found yet.", filesMenu());
1535
+ break;
1536
+ }
1537
+ await editMessage(token, chatId, messageId, "\u{1F916} _Analyzing latest file with Claude..._");
1538
+ const out = await analyzeUpload(upload, "Summarize the file and propose engineering actions.", "claude");
1539
+ await editMessage(
1540
+ token,
1541
+ chatId,
1542
+ messageId,
1543
+ `\u{1F916} *Claude file analysis*
1544
+ ${truncate(out, 3200)}`,
1545
+ filesMenu()
1546
+ );
1547
+ break;
1548
+ }
1549
+ case "file_analyze_qwen": {
1550
+ const upload = latestUpload(chatId);
1551
+ if (!upload) {
1552
+ await editMessage(token, chatId, messageId, "\u{1F4CE} *Files*\nNo uploaded file found yet.", filesMenu());
1553
+ break;
1554
+ }
1555
+ await editMessage(token, chatId, messageId, "\u{1F9E0} _Analyzing latest file with Qwen..._");
1556
+ const out = await analyzeUpload(upload, "Summarize the file and propose engineering actions.", "qwen");
1557
+ await editMessage(
1558
+ token,
1559
+ chatId,
1560
+ messageId,
1561
+ `\u{1F9E0} *Qwen file analysis*
1562
+ ${truncate(out, 3200)}`,
1563
+ filesMenu()
1564
+ );
1565
+ break;
1566
+ }
1567
+ case "logs": {
1568
+ let logs = "No logs yet";
1569
+ try {
1570
+ if (existsSync(LOG_FILE)) {
1571
+ logs = run(`tail -20 "${LOG_FILE}"`);
1572
+ }
1573
+ } catch {
1574
+ }
1575
+ await editMessage(
1576
+ token,
1577
+ chatId,
1578
+ messageId,
1579
+ `\u{1F4DD} *Recent Logs*
1580
+ \`\`\`
1581
+ ${truncate(strip(logs), 3e3)}
1582
+ \`\`\``,
1583
+ backButton()
1584
+ );
1585
+ break;
1586
+ }
1587
+ case "models_menu": {
1588
+ await editMessage(
1589
+ token,
1590
+ chatId,
1591
+ messageId,
1592
+ `\u{1F39B} *Mod\xE8les actifs*
1593
+ \u{1F9E0} Local: \`${state.localModel || "auto"}\`
1594
+ \u{1F916} Claude: \`${state.claudeModel || "sonnet-4-6 (d\xE9faut)"}\``,
1595
+ modelsMenu()
1596
+ );
1597
+ break;
1598
+ }
1599
+ case "notifs": {
1600
+ const { text: t, buttons } = buildNotifsMessage(null, 0);
1601
+ await editMessage(token, chatId, messageId, t, buttons);
1602
+ break;
1603
+ }
1604
+ default: {
1605
+ if (data.startsWith("set_local_")) {
1606
+ const model = data.replace("set_local_", "");
1607
+ state.localModel = model === "auto" ? null : model;
1608
+ saveState(state);
1609
+ await editMessage(
1610
+ token,
1611
+ chatId,
1612
+ messageId,
1613
+ `\u{1F9E0} Mod\xE8le local \u2192 \`${state.localModel || "auto-detect"}\``,
1614
+ modelsMenu()
1615
+ );
1616
+ break;
1617
+ }
1618
+ if (data.startsWith("set_claude_")) {
1619
+ const model = data.replace("set_claude_", "");
1620
+ state.claudeModel = model;
1621
+ saveState(state);
1622
+ await editMessage(
1623
+ token,
1624
+ chatId,
1625
+ messageId,
1626
+ `\u{1F916} Mod\xE8le Claude \u2192 \`${model}\``,
1627
+ modelsMenu()
1628
+ );
1629
+ break;
1630
+ }
1631
+ if (data.startsWith("notif_filter_")) {
1632
+ const proj = data.replace("notif_filter_", "");
1633
+ const { text: t, buttons } = buildNotifsMessage(proj === "all" ? null : proj, 0);
1634
+ await editMessage(token, chatId, messageId, t, buttons);
1635
+ break;
1636
+ }
1637
+ if (data.startsWith("notif_page_")) {
1638
+ const parts = data.replace("notif_page_", "").split("_");
1639
+ const page = parseInt(parts[0], 10) || 0;
1640
+ const proj = parts.slice(1).join("_") || null;
1641
+ const { text: t, buttons } = buildNotifsMessage(proj === "all" ? null : proj, page);
1642
+ await editMessage(token, chatId, messageId, t, buttons);
1643
+ break;
1644
+ }
1645
+ if (data.startsWith("notif_markall_")) {
1646
+ const proj = data.replace("notif_markall_", "");
1647
+ const notifs = loadNotifications();
1648
+ for (const n of notifs) {
1649
+ if (proj === "all" || n.project === proj) n.read = true;
1650
+ }
1651
+ saveNotifications(notifs);
1652
+ const { text: t, buttons } = buildNotifsMessage(proj === "all" ? null : proj, 0);
1653
+ await editMessage(token, chatId, messageId, t, buttons);
1654
+ break;
1655
+ }
1656
+ break;
1657
+ }
1658
+ }
1659
+ }
1660
+ var BLOCKED_COMMANDS = [
1661
+ "rm -rf",
1662
+ "rm -r /",
1663
+ "mkfs",
1664
+ "dd if=",
1665
+ ":(){",
1666
+ "chmod -R 777",
1667
+ "git push --force main",
1668
+ "git push --force master",
1669
+ "sudo rm",
1670
+ "sudo chmod",
1671
+ "eval ",
1672
+ "curl | sh",
1673
+ "curl | bash",
1674
+ "wget | sh",
1675
+ "> /dev/sd",
1676
+ "shutdown",
1677
+ "reboot",
1678
+ "init 0"
1679
+ ];
1680
+ async function handleText(token, chatId, text, from) {
1681
+ const cmd = text.trim().toLowerCase();
1682
+ if (cmd === "/start" || cmd === "/menu" || cmd === "/help" || cmd === "/h") {
1683
+ await send(token, chatId, "\u{1F996} *REX Gateway v3*\nChoisis une action :", mainMenu());
1684
+ logCommand(from, cmd, "menu");
1685
+ return;
1686
+ }
1687
+ if (cmd === "/status" || cmd === "/s") {
1688
+ const out = strip(run("rex status"));
1689
+ await send(token, chatId, `\u{1F4CA} ${out}`, backButton());
1690
+ logCommand(from, "/status", out);
1691
+ return;
1692
+ }
1693
+ if (cmd === "/wake" || cmd === "/w") {
1694
+ await send(token, chatId, "\u{1F4A4} _Sending wake signal..._");
1695
+ const result = await wakeMac();
1696
+ await send(token, chatId, result, backButton());
1697
+ logCommand(from, "/wake", result);
1698
+ return;
1699
+ }
1700
+ if (cmd === "/doctor" || cmd === "/d") {
1701
+ await send(token, chatId, "\u{1FA7A} _Running diagnostics..._");
1702
+ const out = truncate(strip(run("rex doctor")));
1703
+ await send(token, chatId, `\u{1FA7A}
1704
+ \`\`\`
1705
+ ${out}
1706
+ \`\`\``, backButton());
1707
+ logCommand(from, "/doctor", "done");
1708
+ return;
1709
+ }
1710
+ if (cmd === "/ingest" || cmd === "/i") {
1711
+ await send(token, chatId, "\u{1F4E5} _Ingesting..._");
1712
+ const out = truncate(strip(run("rex ingest", 12e4)));
1713
+ await send(token, chatId, `\u{1F4E5}
1714
+ \`\`\`
1715
+ ${out}
1716
+ \`\`\``, backButton());
1717
+ logCommand(from, "/ingest", "done");
1718
+ return;
1719
+ }
1720
+ if (cmd === "/prune") {
1721
+ await send(token, chatId, "\u{1F9F9} _Pruning..._");
1722
+ const out = truncate(strip(run("rex prune", 6e4)));
1723
+ await send(token, chatId, `\u{1F9F9}
1724
+ \`\`\`
1725
+ ${out}
1726
+ \`\`\``, backButton());
1727
+ logCommand(from, "/prune", "done");
1728
+ return;
1729
+ }
1730
+ if (cmd === "/notifs" || cmd === "/notif" || cmd.startsWith("/notifs ")) {
1731
+ const filter = cmd.startsWith("/notifs ") ? text.replace(/^\/notifs\s+/i, "").trim() : null;
1732
+ const { text: t, buttons } = buildNotifsMessage(filter || null, 0);
1733
+ await send(token, chatId, t, buttons);
1734
+ logCommand(from, "/notifs", filter || "all");
1735
+ return;
1736
+ }
1737
+ if (cmd.startsWith("/notify ")) {
1738
+ const raw = text.replace(/^\/notify\s+/i, "");
1739
+ const parts = raw.match(/(?:-p\s+(\S+))?\s*(?:-P\s+(\S+))?\s*(.*)/i) || [];
1740
+ let project = "general";
1741
+ let priority = "normal";
1742
+ let rest = raw;
1743
+ const pMatch = raw.match(/-p\s+(\S+)/);
1744
+ const PMatch = raw.match(/-P\s+(urgent|high|normal|low)/i);
1745
+ if (pMatch) {
1746
+ project = pMatch[1];
1747
+ rest = rest.replace(pMatch[0], "").trim();
1748
+ }
1749
+ if (PMatch) {
1750
+ priority = PMatch[1].toLowerCase();
1751
+ rest = rest.replace(PMatch[0], "").trim();
1752
+ }
1753
+ const [title = rest, ...msgParts] = rest.split("|");
1754
+ const message = msgParts.join("|").trim();
1755
+ addNotification(project, title.trim(), message, priority);
1756
+ const emoji = priorityEmoji(priority);
1757
+ await send(
1758
+ token,
1759
+ chatId,
1760
+ `${emoji} *Notification enregistr\xE9e*
1761
+ *Projet:* ${project}
1762
+ *Titre:* ${title.trim()}${message ? `
1763
+ *D\xE9tail:* ${message}` : ""}`,
1764
+ [[{ text: "\u{1F514} Voir notifs", callback_data: "notifs" }, { text: "\u25C0\uFE0F Menu", callback_data: "menu" }]]
1765
+ );
1766
+ logCommand(from, "/notify", `${project}: ${title.trim()}`);
1767
+ return;
1768
+ }
1769
+ if (cmd.startsWith("/search ") || cmd.startsWith("/q ")) {
1770
+ const query = text.replace(/^\/(search|q)\s+/i, "");
1771
+ if (!query) {
1772
+ await send(token, chatId, "Usage: /search <query>");
1773
+ return;
1774
+ }
1775
+ const out = runRex(["search", query]);
1776
+ await send(
1777
+ token,
1778
+ chatId,
1779
+ out ? `\u{1F50D} *Search:* ${query}
1780
+ \`\`\`
1781
+ ${truncate(out, 3e3)}
1782
+ \`\`\`` : "No results",
1783
+ backButton()
1784
+ );
1785
+ logCommand(from, `/search ${query}`, out ? "found" : "empty");
1786
+ return;
1787
+ }
1788
+ if (cmd.startsWith("/sh ") || cmd.startsWith("/run ")) {
1789
+ const shellCmd = text.replace(/^\/(sh|run)\s+/i, "");
1790
+ if (BLOCKED_COMMANDS.some((b) => shellCmd.toLowerCase().includes(b))) {
1791
+ await send(token, chatId, "\u{1F6AB} Blocked: dangerous command");
1792
+ logCommand(from, `/sh ${shellCmd}`, "BLOCKED");
1793
+ return;
1794
+ }
1795
+ const out = run(shellCmd);
1796
+ await send(token, chatId, `\`$ ${shellCmd}\`
1797
+ \`\`\`
1798
+ ${truncate(out, 3500)}
1799
+ \`\`\``, backButton());
1800
+ logCommand(from, `/sh ${shellCmd}`, out.slice(0, 100));
1801
+ return;
1802
+ }
1803
+ if (cmd.startsWith("/mode")) {
1804
+ state.mode = state.mode === "qwen" ? "claude" : "qwen";
1805
+ state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
1806
+ saveState(state);
1807
+ await send(
1808
+ token,
1809
+ chatId,
1810
+ `\u{1F916} Switched to *${state.mode === "qwen" ? "Qwen (local)" : "Claude"}*`,
1811
+ mainMenu()
1812
+ );
1813
+ logCommand(from, "/mode", state.mode);
1814
+ return;
1815
+ }
1816
+ if (cmd === "/claude" || cmd === "/c") {
1817
+ await send(token, chatId, "\u{1F916} *Claude Remote*\nGere tes sessions Claude a distance :", claudeMenu());
1818
+ return;
1819
+ }
1820
+ if (cmd.startsWith("/claude ") || cmd.startsWith("/c ")) {
1821
+ const prompt = text.replace(/^\/(claude|c)\s+/i, "");
1822
+ const initMsg = await send(token, chatId, "\u{1F916} _Claude is thinking..._");
1823
+ const thinkMsgId = initMsg?.result?.message_id;
1824
+ const out = thinkMsgId ? await claudeSession(token, chatId, thinkMsgId, prompt) : await askClaude(prompt);
1825
+ await send(token, chatId, out, [
1826
+ [
1827
+ { text: "\u{1F4AC} Continue", callback_data: "claude_continue" },
1828
+ { text: "\u25C0\uFE0F Menu", callback_data: "menu" }
1829
+ ]
1830
+ ]);
1831
+ logCommand(from, `/claude ${prompt.slice(0, 50)}`, out.slice(0, 100));
1832
+ return;
1833
+ }
1834
+ if (cmd === "/advanced" || cmd === "/adv") {
1835
+ await send(token, chatId, "\u{1F9ED} *Advanced*\nAgents autonomes, MCP et audit.", advancedMenu());
1836
+ logCommand(from, "/advanced", "menu");
1837
+ return;
1838
+ }
1839
+ if (cmd === "/audit") {
1840
+ await send(token, chatId, "\u{1F9EA} _Running strict audit..._");
1841
+ const out = truncate(strip(run("rex audit --strict", 12e4)), 3500);
1842
+ await send(token, chatId, `\u{1F9EA}
1843
+ \`\`\`
1844
+ ${out}
1845
+ \`\`\``, advancedMenu());
1846
+ logCommand(from, "/audit", "done");
1847
+ return;
1848
+ }
1849
+ if (cmd === "/agents") {
1850
+ await send(token, chatId, renderAgentsSummary(), agentsMenu());
1851
+ logCommand(from, "/agents", "listed");
1852
+ return;
1853
+ }
1854
+ if (cmd.startsWith("/agent_create ")) {
1855
+ const parts = text.trim().split(/\s+/);
1856
+ const profile = sanitizeToken(parts[1] || "");
1857
+ const name = sanitizeToken(parts[2] || "");
1858
+ if (!profile) {
1859
+ await send(token, chatId, "Usage: `/agent_create <read|analysis|code-review|advanced|ultimate> [name]`");
1860
+ return;
1861
+ }
1862
+ const args = ["agents", "create", profile];
1863
+ if (name) args.push(name);
1864
+ const out = truncate(strip(runRex(args, 2e4)), 3200);
1865
+ await send(token, chatId, `\u2795 Agent created
1866
+ \`\`\`
1867
+ ${out}
1868
+ \`\`\``, agentsMenu());
1869
+ logCommand(from, `/agent_create ${profile}`, "done");
1870
+ return;
1871
+ }
1872
+ if (cmd.startsWith("/agent_start ")) {
1873
+ const id = sanitizeToken(text.replace(/^\/agent_start\s+/i, ""));
1874
+ if (!id) {
1875
+ await send(token, chatId, "Usage: `/agent_start <id>`");
1876
+ return;
1877
+ }
1878
+ const out = truncate(strip(runRex(["agents", "run", id], 2e4)), 3200);
1879
+ await send(token, chatId, `\u25B6\uFE0F
1880
+ \`\`\`
1881
+ ${out}
1882
+ \`\`\``, agentsMenu());
1883
+ logCommand(from, `/agent_start ${id}`, out.slice(0, 80));
1884
+ return;
1885
+ }
1886
+ if (cmd.startsWith("/agent_run ")) {
1887
+ const id = sanitizeToken(text.replace(/^\/agent_run\s+/i, ""));
1888
+ if (!id) {
1889
+ await send(token, chatId, "Usage: `/agent_run <id>`");
1890
+ return;
1891
+ }
1892
+ await send(token, chatId, "\u25B6\uFE0F _Running one cycle..._");
1893
+ const out = truncate(strip(runRex(["agents", "run", id, "--once"], 9e4)), 3200);
1894
+ await send(token, chatId, `\u25B6\uFE0F One cycle done
1895
+ \`\`\`
1896
+ ${out}
1897
+ \`\`\``, agentsMenu());
1898
+ logCommand(from, `/agent_run ${id}`, "once");
1899
+ return;
1900
+ }
1901
+ if (cmd.startsWith("/agent_stop ")) {
1902
+ const id = sanitizeToken(text.replace(/^\/agent_stop\s+/i, ""));
1903
+ if (!id) {
1904
+ await send(token, chatId, "Usage: `/agent_stop <id>`");
1905
+ return;
1906
+ }
1907
+ const out = truncate(strip(runRex(["agents", "stop", id], 2e4)), 3200);
1908
+ await send(token, chatId, `\u23F9
1909
+ \`\`\`
1910
+ ${out}
1911
+ \`\`\``, agentsMenu());
1912
+ logCommand(from, `/agent_stop ${id}`, out.slice(0, 80));
1913
+ return;
1914
+ }
1915
+ if (cmd.startsWith("/agent_enable ")) {
1916
+ const id = sanitizeToken(text.replace(/^\/agent_enable\s+/i, ""));
1917
+ if (!id) {
1918
+ await send(token, chatId, "Usage: `/agent_enable <id>`");
1919
+ return;
1920
+ }
1921
+ const out = truncate(strip(runRex(["agents", "enable", id], 2e4)), 3200);
1922
+ await send(token, chatId, `\u2705
1923
+ \`\`\`
1924
+ ${out}
1925
+ \`\`\``, agentsMenu());
1926
+ logCommand(from, `/agent_enable ${id}`, out.slice(0, 80));
1927
+ return;
1928
+ }
1929
+ if (cmd.startsWith("/agent_disable ")) {
1930
+ const id = sanitizeToken(text.replace(/^\/agent_disable\s+/i, ""));
1931
+ if (!id) {
1932
+ await send(token, chatId, "Usage: `/agent_disable <id>`");
1933
+ return;
1934
+ }
1935
+ const out = truncate(strip(runRex(["agents", "disable", id], 2e4)), 3200);
1936
+ await send(token, chatId, `\u26AB\uFE0F
1937
+ \`\`\`
1938
+ ${out}
1939
+ \`\`\``, agentsMenu());
1940
+ logCommand(from, `/agent_disable ${id}`, out.slice(0, 80));
1941
+ return;
1942
+ }
1943
+ if (cmd.startsWith("/agent_delete ")) {
1944
+ const id = sanitizeToken(text.replace(/^\/agent_delete\s+/i, ""));
1945
+ if (!id) {
1946
+ await send(token, chatId, "Usage: `/agent_delete <id>`");
1947
+ return;
1948
+ }
1949
+ const out = truncate(strip(runRex(["agents", "delete", id], 2e4)), 3200);
1950
+ await send(token, chatId, `\u{1F5D1}
1951
+ \`\`\`
1952
+ ${out}
1953
+ \`\`\``, agentsMenu());
1954
+ logCommand(from, `/agent_delete ${id}`, out.slice(0, 80));
1955
+ return;
1956
+ }
1957
+ if (cmd.startsWith("/agent_logs ")) {
1958
+ const id = sanitizeToken(text.replace(/^\/agent_logs\s+/i, ""));
1959
+ if (!id) {
1960
+ await send(token, chatId, "Usage: `/agent_logs <id>`");
1961
+ return;
1962
+ }
1963
+ const out = truncate(strip(runRex(["agents", "logs", id, "--tail", "25"], 2e4)), 3200);
1964
+ await send(token, chatId, `\u{1F4DD} Agent logs
1965
+ \`\`\`
1966
+ ${out}
1967
+ \`\`\``, agentsMenu());
1968
+ logCommand(from, `/agent_logs ${id}`, "done");
1969
+ return;
1970
+ }
1971
+ if (cmd.startsWith("/mcp_check ")) {
1972
+ const id = sanitizeToken(text.replace(/^\/mcp_check\s+/i, ""));
1973
+ if (!id) {
1974
+ await send(token, chatId, "Usage: `/mcp_check <id>`");
1975
+ return;
1976
+ }
1977
+ const out = truncate(strip(runRex(["mcp", "check", id], 2e4)), 3200);
1978
+ await send(token, chatId, `\u2705 MCP check
1979
+ \`\`\`
1980
+ ${out}
1981
+ \`\`\``, mcpMenu());
1982
+ logCommand(from, `/mcp_check ${id}`, out.slice(0, 80));
1983
+ return;
1984
+ }
1985
+ if (cmd.startsWith("/mcp_enable ")) {
1986
+ const id = sanitizeToken(text.replace(/^\/mcp_enable\s+/i, ""));
1987
+ if (!id) {
1988
+ await send(token, chatId, "Usage: `/mcp_enable <id>`");
1989
+ return;
1990
+ }
1991
+ const out = truncate(strip(runRex(["mcp", "enable", id], 2e4)), 3200);
1992
+ await send(token, chatId, `\u{1F7E2} MCP enabled
1993
+ \`\`\`
1994
+ ${out}
1995
+ \`\`\``, mcpMenu());
1996
+ logCommand(from, `/mcp_enable ${id}`, out.slice(0, 80));
1997
+ return;
1998
+ }
1999
+ if (cmd.startsWith("/mcp_disable ")) {
2000
+ const id = sanitizeToken(text.replace(/^\/mcp_disable\s+/i, ""));
2001
+ if (!id) {
2002
+ await send(token, chatId, "Usage: `/mcp_disable <id>`");
2003
+ return;
2004
+ }
2005
+ const out = truncate(strip(runRex(["mcp", "disable", id], 2e4)), 3200);
2006
+ await send(token, chatId, `\u26AB\uFE0F MCP disabled
2007
+ \`\`\`
2008
+ ${out}
2009
+ \`\`\``, mcpMenu());
2010
+ logCommand(from, `/mcp_disable ${id}`, out.slice(0, 80));
2011
+ return;
2012
+ }
2013
+ if (cmd.startsWith("/mcp_remove ")) {
2014
+ const id = sanitizeToken(text.replace(/^\/mcp_remove\s+/i, ""));
2015
+ if (!id) {
2016
+ await send(token, chatId, "Usage: `/mcp_remove <id>`");
2017
+ return;
2018
+ }
2019
+ const out = truncate(strip(runRex(["mcp", "remove", id], 2e4)), 3200);
2020
+ await send(token, chatId, `\u{1F5D1} MCP removed
2021
+ \`\`\`
2022
+ ${out}
2023
+ \`\`\``, mcpMenu());
2024
+ logCommand(from, `/mcp_remove ${id}`, out.slice(0, 80));
2025
+ return;
2026
+ }
2027
+ if (cmd === "/mcp_sync") {
2028
+ const out = truncate(strip(runRex(["mcp", "sync-claude"], 2e4)), 3200);
2029
+ await send(token, chatId, `\u{1F501} MCP synced
2030
+ \`\`\`
2031
+ ${out}
2032
+ \`\`\``, mcpMenu());
2033
+ logCommand(from, "/mcp_sync", "done");
2034
+ return;
2035
+ }
2036
+ if (cmd === "/mcp_export") {
2037
+ const out = truncate(strip(runRex(["mcp", "export"], 2e4)), 3200);
2038
+ await send(token, chatId, `\u{1F4E4} MCP export
2039
+ \`\`\`
2040
+ ${out}
2041
+ \`\`\``, mcpMenu());
2042
+ logCommand(from, "/mcp_export", "done");
2043
+ return;
2044
+ }
2045
+ if (cmd.startsWith("/chat ")) {
2046
+ const userMsg = text.replace(/^\/chat\s+/i, "").trim();
2047
+ if (!userMsg) {
2048
+ await send(token, chatId, "Usage: `/chat <message>` \u2014 Talk to the REX Orchestrator");
2049
+ return;
2050
+ }
2051
+ await send(token, chatId, "\u{1F9E0} _Orchestrator thinking..._");
2052
+ try {
2053
+ const out = truncate(strip(runRex(["agents", "run", "orchestrator", "--task", userMsg, "--once"], 18e4)), 3500);
2054
+ await send(token, chatId, `\u{1F9E0} *Orchestrator*
2055
+ ${out}`, [
2056
+ [{ text: "\u{1F4AC} Continue", callback_data: "menu" }]
2057
+ ]);
2058
+ } catch {
2059
+ const fbMsg = await send(token, chatId, "\u{1F916} _Claude thinking..._");
2060
+ const fbMsgId = fbMsg?.result?.message_id;
2061
+ const response = fbMsgId ? await claudeSession(token, chatId, fbMsgId, userMsg) : await askClaude(userMsg);
2062
+ await send(token, chatId, `\u{1F916} *Claude*
2063
+ ${response}`, [
2064
+ [{ text: "\u{1F4AC} Continue", callback_data: "menu" }]
2065
+ ]);
2066
+ }
2067
+ logCommand(from, `/chat ${userMsg.slice(0, 50)}`, "orchestrator");
2068
+ return;
2069
+ }
2070
+ if (cmd === "/mcp") {
2071
+ await send(token, chatId, renderMcpSummary(), mcpMenu());
2072
+ logCommand(from, "/mcp", "listed");
2073
+ return;
2074
+ }
2075
+ if (cmd === "/files" || cmd === "/file_list") {
2076
+ await send(token, chatId, renderUploadsList(chatId), filesMenu());
2077
+ logCommand(from, "/files", "listed");
2078
+ return;
2079
+ }
2080
+ if (cmd === "/file_last") {
2081
+ const upload = latestUpload(chatId);
2082
+ if (!upload) {
2083
+ await send(token, chatId, "\u{1F4CE} No uploaded file found yet.", filesMenu());
2084
+ return;
2085
+ }
2086
+ await send(token, chatId, `\u{1F4CE} *Last File*
2087
+ \`\`\`
2088
+ ${renderUpload(upload)}
2089
+ \`\`\``, filesMenu());
2090
+ return;
2091
+ }
2092
+ if (cmd.startsWith("/file_analyze")) {
2093
+ const upload = latestUpload(chatId);
2094
+ if (!upload) {
2095
+ await send(token, chatId, "\u{1F4CE} No uploaded file found yet.", filesMenu());
2096
+ return;
2097
+ }
2098
+ const raw = text.replace(/^\/file_analyze\s*/i, "").trim();
2099
+ let mode;
2100
+ let task = raw;
2101
+ if (raw.toLowerCase().startsWith("qwen ")) {
2102
+ mode = "qwen";
2103
+ task = raw.slice(5).trim();
2104
+ } else if (raw.toLowerCase().startsWith("claude ")) {
2105
+ mode = "claude";
2106
+ task = raw.slice(7).trim();
2107
+ }
2108
+ if (!task) task = "Summarize this file and propose concrete next engineering actions.";
2109
+ const runMode = mode || state.mode;
2110
+ await send(token, chatId, `${runMode === "claude" ? "\u{1F916}" : "\u{1F9E0}"} _Analyzing latest file..._`);
2111
+ const out = await analyzeUpload(upload, task, runMode);
2112
+ await send(token, chatId, truncate(out, 3200), filesMenu());
2113
+ logCommand(from, `/file_analyze ${runMode}`, out.slice(0, 120));
2114
+ return;
2115
+ }
2116
+ if (text.length > 2) {
2117
+ state = loadState();
2118
+ state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
2119
+ saveState(state);
2120
+ let response;
2121
+ if (state.mode === "qwen") {
2122
+ response = await askQwenStream(token, chatId, text);
2123
+ } else {
2124
+ const thinkMsg = await send(token, chatId, "\u{1F916} Claude _thinking..._");
2125
+ const thinkId = thinkMsg?.result?.message_id;
2126
+ response = thinkId ? await claudeSession(token, chatId, thinkId, text) : await askClaude(text);
2127
+ await send(token, chatId, response, [
2128
+ [
2129
+ { text: "Mode: claude", callback_data: "switch_mode" },
2130
+ { text: "\u{1F4AC} Continue", callback_data: "claude_continue" }
2131
+ ]
2132
+ ]);
2133
+ }
2134
+ logCommand(from, text.slice(0, 80), response.slice(0, 100));
2135
+ return;
2136
+ }
2137
+ await send(token, chatId, "\u{1F996} *REX*\nEnvoie un message ou appuie sur Menu :", mainMenu());
2138
+ }
2139
+ var processedUpdateIds = /* @__PURE__ */ new Set();
2140
+ var MAX_PROCESSED_IDS = 500;
2141
+ function markProcessed(updateId) {
2142
+ if (processedUpdateIds.has(updateId)) return false;
2143
+ processedUpdateIds.add(updateId);
2144
+ if (processedUpdateIds.size > MAX_PROCESSED_IDS) {
2145
+ const oldest = processedUpdateIds.values().next().value;
2146
+ if (oldest !== void 0) processedUpdateIds.delete(oldest);
2147
+ }
2148
+ return true;
2149
+ }
2150
+ async function gateway() {
2151
+ const creds = getCredentials();
2152
+ if (!creds) {
2153
+ console.error(`${COLORS.red}No Telegram credentials found.${COLORS.reset}`);
2154
+ console.error(`Run ${COLORS.cyan}rex setup${COLORS.reset} to configure Telegram gateway.`);
2155
+ process.exit(1);
2156
+ }
2157
+ const { token, chatId } = creds;
2158
+ config = loadConfig();
2159
+ state = loadState();
2160
+ cleanupOldUploads();
2161
+ if (!acquireLock()) {
2162
+ console.error(`${COLORS.red}Another REX Gateway instance is already running.${COLORS.reset}`);
2163
+ console.error(`Remove ${LOCK_FILE} if this is stale, or stop the other instance first.`);
2164
+ process.exit(1);
2165
+ }
2166
+ console.log(`${COLORS.bold}REX Gateway v3${COLORS.reset} \u2014 Interactive Telegram bot`);
2167
+ console.log(`${COLORS.dim}Chat: ${chatId} | Mode: ${state.mode} | Sessions: ${state.sessionsCount}${COLORS.reset}`);
2168
+ console.log(`${COLORS.dim}Auth: restricted to chat_id ${chatId}${COLORS.reset}`);
2169
+ console.log(`${COLORS.dim}Ctrl+C to stop${COLORS.reset}
2170
+ `);
2171
+ let offset = 0;
2172
+ try {
2173
+ const flush = await fetch(`https://api.telegram.org/bot${token}/getUpdates?offset=-1`);
2174
+ const flushData = await flush.json();
2175
+ if (flushData.result?.length) {
2176
+ offset = flushData.result[flushData.result.length - 1].update_id + 1;
2177
+ }
2178
+ } catch {
2179
+ }
2180
+ await send(token, chatId, `\u{1F7E2} *REX Gateway v3* started
2181
+ Mode: ${state.mode} | Sessions: ${state.sessionsCount}`, mainMenu());
2182
+ const shutdown = (signal) => async () => {
2183
+ console.log(`
2184
+ ${COLORS.dim}Shutting down (${signal})...${COLORS.reset}`);
2185
+ activeStreamController?.abort();
2186
+ state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
2187
+ saveState(state);
2188
+ releaseLock();
2189
+ await send(token, chatId, `\u{1F534} *REX Gateway* stopped (${signal})`).catch(() => {
2190
+ });
2191
+ process.exit(signal === "SIGINT" ? 0 : 1);
2192
+ };
2193
+ process.on("SIGINT", shutdown("SIGINT"));
2194
+ process.on("SIGTERM", shutdown("SIGTERM"));
2195
+ process.on("uncaughtException", (err) => {
2196
+ console.error(`${COLORS.red}Uncaught exception:${COLORS.reset} ${err.message}`);
2197
+ releaseLock();
2198
+ process.exit(1);
2199
+ });
2200
+ process.on("unhandledRejection", (reason) => {
2201
+ console.error(`${COLORS.red}Unhandled rejection:${COLORS.reset} ${reason}`);
2202
+ releaseLock();
2203
+ process.exit(1);
2204
+ });
2205
+ while (true) {
2206
+ try {
2207
+ const pollController = new AbortController();
2208
+ const pollTimer = setTimeout(() => pollController.abort(), (config.pollTimeout + 15) * 1e3);
2209
+ let res;
2210
+ try {
2211
+ res = await fetch(
2212
+ `https://api.telegram.org/bot${token}/getUpdates?offset=${offset}&timeout=${config.pollTimeout}&allowed_updates=["message","callback_query"]`,
2213
+ { signal: pollController.signal }
2214
+ );
2215
+ } finally {
2216
+ clearTimeout(pollTimer);
2217
+ }
2218
+ const data = await res.json();
2219
+ if (!data.ok || !data.result?.length) continue;
2220
+ for (const update of data.result) {
2221
+ offset = update.update_id + 1;
2222
+ if (!markProcessed(update.update_id)) continue;
2223
+ if (update.callback_query) {
2224
+ const cb = update.callback_query;
2225
+ const cbChatId = String(cb.message?.chat?.id);
2226
+ if (!isAuthorized(cbChatId, chatId)) {
2227
+ await answerCallback(token, cb.id, "\u{1F6AB} Unauthorized");
2228
+ continue;
2229
+ }
2230
+ const from2 = cb.from?.username ?? "?";
2231
+ console.log(`${COLORS.cyan}@${from2}${COLORS.reset} [btn] ${cb.data}`);
2232
+ if (!cb.message?.message_id || !cb.data) {
2233
+ await answerCallback(token, cb.id, "\u26A0\uFE0F Stale button");
2234
+ continue;
2235
+ }
2236
+ await handleCallback(token, chatId, cb.message.message_id, cb.id, cb.data, from2);
2237
+ continue;
2238
+ }
2239
+ const msg = update.message;
2240
+ if (!msg) continue;
2241
+ if (!isAuthorized(msg.chat.id, chatId)) {
2242
+ await send(token, String(msg.chat.id), "\u{1F6AB} Unauthorized. This REX instance is private.");
2243
+ continue;
2244
+ }
2245
+ const from = msg.from?.username ?? "?";
2246
+ const attachment = pickAttachment(msg);
2247
+ if (attachment) {
2248
+ console.log(`${COLORS.cyan}@${from}${COLORS.reset}: [upload] ${attachment.kind} ${attachment.fileName}`);
2249
+ await send(token, chatId, "\u{1F4E5} _Downloading attachment..._");
2250
+ try {
2251
+ const response = await handleAttachment(token, chatId, msg, from);
2252
+ await send(token, chatId, response, filesMenu());
2253
+ } catch (e) {
2254
+ const err = e instanceof Error ? e.message : String(e);
2255
+ await send(token, chatId, `\u26A0\uFE0F Upload processing failed: ${err}`, filesMenu());
2256
+ logCommand(from, "[upload]", `error: ${err}`);
2257
+ }
2258
+ if (!msg.text) continue;
2259
+ }
2260
+ if (msg.text) {
2261
+ console.log(`${COLORS.cyan}@${from}${COLORS.reset}: ${msg.text}`);
2262
+ await handleText(token, chatId, msg.text, from);
2263
+ }
2264
+ }
2265
+ } catch (err) {
2266
+ console.error(`${COLORS.red}Poll error:${COLORS.reset} ${err.message}`);
2267
+ await new Promise((r) => setTimeout(r, 5e3));
2268
+ }
2269
+ }
2270
+ }
2271
+ export {
2272
+ gateway
2273
+ };