@cosmicstack/mercury-agent 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,8 +5,8 @@ import { readFileSync as readFileSync12, writeFileSync as writeFileSync13, exist
5
5
  import { fileURLToPath } from "url";
6
6
  import { dirname as dirname3, join as join11 } from "path";
7
7
  import { Command } from "commander";
8
- import readline2 from "readline";
9
- import chalk6 from "chalk";
8
+ import readline3 from "readline";
9
+ import chalk7 from "chalk";
10
10
 
11
11
  // src/utils/config.ts
12
12
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -95,7 +95,10 @@ function getDefaultConfig() {
95
95
  botToken: getEnv("TELEGRAM_BOT_TOKEN", ""),
96
96
  webhookUrl: getEnv("TELEGRAM_WEBHOOK_URL", ""),
97
97
  allowedChatIds: getEnv("TELEGRAM_ALLOWED_CHAT_IDS", "").split(",").filter(Boolean).map(Number),
98
- streaming: getEnvBool("TELEGRAM_STREAMING", true)
98
+ streaming: getEnvBool("TELEGRAM_STREAMING", true),
99
+ admins: [],
100
+ members: [],
101
+ pending: []
99
102
  }
100
103
  },
101
104
  github: {
@@ -122,9 +125,9 @@ function loadConfig() {
122
125
  const raw = readFileSync(CONFIG_PATH, "utf-8");
123
126
  const fileConfig = parseYaml(raw);
124
127
  const defaults = getDefaultConfig();
125
- return deepMerge(defaults, fileConfig);
128
+ return migrateLegacyTelegramAccess(deepMerge(defaults, fileConfig));
126
129
  }
127
- return getDefaultConfig();
130
+ return migrateLegacyTelegramAccess(getDefaultConfig());
128
131
  }
129
132
  function saveConfig(config) {
130
133
  const dir = getMercuryHome();
@@ -168,18 +171,145 @@ function isProviderConfigured(provider) {
168
171
  }
169
172
  return provider.apiKey.length > 0;
170
173
  }
171
- function setTelegramPairing(config, userId, chatId, username) {
172
- config.channels.telegram.pairedUserId = userId;
173
- config.channels.telegram.pairedChatId = chatId;
174
- config.channels.telegram.pairedUsername = username || void 0;
175
- return config;
174
+ function getTelegramApprovedUsers(config) {
175
+ return [
176
+ ...config.channels.telegram.admins,
177
+ ...config.channels.telegram.members
178
+ ];
179
+ }
180
+ function getTelegramApprovedChatIds(config) {
181
+ return [...new Set(getTelegramApprovedUsers(config).map((user) => user.chatId))];
182
+ }
183
+ function getTelegramAdmins(config) {
184
+ return config.channels.telegram.admins;
185
+ }
186
+ function getTelegramPendingRequests(config) {
187
+ return config.channels.telegram.pending;
188
+ }
189
+ function findTelegramApprovedUser(config, userId) {
190
+ return getTelegramApprovedUsers(config).find((user) => user.userId === userId);
191
+ }
192
+ function findTelegramAdmin(config, userId) {
193
+ return config.channels.telegram.admins.find((user) => user.userId === userId);
194
+ }
195
+ function findTelegramPendingRequest(config, userId) {
196
+ return config.channels.telegram.pending.find((request) => request.userId === userId);
197
+ }
198
+ function findTelegramPendingRequestByPairingCode(config, pairingCode) {
199
+ return config.channels.telegram.pending.find((request) => request.pairingCode === pairingCode);
200
+ }
201
+ function hasTelegramAdmins(config) {
202
+ return config.channels.telegram.admins.length > 0;
203
+ }
204
+ function getTelegramAccessSummary(config) {
205
+ return `${config.channels.telegram.admins.length} admin${config.channels.telegram.admins.length === 1 ? "" : "s"}, ${config.channels.telegram.members.length} member${config.channels.telegram.members.length === 1 ? "" : "s"}, ${config.channels.telegram.pending.length} pending`;
206
+ }
207
+ function addTelegramPendingRequest(config, request) {
208
+ const existing = findTelegramPendingRequest(config, request.userId);
209
+ if (existing) {
210
+ existing.chatId = request.chatId;
211
+ existing.username = request.username || existing.username;
212
+ existing.firstName = request.firstName || existing.firstName;
213
+ existing.pairingCode = request.pairingCode || existing.pairingCode;
214
+ return existing;
215
+ }
216
+ const created = {
217
+ ...request,
218
+ requestedAt: request.requestedAt || (/* @__PURE__ */ new Date()).toISOString()
219
+ };
220
+ config.channels.telegram.pending.push(created);
221
+ return created;
222
+ }
223
+ function approveTelegramPendingRequest(config, userId, role = "member") {
224
+ const request = findTelegramPendingRequest(config, userId);
225
+ if (!request) return null;
226
+ const approvedUser = {
227
+ userId: request.userId,
228
+ chatId: request.chatId,
229
+ username: request.username,
230
+ firstName: request.firstName,
231
+ requestedAt: request.requestedAt,
232
+ approvedAt: (/* @__PURE__ */ new Date()).toISOString()
233
+ };
234
+ config.channels.telegram.pending = config.channels.telegram.pending.filter((entry) => entry.userId !== userId);
235
+ config.channels.telegram.admins = config.channels.telegram.admins.filter((entry) => entry.userId !== userId);
236
+ config.channels.telegram.members = config.channels.telegram.members.filter((entry) => entry.userId !== userId);
237
+ if (role === "admin") {
238
+ config.channels.telegram.admins.push(approvedUser);
239
+ } else {
240
+ config.channels.telegram.members.push(approvedUser);
241
+ }
242
+ return approvedUser;
243
+ }
244
+ function approveTelegramPendingRequestByPairingCode(config, pairingCode) {
245
+ const request = findTelegramPendingRequestByPairingCode(config, pairingCode);
246
+ if (!request) return null;
247
+ const role = hasTelegramAdmins(config) ? "member" : "admin";
248
+ return approveTelegramPendingRequest(config, request.userId, role);
249
+ }
250
+ function rejectTelegramPendingRequest(config, userId) {
251
+ const request = findTelegramPendingRequest(config, userId);
252
+ if (!request) return null;
253
+ config.channels.telegram.pending = config.channels.telegram.pending.filter((entry) => entry.userId !== userId);
254
+ return request;
255
+ }
256
+ function removeTelegramUser(config, userId) {
257
+ const admin = config.channels.telegram.admins.find((entry) => entry.userId === userId);
258
+ if (admin) {
259
+ config.channels.telegram.admins = config.channels.telegram.admins.filter((entry) => entry.userId !== userId);
260
+ return admin;
261
+ }
262
+ const member = config.channels.telegram.members.find((entry) => entry.userId === userId);
263
+ if (member) {
264
+ config.channels.telegram.members = config.channels.telegram.members.filter((entry) => entry.userId !== userId);
265
+ return member;
266
+ }
267
+ return null;
268
+ }
269
+ function promoteTelegramUserToAdmin(config, userId) {
270
+ const member = config.channels.telegram.members.find((entry) => entry.userId === userId);
271
+ if (!member) return null;
272
+ config.channels.telegram.members = config.channels.telegram.members.filter((entry) => entry.userId !== userId);
273
+ config.channels.telegram.admins.push(member);
274
+ return member;
275
+ }
276
+ function demoteTelegramAdmin(config, userId) {
277
+ if (config.channels.telegram.admins.length <= 1) {
278
+ return null;
279
+ }
280
+ const admin = config.channels.telegram.admins.find((entry) => entry.userId === userId);
281
+ if (!admin) return null;
282
+ config.channels.telegram.admins = config.channels.telegram.admins.filter((entry) => entry.userId !== userId);
283
+ config.channels.telegram.members.push(admin);
284
+ return admin;
176
285
  }
177
- function clearTelegramPairing(config) {
286
+ function clearTelegramAccess(config) {
287
+ config.channels.telegram.admins = [];
288
+ config.channels.telegram.members = [];
289
+ config.channels.telegram.pending = [];
178
290
  delete config.channels.telegram.pairedUserId;
179
291
  delete config.channels.telegram.pairedChatId;
180
292
  delete config.channels.telegram.pairedUsername;
181
293
  return config;
182
294
  }
295
+ function migrateLegacyTelegramAccess(config) {
296
+ const telegram = config.channels.telegram;
297
+ telegram.admins = telegram.admins || [];
298
+ telegram.members = telegram.members || [];
299
+ telegram.pending = telegram.pending || [];
300
+ if (telegram.admins.length === 0 && telegram.members.length === 0 && typeof telegram.pairedUserId === "number" && typeof telegram.pairedChatId === "number") {
301
+ telegram.admins.push({
302
+ userId: telegram.pairedUserId,
303
+ chatId: telegram.pairedChatId,
304
+ username: telegram.pairedUsername,
305
+ approvedAt: (/* @__PURE__ */ new Date()).toISOString()
306
+ });
307
+ }
308
+ delete telegram.pairedUserId;
309
+ delete telegram.pairedChatId;
310
+ delete telegram.pairedUsername;
311
+ return config;
312
+ }
183
313
 
184
314
  // src/utils/logger.ts
185
315
  import pino from "pino";
@@ -761,208 +891,710 @@ var Lifecycle = class {
761
891
  }
762
892
  };
763
893
 
764
- // src/core/agent.ts
765
- var ToolCallLoopDetector = class {
766
- recentCalls = [];
767
- maxEntries = 10;
768
- record(toolName, params) {
769
- const paramsKey = JSON.stringify(params).slice(0, 100);
770
- this.recentCalls.push({ tool: toolName, params: paramsKey });
771
- if (this.recentCalls.length > this.maxEntries) {
772
- this.recentCalls.shift();
773
- }
894
+ // src/channels/cli.ts
895
+ import readline2 from "readline";
896
+ import fs from "fs";
897
+ import path from "path";
898
+ import chalk3 from "chalk";
899
+
900
+ // src/channels/base.ts
901
+ var BaseChannel = class {
902
+ messageHandler;
903
+ ready = false;
904
+ isReady() {
905
+ return this.ready;
774
906
  }
775
- detect() {
776
- if (this.recentCalls.length < 3) return null;
777
- const last = this.recentCalls[this.recentCalls.length - 1];
778
- let consecutiveCount = 0;
779
- for (let i = this.recentCalls.length - 1; i >= 0; i--) {
780
- if (this.recentCalls[i].tool === last.tool && this.recentCalls[i].params === last.params) {
781
- consecutiveCount++;
782
- } else {
783
- break;
784
- }
785
- }
786
- if (consecutiveCount >= 3) {
787
- return { tool: last.tool, count: consecutiveCount };
788
- }
789
- const lastTool = last.tool;
790
- let toolCount = 0;
791
- for (let i = this.recentCalls.length - 1; i >= 0; i--) {
792
- if (this.recentCalls[i].tool === lastTool) {
793
- toolCount++;
794
- } else {
795
- break;
796
- }
797
- }
798
- if (toolCount >= 4) {
799
- return { tool: lastTool, count: toolCount };
800
- }
801
- return null;
907
+ onMessage(handler) {
908
+ this.messageHandler = handler;
802
909
  }
803
- reset() {
804
- this.recentCalls = [];
910
+ emit(message) {
911
+ this.messageHandler?.(message);
805
912
  }
806
913
  };
807
- var MAX_STEPS = 10;
808
- var Agent = class {
809
- constructor(config, providers, identity, shortTerm, longTerm, episodic, channels, tokenBudget, capabilities, scheduler) {
810
- this.config = config;
811
- this.providers = providers;
812
- this.identity = identity;
813
- this.shortTerm = shortTerm;
814
- this.longTerm = longTerm;
815
- this.episodic = episodic;
816
- this.channels = channels;
817
- this.tokenBudget = tokenBudget;
818
- this.lifecycle = new Lifecycle();
819
- this.scheduler = scheduler;
820
- this.capabilities = capabilities;
821
- this.telegramStreaming = config.channels.telegram.streaming ?? true;
822
- this.scheduler.setOnScheduledTask(async (manifest) => this.handleScheduledTask(manifest));
823
- this.channels.onIncomingMessage((msg) => this.enqueueMessage(msg));
824
- this.scheduler.onHeartbeat(async () => {
825
- await this.heartbeat();
826
- });
914
+
915
+ // src/utils/markdown.ts
916
+ import { Marked } from "marked";
917
+ import chalk from "chalk";
918
+ var lexer = new Marked();
919
+ function renderMarkdown(text) {
920
+ try {
921
+ const tokens = lexer.lexer(text);
922
+ const result = renderTokens(tokens);
923
+ return result.replace(/\n{3,}/g, "\n\n").trimEnd();
924
+ } catch {
925
+ return text;
827
926
  }
828
- config;
829
- providers;
830
- identity;
831
- shortTerm;
832
- longTerm;
833
- episodic;
834
- channels;
835
- tokenBudget;
836
- lifecycle;
837
- scheduler;
838
- capabilities;
839
- running = false;
840
- messageQueue = [];
841
- processing = false;
842
- telegramStreaming;
843
- enqueueMessage(msg) {
844
- logger.info({ from: msg.channelType, content: msg.content.slice(0, 50) }, "Message enqueued");
845
- this.messageQueue.push(msg);
846
- this.processQueue();
927
+ }
928
+ function renderTokens(tokens) {
929
+ return tokens.map((t) => renderToken(t)).join("");
930
+ }
931
+ function renderToken(t) {
932
+ if (!t || typeof t !== "object") return String(t ?? "");
933
+ switch (t.type) {
934
+ case "heading":
935
+ return renderHeading(t);
936
+ case "paragraph":
937
+ return renderInline(t.tokens) + "\n\n";
938
+ case "strong":
939
+ return chalk.bold(renderInline(t.tokens));
940
+ case "em":
941
+ return chalk.italic(renderInline(t.tokens));
942
+ case "del":
943
+ return chalk.dim.strikethrough(renderInline(t.tokens));
944
+ case "codespan":
945
+ return chalk.yellow(t.text);
946
+ case "code":
947
+ return renderCodeBlock(t);
948
+ case "list":
949
+ return renderList(t);
950
+ case "blockquote":
951
+ return renderBlockquote(t);
952
+ case "hr":
953
+ return chalk.dim("\u2500".repeat(50)) + "\n\n";
954
+ case "link":
955
+ return `${chalk.blue.underline(renderInline(t.tokens))} ${chalk.dim(`(${t.href})`)}`;
956
+ case "image":
957
+ return chalk.blue(`\u{1F5BC} ${t.title || t.href}`);
958
+ case "table":
959
+ return renderTable(t);
960
+ case "text":
961
+ if (t.tokens) return renderInline(t.tokens);
962
+ return t.text || "";
963
+ case "html":
964
+ return t.text || "";
965
+ case "space":
966
+ return "";
967
+ default:
968
+ return t.text || "";
847
969
  }
848
- async processQueue() {
849
- if (this.processing) return;
850
- if (this.messageQueue.length === 0) return;
851
- if (!this.lifecycle.is("idle")) return;
852
- this.processing = true;
853
- while (this.messageQueue.length > 0) {
854
- const msg = this.messageQueue.shift();
855
- try {
856
- await this.handleMessage(msg);
857
- } catch (err) {
858
- logger.error({ err, msg: msg.content.slice(0, 50) }, "Failed to handle message");
970
+ }
971
+ function renderHeading(t) {
972
+ const text = renderInline(t.tokens);
973
+ if (t.depth === 1) return `
974
+ ${chalk.bold.cyan(text)}
975
+
976
+ `;
977
+ if (t.depth === 2) return `
978
+ ${chalk.bold.cyan(` \u25A0 ${text}`)}
979
+
980
+ `;
981
+ return `
982
+ ${chalk.bold(` \u25A0 ${text}`)}
983
+
984
+ `;
985
+ }
986
+ function renderInline(tokens) {
987
+ if (!tokens) return "";
988
+ return tokens.map((t) => {
989
+ if (typeof t === "string") return t;
990
+ if (t.type === "strong") return chalk.bold(renderInline(t.tokens));
991
+ if (t.type === "em") return chalk.italic(renderInline(t.tokens));
992
+ if (t.type === "del") return chalk.dim.strikethrough(renderInline(t.tokens));
993
+ if (t.type === "codespan") return chalk.yellow(t.text);
994
+ if (t.type === "link") return `${chalk.blue.underline(renderInline(t.tokens))} ${chalk.dim(`(${t.href})`)}`;
995
+ if (t.type === "image") return chalk.blue(`\u{1F5BC} ${t.title || t.href}`);
996
+ if (t.type === "text") {
997
+ return t.tokens ? renderInline(t.tokens) : t.text || "";
998
+ }
999
+ if (t.type === "html") return t.text || "";
1000
+ return t.text || "";
1001
+ }).join("");
1002
+ }
1003
+ function renderCodeBlock(t) {
1004
+ const lines = t.text.split("\n").map((l) => `${chalk.dim(" ")}${chalk.yellow(l)}`).join("\n");
1005
+ const langStr = t.lang ? chalk.dim(` [${t.lang}]`) : "";
1006
+ return `
1007
+ ${langStr}
1008
+ ${lines}
1009
+
1010
+ `;
1011
+ }
1012
+ function renderList(t) {
1013
+ const lines = [];
1014
+ const items = t.items || [];
1015
+ items.forEach((item, i) => {
1016
+ const bullet = t.ordered ? `${i + 1}.` : "\u2022";
1017
+ const firstLine = renderInline(item.tokens?.[0]?.tokens || [{ text: item.text }]);
1018
+ lines.push(` ${chalk.dim(bullet)} ${firstLine}`);
1019
+ const restTokens = (item.tokens || []).slice(1);
1020
+ for (const sub of restTokens) {
1021
+ if (sub.type === "list") {
1022
+ const subLines = renderList(sub).split("\n").filter(Boolean).map((l) => ` ${l}`).join("\n");
1023
+ lines.push(subLines);
1024
+ } else if (sub.type === "text") {
1025
+ lines.push(` ${chalk.dim("\u2022")} ${renderInline(sub.tokens)}`);
859
1026
  }
860
1027
  }
861
- this.processing = false;
862
- }
863
- async birth() {
864
- this.lifecycle.transition("birthing");
865
- logger.info({ name: this.config.identity.name }, "Mercury is being born...");
866
- this.lifecycle.transition("onboarding");
867
- }
868
- async wake() {
869
- this.lifecycle.transition("onboarding");
870
- this.lifecycle.transition("idle");
871
- this.scheduler.restorePersistedTasks();
872
- this.scheduler.startHeartbeat();
873
- await this.channels.startAll();
874
- this.running = true;
875
- const activeChannels = this.channels.getActiveChannels();
876
- const toolNames = this.capabilities.getToolNames();
877
- logger.info({ channels: activeChannels, tools: toolNames }, "Mercury is awake");
1028
+ });
1029
+ return lines.join("\n") + "\n\n";
1030
+ }
1031
+ function renderBlockquote(t) {
1032
+ const content = renderTokens(t.tokens || []);
1033
+ const lines = content.split("\n").filter((l) => l.trim()).map((l) => `${chalk.dim("\u2502 ")}${chalk.gray(l)}`).join("\n");
1034
+ return `
1035
+ ${lines}
1036
+
1037
+ `;
1038
+ }
1039
+ function renderTable(t) {
1040
+ const headers = (t.header || []).map((h) => chalk.bold(renderInline(h.tokens)));
1041
+ const colWidths = (t.header || []).map((h, i) => {
1042
+ const hLen = (h.text || "").length;
1043
+ const rowLens = (t.rows || []).map((row) => {
1044
+ const cell = row[i];
1045
+ return cell?.text?.length ?? 0;
1046
+ });
1047
+ return Math.max(hLen, ...rowLens) + 2;
1048
+ });
1049
+ const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(chalk.dim(" \u2502 "));
1050
+ const separator = colWidths.map((w) => "\u2500".repeat(w)).join(chalk.dim("\u2500\u253C\u2500"));
1051
+ const dataLines = (t.rows || []).map(
1052
+ (row) => row.map((cell, i) => {
1053
+ const text = renderInline(cell.tokens) || cell.text || "";
1054
+ return text.padEnd(colWidths[i]);
1055
+ }).join(chalk.dim(" \u2502 "))
1056
+ );
1057
+ return `
1058
+ ${headerLine}
1059
+ ${chalk.dim(separator)}
1060
+ ${dataLines.join("\n")}
1061
+
1062
+ `;
1063
+ }
1064
+ function escapeHtml(text) {
1065
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1066
+ }
1067
+ function mdToTelegram(text) {
1068
+ let out = text;
1069
+ const codeBlocks = [];
1070
+ out = out.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
1071
+ const placeholder = `__CODEBLOCK_${codeBlocks.length}__`;
1072
+ codeBlocks.push(`<pre><code class="${lang}">${escapeHtml(code)}</code></pre>`);
1073
+ return placeholder;
1074
+ });
1075
+ const inlineCodes = [];
1076
+ out = out.replace(/`([^`]+)`/g, (_match, code) => {
1077
+ const placeholder = `__INLINECODE_${inlineCodes.length}__`;
1078
+ inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
1079
+ return placeholder;
1080
+ });
1081
+ const links = [];
1082
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, href) => {
1083
+ const placeholder = `__LINK_${links.length}__`;
1084
+ links.push(`<a href="${escapeHtml(href)}">${escapeHtml(label)}</a>`);
1085
+ return placeholder;
1086
+ });
1087
+ out = escapeHtml(out);
1088
+ out = out.replace(/^### (.+)$/gm, "<b><i>$1</i></b>");
1089
+ out = out.replace(/^## (.+)$/gm, "<b>$1</b>");
1090
+ out = out.replace(/^# (.+)$/gm, "<b>$1</b>");
1091
+ out = out.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
1092
+ out = out.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<i>$1</i>");
1093
+ out = out.replace(/~~([^~]+)~~/g, "<s>$1</s>");
1094
+ for (let i = 0; i < inlineCodes.length; i++) {
1095
+ out = out.replace(`__INLINECODE_${i}__`, inlineCodes[i]);
878
1096
  }
879
- async sleep() {
880
- this.running = false;
881
- this.scheduler.stopAll();
882
- await this.channels.stopAll();
883
- this.lifecycle.transition("sleeping");
884
- logger.info("Mercury is sleeping");
1097
+ for (let i = 0; i < codeBlocks.length; i++) {
1098
+ out = out.replace(`__CODEBLOCK_${i}__`, codeBlocks[i]);
885
1099
  }
886
- async handleMessage(msg) {
887
- this.lifecycle.transition("thinking");
888
- const startTime = Date.now();
889
- const isInternal = msg.channelType === "internal";
890
- const isScheduled = msg.senderId === "system" && msg.channelType !== "internal";
891
- if (isInternal || isScheduled) {
892
- this.capabilities.permissions.setAutoApproveAll(true);
1100
+ for (let i = 0; i < links.length; i++) {
1101
+ out = out.replace(`__LINK_${i}__`, links[i]);
1102
+ }
1103
+ if (out.length > 4096) {
1104
+ out = out.slice(0, 4090) + "...";
1105
+ }
1106
+ return out;
1107
+ }
1108
+
1109
+ // src/utils/arrow-select.ts
1110
+ import readline from "readline";
1111
+ import chalk2 from "chalk";
1112
+ var ArrowSelectCancelledError = class extends Error {
1113
+ constructor(message = "Arrow select cancelled") {
1114
+ super(message);
1115
+ this.name = "ArrowSelectCancelledError";
1116
+ }
1117
+ };
1118
+ async function selectWithArrowKeys(title, options, config = {}) {
1119
+ if (!process.stdin.isTTY || !process.stdout.isTTY || options.length === 0) {
1120
+ return options[0]?.value ?? "";
1121
+ }
1122
+ readline.emitKeypressEvents(process.stdin);
1123
+ const stdin = process.stdin;
1124
+ const stdout = process.stdout;
1125
+ const canUseRawMode = typeof stdin.setRawMode === "function";
1126
+ const helperText = config.helperText || "Use the arrow keys, then press Enter.";
1127
+ const maxVisibleOptions = Math.max(
1128
+ 1,
1129
+ Math.min(
1130
+ options.length,
1131
+ config.maxVisibleOptions ?? Math.max(5, (stdout.rows || 12) - 7)
1132
+ )
1133
+ );
1134
+ let activeIndex = 0;
1135
+ let renderedLineCount = 0;
1136
+ let windowStart = 0;
1137
+ const topIndicator = (hasHiddenAbove) => hasHiddenAbove ? chalk2.dim(" \u2191 more") : " ";
1138
+ const bottomIndicator = (hasHiddenBelow) => hasHiddenBelow ? chalk2.dim(" \u2193 more") : " ";
1139
+ const writeLines = (lines) => {
1140
+ if (renderedLineCount > 0) {
1141
+ readline.moveCursor(stdout, 0, -(renderedLineCount - 1));
1142
+ }
1143
+ for (let index = 0; index < lines.length; index += 1) {
1144
+ readline.cursorTo(stdout, 0);
1145
+ readline.clearLine(stdout, 0);
1146
+ stdout.write(lines[index]);
1147
+ if (index < lines.length - 1) {
1148
+ stdout.write("\n");
1149
+ }
893
1150
  }
894
- try {
895
- const trimmed = msg.content.trim();
896
- if (trimmed.startsWith("/budget")) {
897
- const subcommand = trimmed.slice("/budget".length).trim();
898
- await this.handleBudgetCommand(subcommand || "status", msg.channelType, msg.channelId);
899
- this.lifecycle.transition("idle");
900
- return;
1151
+ };
1152
+ const render = () => {
1153
+ if (activeIndex < windowStart) {
1154
+ windowStart = activeIndex;
1155
+ } else if (activeIndex >= windowStart + maxVisibleOptions) {
1156
+ windowStart = activeIndex - maxVisibleOptions + 1;
1157
+ }
1158
+ const visibleOptions = options.slice(windowStart, windowStart + maxVisibleOptions);
1159
+ const hasHiddenAbove = windowStart > 0;
1160
+ const hasHiddenBelow = windowStart + maxVisibleOptions < options.length;
1161
+ const lines = [
1162
+ chalk2.bold.white(` ${title}`),
1163
+ chalk2.dim(` ${helperText}`),
1164
+ "",
1165
+ topIndicator(hasHiddenAbove),
1166
+ ...visibleOptions.map((option, visibleIndex) => {
1167
+ const index = windowStart + visibleIndex;
1168
+ const isActive = index === activeIndex;
1169
+ const marker = isActive ? chalk2.cyanBright("\u25CF") : chalk2.dim("\xB7");
1170
+ const text = isActive ? chalk2.cyanBright(option.label) : chalk2.dim(option.label);
1171
+ return ` ${marker} ${text}`;
1172
+ }),
1173
+ bottomIndicator(hasHiddenBelow),
1174
+ ""
1175
+ ];
1176
+ writeLines(lines);
1177
+ renderedLineCount = lines.length;
1178
+ };
1179
+ return await new Promise((resolve13, reject) => {
1180
+ const cleanup = () => {
1181
+ stdin.off("keypress", onKeypress);
1182
+ config.signal?.removeEventListener("abort", onAbort);
1183
+ if (canUseRawMode) {
1184
+ stdin.setRawMode(false);
901
1185
  }
902
- if (trimmed === "/budget_override") {
903
- await this.handleBudgetCommand("override", msg.channelType, msg.channelId);
904
- this.lifecycle.transition("idle");
1186
+ };
1187
+ const onAbort = () => {
1188
+ cleanup();
1189
+ reject(new ArrowSelectCancelledError());
1190
+ };
1191
+ const onKeypress = (_input, key) => {
1192
+ if (key.ctrl && key.name === "c") {
1193
+ cleanup();
1194
+ process.kill(process.pid, "SIGINT");
905
1195
  return;
906
1196
  }
907
- if (trimmed === "/budget_reset") {
908
- await this.handleBudgetCommand("reset", msg.channelType, msg.channelId);
909
- this.lifecycle.transition("idle");
1197
+ if (key.name === "up") {
1198
+ activeIndex = (activeIndex - 1 + options.length) % options.length;
1199
+ render();
910
1200
  return;
911
1201
  }
912
- if (trimmed.startsWith("/budget_set")) {
913
- const args = trimmed.slice("/budget_set".length).trim();
914
- await this.handleBudgetCommand("set " + args, msg.channelType, msg.channelId);
915
- this.lifecycle.transition("idle");
1202
+ if (key.name === "down") {
1203
+ activeIndex = (activeIndex + 1) % options.length;
1204
+ render();
916
1205
  return;
917
1206
  }
918
- if (trimmed.startsWith("/stream")) {
919
- const sub = trimmed.slice("/stream".length).trim().toLowerCase();
920
- if (sub === "off") {
921
- this.telegramStreaming = false;
922
- } else if (sub === "on") {
923
- this.telegramStreaming = true;
924
- } else {
925
- this.telegramStreaming = !this.telegramStreaming;
926
- }
927
- const ch = this.channels.get(msg.channelType);
928
- if (ch) await ch.send(
929
- this.telegramStreaming ? "Telegram streaming enabled. Responses will appear progressively." : "Telegram streaming disabled. Responses will arrive as a single message.",
930
- msg.channelId
931
- );
932
- this.lifecycle.transition("idle");
933
- return;
1207
+ if (key.name === "return") {
1208
+ const selected = options[activeIndex]?.value ?? "";
1209
+ cleanup();
1210
+ stdout.write("\n");
1211
+ resolve13(selected);
934
1212
  }
935
- if (await this.handleChatCommand(trimmed, msg.channelType, msg.channelId)) {
936
- this.lifecycle.transition("idle");
1213
+ };
1214
+ if (canUseRawMode) {
1215
+ stdin.setRawMode(true);
1216
+ }
1217
+ stdin.resume();
1218
+ stdin.on("keypress", onKeypress);
1219
+ config.signal?.addEventListener("abort", onAbort, { once: true });
1220
+ if (config.signal?.aborted) {
1221
+ onAbort();
1222
+ return;
1223
+ }
1224
+ render();
1225
+ });
1226
+ }
1227
+
1228
+ // src/channels/cli.ts
1229
+ var CLIChannel = class extends BaseChannel {
1230
+ type = "cli";
1231
+ rl = null;
1232
+ agentName;
1233
+ menuDepth = 0;
1234
+ menuAbortController = null;
1235
+ outputInProgress = 0;
1236
+ constructor(agentName = "Mercury") {
1237
+ super();
1238
+ this.agentName = agentName;
1239
+ }
1240
+ setAgentName(name) {
1241
+ this.agentName = name;
1242
+ }
1243
+ async start() {
1244
+ this.createInterface();
1245
+ this.ready = true;
1246
+ this.showPrompt();
1247
+ logger.info("CLI channel started");
1248
+ }
1249
+ createInterface() {
1250
+ this.rl = readline2.createInterface({
1251
+ input: process.stdin,
1252
+ output: process.stdout,
1253
+ prompt: " You: "
1254
+ });
1255
+ this.rl.on("line", (line) => {
1256
+ const trimmed = line.trim();
1257
+ if (!trimmed) {
1258
+ this.showPrompt();
937
1259
  return;
938
1260
  }
939
- if (this.tokenBudget.isOverBudget()) {
940
- const channel2 = this.channels.getChannelForMessage(msg);
941
- if (channel2 && msg.channelType !== "internal") {
942
- if (msg.channelType === "cli") {
943
- if (["1", "2", "3", "4"].includes(trimmed)) {
944
- await this.handleBudgetCommand(trimmed, msg.channelType, msg.channelId);
945
- this.lifecycle.transition("idle");
946
- return;
947
- }
948
- await this.handleBudgetOverrideCLI(channel2, msg);
949
- } else {
950
- await channel2.send(
951
- `I've exceeded my daily token budget (${this.tokenBudget.getStatusText()}).
952
-
953
- You can override this:
954
- \u2022 /budget override \u2014 allow one more request
955
- \u2022 /budget reset \u2014 reset usage to zero
956
- \u2022 /budget set <number> \u2014 change daily budget`,
957
- msg.channelId
958
- );
959
- }
1261
+ const msg = {
1262
+ id: Date.now().toString(36),
1263
+ channelId: "cli",
1264
+ channelType: "cli",
1265
+ senderId: "owner",
1266
+ content: trimmed,
1267
+ timestamp: Date.now()
1268
+ };
1269
+ this.emit(msg);
1270
+ });
1271
+ }
1272
+ async stop() {
1273
+ this.rl?.close();
1274
+ this.rl = null;
1275
+ this.ready = false;
1276
+ }
1277
+ async send(content, _targetId, elapsedMs) {
1278
+ this.closeActiveMenu();
1279
+ this.beginOutput();
1280
+ const timeStr = elapsedMs != null ? chalk3.dim(` (${(elapsedMs / 1e3).toFixed(1)}s)`) : "";
1281
+ const rendered = renderMarkdown(content);
1282
+ console.log("");
1283
+ console.log(chalk3.cyan(` ${this.agentName}:`) + timeStr);
1284
+ const indented = rendered.split("\n").map((line) => ` ${line}`).join("\n");
1285
+ console.log(indented);
1286
+ console.log("");
1287
+ this.endOutput();
1288
+ }
1289
+ async sendFile(filePath, _targetId) {
1290
+ this.closeActiveMenu();
1291
+ this.beginOutput();
1292
+ const resolved = path.resolve(filePath);
1293
+ if (!fs.existsSync(resolved)) {
1294
+ console.log(chalk3.red(` File not found: ${filePath}`));
1295
+ this.endOutput();
1296
+ return;
1297
+ }
1298
+ const stat = fs.statSync(resolved);
1299
+ const sizeStr = stat.size > 1024 * 1024 ? `${(stat.size / (1024 * 1024)).toFixed(1)}MB` : stat.size > 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
1300
+ console.log("");
1301
+ console.log(chalk3.cyan(` ${this.agentName}:`) + chalk3.dim(" (file)"));
1302
+ console.log(chalk3.dim(` path: ${resolved}`));
1303
+ console.log(chalk3.dim(` size: ${sizeStr}`));
1304
+ console.log("");
1305
+ this.endOutput();
1306
+ }
1307
+ async stream(content, _targetId) {
1308
+ this.closeActiveMenu();
1309
+ this.beginOutput();
1310
+ console.log("");
1311
+ process.stdout.write(chalk3.cyan(` ${this.agentName}: `));
1312
+ let full = "";
1313
+ for await (const chunk of content) {
1314
+ process.stdout.write(chunk);
1315
+ full += chunk;
1316
+ }
1317
+ console.log("\n");
1318
+ this.endOutput();
1319
+ return full;
1320
+ }
1321
+ async typing(_targetId) {
1322
+ process.stdout.write(chalk3.dim(` ${this.agentName} is thinking...\r`));
1323
+ }
1324
+ showPrompt() {
1325
+ if (this.rl) {
1326
+ this.rl.setPrompt(" You: ");
1327
+ this.rl.prompt();
1328
+ }
1329
+ }
1330
+ async withMenu(runner) {
1331
+ this.menuDepth += 1;
1332
+ this.menuAbortController = new AbortController();
1333
+ this.suspendPrompt();
1334
+ try {
1335
+ return await runner((title, options) => selectWithArrowKeys(title, options, {
1336
+ signal: this.menuAbortController?.signal
1337
+ }));
1338
+ } catch (error) {
1339
+ if (error instanceof ArrowSelectCancelledError) {
1340
+ return void 0;
1341
+ }
1342
+ throw error;
1343
+ } finally {
1344
+ this.menuDepth = Math.max(0, this.menuDepth - 1);
1345
+ if (this.menuDepth === 0) {
1346
+ this.menuAbortController = null;
1347
+ }
1348
+ if (this.menuDepth === 0) {
1349
+ this.resumePrompt();
1350
+ if (this.outputInProgress === 0) {
1351
+ this.showPrompt();
960
1352
  }
961
- this.lifecycle.transition("idle");
962
- return;
963
1353
  }
964
- const systemPrompt = this.buildSystemPrompt();
965
- const recentMemory = this.shortTerm.getRecent(msg.channelId, 10);
1354
+ }
1355
+ }
1356
+ closeActiveMenu() {
1357
+ if (!this.menuAbortController?.signal.aborted) {
1358
+ this.menuAbortController?.abort();
1359
+ }
1360
+ }
1361
+ beginOutput() {
1362
+ this.outputInProgress += 1;
1363
+ }
1364
+ endOutput() {
1365
+ this.outputInProgress = Math.max(0, this.outputInProgress - 1);
1366
+ if (this.menuDepth === 0 && this.outputInProgress === 0) {
1367
+ this.showPrompt();
1368
+ }
1369
+ }
1370
+ suspendPrompt() {
1371
+ if (!this.rl) return;
1372
+ process.stdout.write("\n");
1373
+ this.rl.close();
1374
+ this.rl = null;
1375
+ }
1376
+ resumePrompt() {
1377
+ if (!this.ready || this.rl) return;
1378
+ this.createInterface();
1379
+ }
1380
+ async prompt(question) {
1381
+ return new Promise((resolve13) => {
1382
+ this.rl?.question(question, (answer) => resolve13(answer.trim()));
1383
+ });
1384
+ }
1385
+ async askPermission(prompt) {
1386
+ return new Promise((resolve13) => {
1387
+ console.log("");
1388
+ console.log(chalk3.yellow(` \u26A0 ${prompt}`));
1389
+ this.rl?.question(chalk3.yellow(" > "), (answer) => {
1390
+ resolve13(answer.trim());
1391
+ });
1392
+ });
1393
+ }
1394
+ };
1395
+
1396
+ // src/core/agent.ts
1397
+ var ToolCallLoopDetector = class {
1398
+ recentCalls = [];
1399
+ maxEntries = 10;
1400
+ record(toolName, params) {
1401
+ const paramsKey = JSON.stringify(params).slice(0, 100);
1402
+ this.recentCalls.push({ tool: toolName, params: paramsKey });
1403
+ if (this.recentCalls.length > this.maxEntries) {
1404
+ this.recentCalls.shift();
1405
+ }
1406
+ }
1407
+ detect() {
1408
+ if (this.recentCalls.length < 3) return null;
1409
+ const last = this.recentCalls[this.recentCalls.length - 1];
1410
+ let consecutiveCount = 0;
1411
+ for (let i = this.recentCalls.length - 1; i >= 0; i--) {
1412
+ if (this.recentCalls[i].tool === last.tool && this.recentCalls[i].params === last.params) {
1413
+ consecutiveCount++;
1414
+ } else {
1415
+ break;
1416
+ }
1417
+ }
1418
+ if (consecutiveCount >= 3) {
1419
+ return { tool: last.tool, count: consecutiveCount };
1420
+ }
1421
+ const lastTool = last.tool;
1422
+ let toolCount = 0;
1423
+ for (let i = this.recentCalls.length - 1; i >= 0; i--) {
1424
+ if (this.recentCalls[i].tool === lastTool) {
1425
+ toolCount++;
1426
+ } else {
1427
+ break;
1428
+ }
1429
+ }
1430
+ if (toolCount >= 4) {
1431
+ return { tool: lastTool, count: toolCount };
1432
+ }
1433
+ return null;
1434
+ }
1435
+ reset() {
1436
+ this.recentCalls = [];
1437
+ }
1438
+ };
1439
+ var MAX_STEPS = 10;
1440
+ var Agent = class {
1441
+ constructor(config, providers, identity, shortTerm, longTerm, episodic, channels, tokenBudget, capabilities, scheduler) {
1442
+ this.config = config;
1443
+ this.providers = providers;
1444
+ this.identity = identity;
1445
+ this.shortTerm = shortTerm;
1446
+ this.longTerm = longTerm;
1447
+ this.episodic = episodic;
1448
+ this.channels = channels;
1449
+ this.tokenBudget = tokenBudget;
1450
+ this.lifecycle = new Lifecycle();
1451
+ this.scheduler = scheduler;
1452
+ this.capabilities = capabilities;
1453
+ this.telegramStreaming = config.channels.telegram.streaming ?? true;
1454
+ this.scheduler.setOnScheduledTask(async (manifest) => this.handleScheduledTask(manifest));
1455
+ this.channels.onIncomingMessage((msg) => this.enqueueMessage(msg));
1456
+ this.scheduler.onHeartbeat(async () => {
1457
+ await this.heartbeat();
1458
+ });
1459
+ }
1460
+ config;
1461
+ providers;
1462
+ identity;
1463
+ shortTerm;
1464
+ longTerm;
1465
+ episodic;
1466
+ channels;
1467
+ tokenBudget;
1468
+ lifecycle;
1469
+ scheduler;
1470
+ capabilities;
1471
+ running = false;
1472
+ messageQueue = [];
1473
+ processing = false;
1474
+ telegramStreaming;
1475
+ enqueueMessage(msg) {
1476
+ logger.info({ from: msg.channelType, content: msg.content.slice(0, 50) }, "Message enqueued");
1477
+ this.messageQueue.push(msg);
1478
+ this.processQueue();
1479
+ }
1480
+ async processQueue() {
1481
+ if (this.processing) return;
1482
+ if (this.messageQueue.length === 0) return;
1483
+ if (!this.lifecycle.is("idle")) return;
1484
+ this.processing = true;
1485
+ while (this.messageQueue.length > 0) {
1486
+ const msg = this.messageQueue.shift();
1487
+ try {
1488
+ await this.handleMessage(msg);
1489
+ } catch (err) {
1490
+ logger.error({ err, msg: msg.content.slice(0, 50) }, "Failed to handle message");
1491
+ }
1492
+ }
1493
+ this.processing = false;
1494
+ }
1495
+ async birth() {
1496
+ this.lifecycle.transition("birthing");
1497
+ logger.info({ name: this.config.identity.name }, "Mercury is being born...");
1498
+ this.lifecycle.transition("onboarding");
1499
+ }
1500
+ async wake() {
1501
+ this.lifecycle.transition("onboarding");
1502
+ this.lifecycle.transition("idle");
1503
+ this.scheduler.restorePersistedTasks();
1504
+ this.scheduler.startHeartbeat();
1505
+ await this.channels.startAll();
1506
+ this.running = true;
1507
+ const activeChannels = this.channels.getActiveChannels();
1508
+ const toolNames = this.capabilities.getToolNames();
1509
+ logger.info({ channels: activeChannels, tools: toolNames }, "Mercury is awake");
1510
+ }
1511
+ async sleep() {
1512
+ this.running = false;
1513
+ this.scheduler.stopAll();
1514
+ await this.channels.stopAll();
1515
+ this.lifecycle.transition("sleeping");
1516
+ logger.info("Mercury is sleeping");
1517
+ }
1518
+ async handleMessage(msg) {
1519
+ this.lifecycle.transition("thinking");
1520
+ const startTime = Date.now();
1521
+ const isInternal = msg.channelType === "internal";
1522
+ const isScheduled = msg.senderId === "system" && msg.channelType !== "internal";
1523
+ if (isInternal || isScheduled) {
1524
+ this.capabilities.permissions.setAutoApproveAll(true);
1525
+ }
1526
+ try {
1527
+ const trimmed = msg.content.trim();
1528
+ if (trimmed.startsWith("/budget")) {
1529
+ const subcommand = trimmed.slice("/budget".length).trim();
1530
+ await this.handleBudgetCommand(subcommand || "status", msg.channelType, msg.channelId);
1531
+ this.lifecycle.transition("idle");
1532
+ return;
1533
+ }
1534
+ if (trimmed === "/budget_override") {
1535
+ await this.handleBudgetCommand("override", msg.channelType, msg.channelId);
1536
+ this.lifecycle.transition("idle");
1537
+ return;
1538
+ }
1539
+ if (trimmed === "/budget_reset") {
1540
+ await this.handleBudgetCommand("reset", msg.channelType, msg.channelId);
1541
+ this.lifecycle.transition("idle");
1542
+ return;
1543
+ }
1544
+ if (trimmed.startsWith("/budget_set")) {
1545
+ const args = trimmed.slice("/budget_set".length).trim();
1546
+ await this.handleBudgetCommand("set " + args, msg.channelType, msg.channelId);
1547
+ this.lifecycle.transition("idle");
1548
+ return;
1549
+ }
1550
+ if (trimmed.startsWith("/stream")) {
1551
+ const sub = trimmed.slice("/stream".length).trim().toLowerCase();
1552
+ if (sub === "off") {
1553
+ this.telegramStreaming = false;
1554
+ } else if (sub === "on") {
1555
+ this.telegramStreaming = true;
1556
+ } else {
1557
+ this.telegramStreaming = !this.telegramStreaming;
1558
+ }
1559
+ const ch = this.channels.get(msg.channelType);
1560
+ if (ch) await ch.send(
1561
+ this.telegramStreaming ? "Telegram streaming enabled. Responses will appear progressively." : "Telegram streaming disabled. Responses will arrive as a single message.",
1562
+ msg.channelId
1563
+ );
1564
+ this.lifecycle.transition("idle");
1565
+ return;
1566
+ }
1567
+ if (await this.handleChatCommand(trimmed, msg.channelType, msg.channelId)) {
1568
+ this.lifecycle.transition("idle");
1569
+ return;
1570
+ }
1571
+ if (this.tokenBudget.isOverBudget()) {
1572
+ const channel2 = this.channels.getChannelForMessage(msg);
1573
+ if (channel2 && msg.channelType !== "internal") {
1574
+ if (msg.channelType === "cli") {
1575
+ if (["1", "2", "3", "4"].includes(trimmed)) {
1576
+ await this.handleBudgetCommand(trimmed, msg.channelType, msg.channelId);
1577
+ this.lifecycle.transition("idle");
1578
+ return;
1579
+ }
1580
+ await this.handleBudgetOverrideCLI(channel2, msg);
1581
+ } else {
1582
+ await channel2.send(
1583
+ `I've exceeded my daily token budget (${this.tokenBudget.getStatusText()}).
1584
+
1585
+ You can override this:
1586
+ \u2022 /budget override \u2014 allow one more request
1587
+ \u2022 /budget reset \u2014 reset usage to zero
1588
+ \u2022 /budget set <number> \u2014 change daily budget`,
1589
+ msg.channelId
1590
+ );
1591
+ }
1592
+ }
1593
+ this.lifecycle.transition("idle");
1594
+ return;
1595
+ }
1596
+ const systemPrompt = this.buildSystemPrompt();
1597
+ const recentMemory = this.shortTerm.getRecent(msg.channelId, 10);
966
1598
  const relevantFacts = this.longTerm.search(msg.content, 3);
967
1599
  const messages = [];
968
1600
  const recentSteps = this.shortTerm.getRecent(msg.channelId, 4);
@@ -1364,7 +1996,8 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
1364
1996
  }
1365
1997
  }
1366
1998
  async handleChatCommand(content, channelType, channelId) {
1367
- const cmd = content.toLowerCase().trim();
1999
+ const trimmed = content.trim();
2000
+ const cmd = trimmed.toLowerCase();
1368
2001
  const channel = this.channels.get(channelType);
1369
2002
  if (!channel) return false;
1370
2003
  const ctx = this.capabilities.getChatCommandContext();
@@ -1376,48 +2009,217 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
1376
2009
  if (cmd === "/status") {
1377
2010
  const config = ctx.config();
1378
2011
  const budget = ctx.tokenBudget();
1379
- const telegramPairing = config.channels.telegram.pairedUserId != null ? `paired to user ${config.channels.telegram.pairedUserId}${config.channels.telegram.pairedUsername ? ` (@${config.channels.telegram.pairedUsername})` : ""}` : "unpaired";
1380
2012
  const lines = [
1381
2013
  `**${config.identity.name}** \u2014 Status`,
1382
2014
  `Owner: ${config.identity.owner || "(not set)"}`,
1383
2015
  `Provider: ${config.providers.default}`,
1384
2016
  `Telegram: ${config.channels.telegram.enabled ? "enabled" : "disabled"}`,
1385
- `Telegram pairing: ${telegramPairing}`,
2017
+ `Telegram access: ${getTelegramAccessSummary(config)}`,
1386
2018
  `Budget: ${budget.getStatusText()}`,
1387
2019
  `Skills: ${ctx.skillNames().length > 0 ? ctx.skillNames().join(", ") : "none"}`
1388
2020
  ];
1389
2021
  await channel.send(lines.join("\n"), channelId);
1390
2022
  return true;
1391
2023
  }
1392
- if (cmd === "/tools") {
1393
- const tools = ctx.toolNames();
1394
- const grouped = [
1395
- `**${tools.length} tools loaded:**`,
1396
- "",
1397
- ...tools.sort().map((t) => `\u2022 \`${t}\``)
1398
- ];
1399
- await channel.send(grouped.join("\n"), channelId);
1400
- return true;
1401
- }
1402
- if (cmd === "/skills") {
1403
- const names = ctx.skillNames();
1404
- if (names.length === 0) {
1405
- await channel.send('No skills installed. Ask me to "install skill from <url>" to add one.', channelId);
1406
- } else {
2024
+ if (cmd.startsWith("/telegram")) {
2025
+ if (channelType !== "cli") {
2026
+ await channel.send("`/telegram` is only available from the Mercury CLI chat.", channelId);
2027
+ return true;
2028
+ }
2029
+ const config = ctx.config();
2030
+ const rawSubcommand = trimmed.slice("/telegram".length).trim();
2031
+ if (!rawSubcommand && channel instanceof CLIChannel) {
2032
+ await channel.withMenu(async (select) => {
2033
+ await this.openCliTelegramMenu(channel, channelId, select);
2034
+ });
2035
+ return true;
2036
+ }
2037
+ const parts = rawSubcommand.split(/\s+/).filter(Boolean);
2038
+ const action = parts[0]?.toLowerCase() || "help";
2039
+ const formatTelegramUser2 = (user) => {
2040
+ const username = user.username ? ` (@${user.username})` : "";
2041
+ const firstName = user.firstName ? ` ${user.firstName}` : "";
2042
+ const pairingCode = user.pairingCode ? ` [code: ${user.pairingCode}]` : "";
2043
+ return `${user.userId}${username}${firstName}${pairingCode}`;
2044
+ };
2045
+ const sendTelegramOverview = async () => {
1407
2046
  const lines = [
1408
- `**${names.length} skill${names.length > 1 ? "s" : ""} installed:**`,
2047
+ "**Telegram Management**",
1409
2048
  "",
1410
- ...names.map((n) => `\u2022 ${n}`)
2049
+ `Access: ${getTelegramAccessSummary(config)}`,
2050
+ `Admins: ${config.channels.telegram.admins.length > 0 ? config.channels.telegram.admins.map(formatTelegramUser2).join(", ") : "none"}`,
2051
+ `Members: ${config.channels.telegram.members.length > 0 ? config.channels.telegram.members.map(formatTelegramUser2).join(", ") : "none"}`,
2052
+ `Pending: ${config.channels.telegram.pending.length > 0 ? config.channels.telegram.pending.map(formatTelegramUser2).join(", ") : "none"}`,
2053
+ "",
2054
+ "Commands:",
2055
+ "\u2022 `/telegram pending`",
2056
+ "\u2022 `/telegram users`",
2057
+ "\u2022 `/telegram approve <pairing-code|user-id>`",
2058
+ "\u2022 `/telegram reject <user-id>`",
2059
+ "\u2022 `/telegram remove <user-id>`",
2060
+ "\u2022 `/telegram promote <user-id>`",
2061
+ "\u2022 `/telegram demote <user-id>`",
2062
+ "\u2022 `/telegram reset`"
1411
2063
  ];
1412
2064
  await channel.send(lines.join("\n"), channelId);
2065
+ };
2066
+ if (action === "help" || action === "status") {
2067
+ await sendTelegramOverview();
2068
+ return true;
1413
2069
  }
1414
- return true;
1415
- }
1416
- if (cmd === "/stream on") {
1417
- this.telegramStreaming = true;
1418
- await channel.send("Telegram streaming enabled. Responses will appear progressively.", channelId);
1419
- return true;
1420
- }
2070
+ if (action === "pending") {
2071
+ const pending = getTelegramPendingRequests(config);
2072
+ const lines = [
2073
+ "**Telegram Pending Requests**",
2074
+ "",
2075
+ pending.length > 0 ? pending.map(formatTelegramUser2).join("\n") : "No pending Telegram requests."
2076
+ ];
2077
+ await channel.send(lines.join("\n"), channelId);
2078
+ return true;
2079
+ }
2080
+ if (action === "users") {
2081
+ const approved = getTelegramApprovedUsers(config);
2082
+ const lines = [
2083
+ "**Telegram Approved Users**",
2084
+ "",
2085
+ `Admins: ${config.channels.telegram.admins.length > 0 ? config.channels.telegram.admins.map(formatTelegramUser2).join(", ") : "none"}`,
2086
+ `Members: ${config.channels.telegram.members.length > 0 ? config.channels.telegram.members.map(formatTelegramUser2).join(", ") : "none"}`,
2087
+ "",
2088
+ `Total approved: ${approved.length}`
2089
+ ];
2090
+ await channel.send(lines.join("\n"), channelId);
2091
+ return true;
2092
+ }
2093
+ if (action === "approve") {
2094
+ const value = parts[1];
2095
+ if (!value) {
2096
+ await channel.send("Usage: `/telegram approve <pairing-code|user-id>`", channelId);
2097
+ return true;
2098
+ }
2099
+ let approved = approveTelegramPendingRequestByPairingCode(config, value);
2100
+ let resultLabel = value;
2101
+ if (!approved) {
2102
+ const userId = Number(value);
2103
+ if (!isNaN(userId)) {
2104
+ approved = approveTelegramPendingRequest(config, userId, "member");
2105
+ resultLabel = userId.toString();
2106
+ }
2107
+ }
2108
+ if (!approved) {
2109
+ await channel.send(`No pending Telegram request found for \`${resultLabel}\`.`, channelId);
2110
+ return true;
2111
+ }
2112
+ saveConfig(config);
2113
+ await channel.send(`Approved Telegram user ${formatTelegramUser2(approved)}.`, channelId);
2114
+ return true;
2115
+ }
2116
+ if (action === "reject") {
2117
+ const value = Number(parts[1]);
2118
+ if (isNaN(value)) {
2119
+ await channel.send("Usage: `/telegram reject <user-id>`", channelId);
2120
+ return true;
2121
+ }
2122
+ const rejected = rejectTelegramPendingRequest(config, value);
2123
+ if (!rejected) {
2124
+ await channel.send(`No pending Telegram request found for \`${value}\`.`, channelId);
2125
+ return true;
2126
+ }
2127
+ saveConfig(config);
2128
+ await channel.send(`Rejected Telegram request for ${formatTelegramUser2(rejected)}.`, channelId);
2129
+ return true;
2130
+ }
2131
+ if (action === "remove") {
2132
+ const value = Number(parts[1]);
2133
+ if (isNaN(value)) {
2134
+ await channel.send("Usage: `/telegram remove <user-id>`", channelId);
2135
+ return true;
2136
+ }
2137
+ const removed = removeTelegramUser(config, value);
2138
+ if (!removed) {
2139
+ await channel.send(`No approved Telegram user found for \`${value}\`.`, channelId);
2140
+ return true;
2141
+ }
2142
+ saveConfig(config);
2143
+ await channel.send(`Removed Telegram access for ${formatTelegramUser2(removed)}.`, channelId);
2144
+ return true;
2145
+ }
2146
+ if (action === "promote") {
2147
+ const value = Number(parts[1]);
2148
+ if (isNaN(value)) {
2149
+ await channel.send("Usage: `/telegram promote <user-id>`", channelId);
2150
+ return true;
2151
+ }
2152
+ const promoted = promoteTelegramUserToAdmin(config, value);
2153
+ if (!promoted) {
2154
+ await channel.send(`No Telegram member found for \`${value}\`.`, channelId);
2155
+ return true;
2156
+ }
2157
+ saveConfig(config);
2158
+ await channel.send(`Promoted ${formatTelegramUser2(promoted)} to Telegram admin.`, channelId);
2159
+ return true;
2160
+ }
2161
+ if (action === "demote") {
2162
+ const value = Number(parts[1]);
2163
+ if (isNaN(value)) {
2164
+ await channel.send("Usage: `/telegram demote <user-id>`", channelId);
2165
+ return true;
2166
+ }
2167
+ const demoted = demoteTelegramAdmin(config, value);
2168
+ if (!demoted) {
2169
+ await channel.send("Could not demote that Telegram admin. Mercury must keep at least one admin.", channelId);
2170
+ return true;
2171
+ }
2172
+ saveConfig(config);
2173
+ await channel.send(`Demoted ${formatTelegramUser2(demoted)} to Telegram member.`, channelId);
2174
+ return true;
2175
+ }
2176
+ if (action === "reset" || action === "unpair") {
2177
+ config.channels.telegram.admins = [];
2178
+ config.channels.telegram.members = [];
2179
+ config.channels.telegram.pending = [];
2180
+ saveConfig(config);
2181
+ await channel.send("Telegram access reset. New users can send /start to begin pairing again.", channelId);
2182
+ return true;
2183
+ }
2184
+ await channel.send(
2185
+ `Unknown Telegram command "${action}". Try \`/telegram\`, \`/telegram pending\`, or \`/telegram users\`.`,
2186
+ channelId
2187
+ );
2188
+ return true;
2189
+ }
2190
+ if ((cmd === "/" || cmd === "/menu") && channelType === "cli" && channel instanceof CLIChannel) {
2191
+ await this.openCliCommandMenu(channel, channelId);
2192
+ return true;
2193
+ }
2194
+ if (cmd === "/tools") {
2195
+ const tools = ctx.toolNames();
2196
+ const grouped = [
2197
+ `**${tools.length} tools loaded:**`,
2198
+ "",
2199
+ ...tools.sort().map((t) => `\u2022 \`${t}\``)
2200
+ ];
2201
+ await channel.send(grouped.join("\n"), channelId);
2202
+ return true;
2203
+ }
2204
+ if (cmd === "/skills") {
2205
+ const names = ctx.skillNames();
2206
+ if (names.length === 0) {
2207
+ await channel.send('No skills installed. Ask me to "install skill from <url>" to add one.', channelId);
2208
+ } else {
2209
+ const lines = [
2210
+ `**${names.length} skill${names.length > 1 ? "s" : ""} installed:**`,
2211
+ "",
2212
+ ...names.map((n) => `\u2022 ${n}`)
2213
+ ];
2214
+ await channel.send(lines.join("\n"), channelId);
2215
+ }
2216
+ return true;
2217
+ }
2218
+ if (cmd === "/stream on") {
2219
+ this.telegramStreaming = true;
2220
+ await channel.send("Telegram streaming enabled. Responses will appear progressively.", channelId);
2221
+ return true;
2222
+ }
1421
2223
  if (cmd === "/stream off") {
1422
2224
  this.telegramStreaming = false;
1423
2225
  await channel.send("Telegram streaming disabled. Responses will arrive as a single message.", channelId);
@@ -1438,6 +2240,205 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
1438
2240
  }
1439
2241
  return false;
1440
2242
  }
2243
+ async openCliCommandMenu(channel, channelId) {
2244
+ const ctx = this.capabilities.getChatCommandContext();
2245
+ if (!ctx) return;
2246
+ await channel.withMenu(async (select) => {
2247
+ while (true) {
2248
+ const streamLabel = this.telegramStreaming ? "Disable Telegram Streaming" : "Enable Telegram Streaming";
2249
+ const action = await select("Mercury Commands", [
2250
+ { value: "status", label: "Status" },
2251
+ { value: "telegram", label: "Telegram" },
2252
+ { value: "tools", label: "Tools" },
2253
+ { value: "skills", label: "Skills" },
2254
+ { value: "stream", label: streamLabel },
2255
+ { value: "help", label: "Help" },
2256
+ { value: "exit", label: "Exit" }
2257
+ ]);
2258
+ if (action === "exit") {
2259
+ return;
2260
+ }
2261
+ if (action === "status") {
2262
+ await this.handleChatCommand("/status", "cli", channelId);
2263
+ continue;
2264
+ }
2265
+ if (action === "telegram") {
2266
+ await this.openCliTelegramMenu(channel, channelId, select);
2267
+ continue;
2268
+ }
2269
+ if (action === "tools") {
2270
+ await this.handleChatCommand("/tools", "cli", channelId);
2271
+ continue;
2272
+ }
2273
+ if (action === "skills") {
2274
+ await this.handleChatCommand("/skills", "cli", channelId);
2275
+ continue;
2276
+ }
2277
+ if (action === "stream") {
2278
+ await this.handleChatCommand("/stream", "cli", channelId);
2279
+ continue;
2280
+ }
2281
+ if (action === "help") {
2282
+ await channel.send(ctx.manual(), channelId);
2283
+ }
2284
+ }
2285
+ });
2286
+ }
2287
+ async openCliTelegramMenu(channel, channelId, select) {
2288
+ const ctx = this.capabilities.getChatCommandContext();
2289
+ if (!ctx) return;
2290
+ const formatTelegramUser2 = (user) => {
2291
+ const username = user.username ? ` (@${user.username})` : "";
2292
+ const firstName = user.firstName ? ` ${user.firstName}` : "";
2293
+ const pairingCode = user.pairingCode ? ` [code: ${user.pairingCode}]` : "";
2294
+ return `${user.userId}${username}${firstName}${pairingCode}`;
2295
+ };
2296
+ const selectFromUsers = async (title, users, emptyMessage, backValue = "back") => {
2297
+ if (users.length === 0) {
2298
+ await channel.send(emptyMessage, channelId);
2299
+ return backValue;
2300
+ }
2301
+ return select(title, [
2302
+ ...users.map((user) => ({
2303
+ value: user.pairingCode || user.userId.toString(),
2304
+ label: formatTelegramUser2(user)
2305
+ })),
2306
+ { value: backValue, label: "Back" }
2307
+ ]);
2308
+ };
2309
+ while (true) {
2310
+ const config = ctx.config();
2311
+ const action = await select("Telegram Commands", [
2312
+ { value: "overview", label: "Overview" },
2313
+ { value: "pending", label: `Pending Requests (${config.channels.telegram.pending.length})` },
2314
+ { value: "users", label: `Approved Users (${getTelegramApprovedUsers(config).length})` },
2315
+ { value: "approve", label: "Approve Request" },
2316
+ { value: "reject", label: "Reject Request" },
2317
+ { value: "remove", label: "Remove User" },
2318
+ { value: "promote", label: "Promote to Admin" },
2319
+ { value: "demote", label: "Demote Admin" },
2320
+ { value: "reset", label: "Reset Telegram Access" },
2321
+ { value: "back", label: "Back" },
2322
+ { value: "exit", label: "Exit" }
2323
+ ]);
2324
+ if (action === "exit") {
2325
+ return;
2326
+ }
2327
+ if (action === "back") {
2328
+ return;
2329
+ }
2330
+ if (action === "overview") {
2331
+ await this.handleChatCommand("/telegram status", "cli", channelId);
2332
+ continue;
2333
+ }
2334
+ if (action === "pending") {
2335
+ await this.handleChatCommand("/telegram pending", "cli", channelId);
2336
+ continue;
2337
+ }
2338
+ if (action === "users") {
2339
+ await this.handleChatCommand("/telegram users", "cli", channelId);
2340
+ continue;
2341
+ }
2342
+ if (action === "approve") {
2343
+ const pending = getTelegramPendingRequests(config);
2344
+ const selected = await selectFromUsers(
2345
+ "Approve Telegram Request",
2346
+ pending,
2347
+ "There are no pending Telegram requests to approve."
2348
+ );
2349
+ if (selected === "back") {
2350
+ continue;
2351
+ }
2352
+ await this.handleChatCommand(`/telegram approve ${selected}`, "cli", channelId);
2353
+ continue;
2354
+ }
2355
+ if (action === "reject") {
2356
+ const pending = getTelegramPendingRequests(config);
2357
+ const selected = await selectFromUsers(
2358
+ "Reject Telegram Request",
2359
+ pending,
2360
+ "There are no pending Telegram requests to reject."
2361
+ );
2362
+ if (selected === "back") {
2363
+ continue;
2364
+ }
2365
+ const request = pending.find((entry) => (entry.pairingCode || entry.userId.toString()) === selected);
2366
+ if (!request) {
2367
+ await channel.send("That Telegram request is no longer pending.", channelId);
2368
+ continue;
2369
+ }
2370
+ await this.handleChatCommand(`/telegram reject ${request.userId}`, "cli", channelId);
2371
+ continue;
2372
+ }
2373
+ if (action === "remove") {
2374
+ const approved = getTelegramApprovedUsers(config);
2375
+ const selected = await selectFromUsers(
2376
+ "Remove Telegram User",
2377
+ approved,
2378
+ "There are no approved Telegram users to remove."
2379
+ );
2380
+ if (selected === "back") {
2381
+ continue;
2382
+ }
2383
+ const user = approved.find((entry) => entry.userId.toString() === selected);
2384
+ if (!user) {
2385
+ await channel.send("That Telegram user is no longer approved.", channelId);
2386
+ continue;
2387
+ }
2388
+ await this.handleChatCommand(`/telegram remove ${user.userId}`, "cli", channelId);
2389
+ continue;
2390
+ }
2391
+ if (action === "promote") {
2392
+ const members = config.channels.telegram.members;
2393
+ const selected = await selectFromUsers(
2394
+ "Promote Telegram Member",
2395
+ members,
2396
+ "There are no Telegram members available to promote."
2397
+ );
2398
+ if (selected === "back") {
2399
+ continue;
2400
+ }
2401
+ const member = members.find((entry) => entry.userId.toString() === selected);
2402
+ if (!member) {
2403
+ await channel.send("That Telegram member is no longer available.", channelId);
2404
+ continue;
2405
+ }
2406
+ await this.handleChatCommand(`/telegram promote ${member.userId}`, "cli", channelId);
2407
+ continue;
2408
+ }
2409
+ if (action === "demote") {
2410
+ const admins = config.channels.telegram.admins;
2411
+ const selected = await selectFromUsers(
2412
+ "Demote Telegram Admin",
2413
+ admins,
2414
+ "There are no Telegram admins available to demote."
2415
+ );
2416
+ if (selected === "back") {
2417
+ continue;
2418
+ }
2419
+ const admin = admins.find((entry) => entry.userId.toString() === selected);
2420
+ if (!admin) {
2421
+ await channel.send("That Telegram admin is no longer available.", channelId);
2422
+ continue;
2423
+ }
2424
+ await this.handleChatCommand(`/telegram demote ${admin.userId}`, "cli", channelId);
2425
+ continue;
2426
+ }
2427
+ if (action === "reset") {
2428
+ const confirmation = await select("Reset Telegram Access?", [
2429
+ { value: "cancel", label: "Cancel" },
2430
+ { value: "confirm", label: "Reset all Telegram access" },
2431
+ { value: "back", label: "Back" }
2432
+ ]);
2433
+ if (confirmation === "confirm") {
2434
+ clearTelegramAccess(config);
2435
+ saveConfig(config);
2436
+ await channel.send("Telegram access reset. New users can send /start to begin pairing again.", channelId);
2437
+ }
2438
+ continue;
2439
+ }
2440
+ }
2441
+ }
1441
2442
  };
1442
2443
 
1443
2444
  // src/core/scheduler.ts
@@ -1496,437 +2497,117 @@ var Scheduler = class {
1496
2497
  await this.heartbeatHandler?.();
1497
2498
  } catch (err) {
1498
2499
  logger.error({ err }, "Heartbeat error");
1499
- }
1500
- }, ms);
1501
- }
1502
- stopHeartbeat() {
1503
- if (this.heartbeatTimer) {
1504
- clearInterval(this.heartbeatTimer);
1505
- this.heartbeatTimer = null;
1506
- logger.info("Heartbeat stopped");
1507
- }
1508
- }
1509
- addTask(task) {
1510
- if (this.tasks.has(task.id)) {
1511
- this.removeTask(task.id);
1512
- }
1513
- const scheduled = cron.schedule(task.cron, async () => {
1514
- try {
1515
- await task.handler();
1516
- } catch (err) {
1517
- logger.error({ task: task.id, err }, "Scheduled task error");
1518
- }
1519
- });
1520
- this.tasks.set(task.id, scheduled);
1521
- logger.info({ id: task.id, cron: task.cron, desc: task.description }, "Task scheduled");
1522
- }
1523
- addPersistedTask(manifest) {
1524
- this.taskManifests.set(manifest.id, manifest);
1525
- this.addTask({
1526
- id: manifest.id,
1527
- cron: manifest.cron,
1528
- description: manifest.description,
1529
- handler: async () => {
1530
- logger.info({ task: manifest.id }, "Scheduled task firing");
1531
- if (this.onScheduledTask) {
1532
- await this.onScheduledTask(manifest);
1533
- }
1534
- }
1535
- });
1536
- }
1537
- addDelayedTask(manifest) {
1538
- this.taskManifests.set(manifest.id, manifest);
1539
- const delayMs = (manifest.delaySeconds || 60) * 1e3;
1540
- const timer = setTimeout(async () => {
1541
- try {
1542
- logger.info({ task: manifest.id }, "Delayed task firing");
1543
- if (this.onScheduledTask) {
1544
- await this.onScheduledTask(manifest);
1545
- }
1546
- } catch (err) {
1547
- logger.error({ task: manifest.id, err }, "Delayed task error");
1548
- } finally {
1549
- this.delayedTasks.delete(manifest.id);
1550
- this.taskManifests.delete(manifest.id);
1551
- this.persistSchedules();
1552
- }
1553
- }, delayMs);
1554
- this.delayedTasks.set(manifest.id, timer);
1555
- logger.info({ id: manifest.id, delaySeconds: manifest.delaySeconds }, "Delayed task scheduled");
1556
- }
1557
- removeTask(id) {
1558
- const task = this.tasks.get(id);
1559
- if (task) {
1560
- task.stop();
1561
- this.tasks.delete(id);
1562
- }
1563
- const timer = this.delayedTasks.get(id);
1564
- if (timer) {
1565
- clearTimeout(timer);
1566
- this.delayedTasks.delete(id);
1567
- }
1568
- this.taskManifests.delete(id);
1569
- }
1570
- getManifests() {
1571
- return [...this.taskManifests.values()];
1572
- }
1573
- restorePersistedTasks() {
1574
- const persisted = loadSchedules();
1575
- for (const manifest of persisted) {
1576
- if (manifest.delaySeconds) {
1577
- const executeAt = manifest.executeAt ? new Date(manifest.executeAt) : null;
1578
- const now = Date.now();
1579
- if (executeAt && executeAt.getTime() > now) {
1580
- const remainingMs = executeAt.getTime() - now;
1581
- manifest.delaySeconds = Math.ceil(remainingMs / 1e3);
1582
- this.addDelayedTask(manifest);
1583
- } else {
1584
- logger.info({ id: manifest.id }, "Delayed task already expired, skipping");
1585
- }
1586
- } else if (manifest.cron && cron.validate(manifest.cron)) {
1587
- this.addPersistedTask(manifest);
1588
- } else {
1589
- logger.warn({ id: manifest.id, cron: manifest.cron }, "Skipping invalid task");
1590
- }
1591
- }
1592
- if (persisted.length > 0) {
1593
- logger.info({ count: persisted.length }, "Restored persisted scheduled tasks");
1594
- }
1595
- }
1596
- persistSchedules() {
1597
- saveSchedules(this.getManifests());
1598
- }
1599
- stopAll() {
1600
- this.stopHeartbeat();
1601
- for (const [, task] of this.tasks) {
1602
- task.stop();
1603
- }
1604
- for (const [, timer] of this.delayedTasks) {
1605
- clearTimeout(timer);
1606
- }
1607
- this.tasks.clear();
1608
- this.delayedTasks.clear();
1609
- this.taskManifests.clear();
1610
- }
1611
- };
1612
-
1613
- // src/channels/cli.ts
1614
- import readline from "readline";
1615
- import fs from "fs";
1616
- import path from "path";
1617
- import chalk2 from "chalk";
1618
-
1619
- // src/channels/base.ts
1620
- var BaseChannel = class {
1621
- messageHandler;
1622
- ready = false;
1623
- isReady() {
1624
- return this.ready;
1625
- }
1626
- onMessage(handler) {
1627
- this.messageHandler = handler;
1628
- }
1629
- emit(message) {
1630
- this.messageHandler?.(message);
1631
- }
1632
- };
1633
-
1634
- // src/utils/markdown.ts
1635
- import { Marked } from "marked";
1636
- import chalk from "chalk";
1637
- var lexer = new Marked();
1638
- function renderMarkdown(text) {
1639
- try {
1640
- const tokens = lexer.lexer(text);
1641
- const result = renderTokens(tokens);
1642
- return result.replace(/\n{3,}/g, "\n\n").trimEnd();
1643
- } catch {
1644
- return text;
1645
- }
1646
- }
1647
- function renderTokens(tokens) {
1648
- return tokens.map((t) => renderToken(t)).join("");
1649
- }
1650
- function renderToken(t) {
1651
- if (!t || typeof t !== "object") return String(t ?? "");
1652
- switch (t.type) {
1653
- case "heading":
1654
- return renderHeading(t);
1655
- case "paragraph":
1656
- return renderInline(t.tokens) + "\n\n";
1657
- case "strong":
1658
- return chalk.bold(renderInline(t.tokens));
1659
- case "em":
1660
- return chalk.italic(renderInline(t.tokens));
1661
- case "del":
1662
- return chalk.dim.strikethrough(renderInline(t.tokens));
1663
- case "codespan":
1664
- return chalk.yellow(t.text);
1665
- case "code":
1666
- return renderCodeBlock(t);
1667
- case "list":
1668
- return renderList(t);
1669
- case "blockquote":
1670
- return renderBlockquote(t);
1671
- case "hr":
1672
- return chalk.dim("\u2500".repeat(50)) + "\n\n";
1673
- case "link":
1674
- return `${chalk.blue.underline(renderInline(t.tokens))} ${chalk.dim(`(${t.href})`)}`;
1675
- case "image":
1676
- return chalk.blue(`\u{1F5BC} ${t.title || t.href}`);
1677
- case "table":
1678
- return renderTable(t);
1679
- case "text":
1680
- if (t.tokens) return renderInline(t.tokens);
1681
- return t.text || "";
1682
- case "html":
1683
- return t.text || "";
1684
- case "space":
1685
- return "";
1686
- default:
1687
- return t.text || "";
1688
- }
1689
- }
1690
- function renderHeading(t) {
1691
- const text = renderInline(t.tokens);
1692
- if (t.depth === 1) return `
1693
- ${chalk.bold.cyan(text)}
1694
-
1695
- `;
1696
- if (t.depth === 2) return `
1697
- ${chalk.bold.cyan(` \u25A0 ${text}`)}
1698
-
1699
- `;
1700
- return `
1701
- ${chalk.bold(` \u25A0 ${text}`)}
1702
-
1703
- `;
1704
- }
1705
- function renderInline(tokens) {
1706
- if (!tokens) return "";
1707
- return tokens.map((t) => {
1708
- if (typeof t === "string") return t;
1709
- if (t.type === "strong") return chalk.bold(renderInline(t.tokens));
1710
- if (t.type === "em") return chalk.italic(renderInline(t.tokens));
1711
- if (t.type === "del") return chalk.dim.strikethrough(renderInline(t.tokens));
1712
- if (t.type === "codespan") return chalk.yellow(t.text);
1713
- if (t.type === "link") return `${chalk.blue.underline(renderInline(t.tokens))} ${chalk.dim(`(${t.href})`)}`;
1714
- if (t.type === "image") return chalk.blue(`\u{1F5BC} ${t.title || t.href}`);
1715
- if (t.type === "text") {
1716
- return t.tokens ? renderInline(t.tokens) : t.text || "";
1717
- }
1718
- if (t.type === "html") return t.text || "";
1719
- return t.text || "";
1720
- }).join("");
1721
- }
1722
- function renderCodeBlock(t) {
1723
- const lines = t.text.split("\n").map((l) => `${chalk.dim(" ")}${chalk.yellow(l)}`).join("\n");
1724
- const langStr = t.lang ? chalk.dim(` [${t.lang}]`) : "";
1725
- return `
1726
- ${langStr}
1727
- ${lines}
1728
-
1729
- `;
1730
- }
1731
- function renderList(t) {
1732
- const lines = [];
1733
- const items = t.items || [];
1734
- items.forEach((item, i) => {
1735
- const bullet = t.ordered ? `${i + 1}.` : "\u2022";
1736
- const firstLine = renderInline(item.tokens?.[0]?.tokens || [{ text: item.text }]);
1737
- lines.push(` ${chalk.dim(bullet)} ${firstLine}`);
1738
- const restTokens = (item.tokens || []).slice(1);
1739
- for (const sub of restTokens) {
1740
- if (sub.type === "list") {
1741
- const subLines = renderList(sub).split("\n").filter(Boolean).map((l) => ` ${l}`).join("\n");
1742
- lines.push(subLines);
1743
- } else if (sub.type === "text") {
1744
- lines.push(` ${chalk.dim("\u2022")} ${renderInline(sub.tokens)}`);
1745
- }
1746
- }
1747
- });
1748
- return lines.join("\n") + "\n\n";
1749
- }
1750
- function renderBlockquote(t) {
1751
- const content = renderTokens(t.tokens || []);
1752
- const lines = content.split("\n").filter((l) => l.trim()).map((l) => `${chalk.dim("\u2502 ")}${chalk.gray(l)}`).join("\n");
1753
- return `
1754
- ${lines}
1755
-
1756
- `;
1757
- }
1758
- function renderTable(t) {
1759
- const headers = (t.header || []).map((h) => chalk.bold(renderInline(h.tokens)));
1760
- const colWidths = (t.header || []).map((h, i) => {
1761
- const hLen = (h.text || "").length;
1762
- const rowLens = (t.rows || []).map((row) => {
1763
- const cell = row[i];
1764
- return cell?.text?.length ?? 0;
1765
- });
1766
- return Math.max(hLen, ...rowLens) + 2;
1767
- });
1768
- const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(chalk.dim(" \u2502 "));
1769
- const separator = colWidths.map((w) => "\u2500".repeat(w)).join(chalk.dim("\u2500\u253C\u2500"));
1770
- const dataLines = (t.rows || []).map(
1771
- (row) => row.map((cell, i) => {
1772
- const text = renderInline(cell.tokens) || cell.text || "";
1773
- return text.padEnd(colWidths[i]);
1774
- }).join(chalk.dim(" \u2502 "))
1775
- );
1776
- return `
1777
- ${headerLine}
1778
- ${chalk.dim(separator)}
1779
- ${dataLines.join("\n")}
1780
-
1781
- `;
1782
- }
1783
- function escapeHtml(text) {
1784
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1785
- }
1786
- function mdToTelegram(text) {
1787
- let out = text;
1788
- const codeBlocks = [];
1789
- out = out.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
1790
- const placeholder = `__CODEBLOCK_${codeBlocks.length}__`;
1791
- codeBlocks.push(`<pre><code class="${lang}">${escapeHtml(code)}</code></pre>`);
1792
- return placeholder;
1793
- });
1794
- const inlineCodes = [];
1795
- out = out.replace(/`([^`]+)`/g, (_match, code) => {
1796
- const placeholder = `__INLINECODE_${inlineCodes.length}__`;
1797
- inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
1798
- return placeholder;
1799
- });
1800
- const links = [];
1801
- out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, href) => {
1802
- const placeholder = `__LINK_${links.length}__`;
1803
- links.push(`<a href="${escapeHtml(href)}">${escapeHtml(label)}</a>`);
1804
- return placeholder;
1805
- });
1806
- out = escapeHtml(out);
1807
- out = out.replace(/^### (.+)$/gm, "<b><i>$1</i></b>");
1808
- out = out.replace(/^## (.+)$/gm, "<b>$1</b>");
1809
- out = out.replace(/^# (.+)$/gm, "<b>$1</b>");
1810
- out = out.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
1811
- out = out.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<i>$1</i>");
1812
- out = out.replace(/~~([^~]+)~~/g, "<s>$1</s>");
1813
- for (let i = 0; i < inlineCodes.length; i++) {
1814
- out = out.replace(`__INLINECODE_${i}__`, inlineCodes[i]);
1815
- }
1816
- for (let i = 0; i < codeBlocks.length; i++) {
1817
- out = out.replace(`__CODEBLOCK_${i}__`, codeBlocks[i]);
1818
- }
1819
- for (let i = 0; i < links.length; i++) {
1820
- out = out.replace(`__LINK_${i}__`, links[i]);
1821
- }
1822
- if (out.length > 4096) {
1823
- out = out.slice(0, 4090) + "...";
1824
- }
1825
- return out;
1826
- }
1827
-
1828
- // src/channels/cli.ts
1829
- var CLIChannel = class extends BaseChannel {
1830
- type = "cli";
1831
- rl = null;
1832
- agentName;
1833
- constructor(agentName = "Mercury") {
1834
- super();
1835
- this.agentName = agentName;
2500
+ }
2501
+ }, ms);
1836
2502
  }
1837
- setAgentName(name) {
1838
- this.agentName = name;
2503
+ stopHeartbeat() {
2504
+ if (this.heartbeatTimer) {
2505
+ clearInterval(this.heartbeatTimer);
2506
+ this.heartbeatTimer = null;
2507
+ logger.info("Heartbeat stopped");
2508
+ }
1839
2509
  }
1840
- async start() {
1841
- this.rl = readline.createInterface({
1842
- input: process.stdin,
1843
- output: process.stdout,
1844
- prompt: " You: "
1845
- });
1846
- this.rl.on("line", (line) => {
1847
- const trimmed = line.trim();
1848
- if (!trimmed) {
1849
- this.showPrompt();
1850
- return;
2510
+ addTask(task) {
2511
+ if (this.tasks.has(task.id)) {
2512
+ this.removeTask(task.id);
2513
+ }
2514
+ const scheduled = cron.schedule(task.cron, async () => {
2515
+ try {
2516
+ await task.handler();
2517
+ } catch (err) {
2518
+ logger.error({ task: task.id, err }, "Scheduled task error");
1851
2519
  }
1852
- const msg = {
1853
- id: Date.now().toString(36),
1854
- channelId: "cli",
1855
- channelType: "cli",
1856
- senderId: "owner",
1857
- content: trimmed,
1858
- timestamp: Date.now()
1859
- };
1860
- this.emit(msg);
1861
2520
  });
1862
- this.ready = true;
1863
- this.showPrompt();
1864
- logger.info("CLI channel started");
2521
+ this.tasks.set(task.id, scheduled);
2522
+ logger.info({ id: task.id, cron: task.cron, desc: task.description }, "Task scheduled");
1865
2523
  }
1866
- async stop() {
1867
- this.rl?.close();
1868
- this.rl = null;
1869
- this.ready = false;
2524
+ addPersistedTask(manifest) {
2525
+ this.taskManifests.set(manifest.id, manifest);
2526
+ this.addTask({
2527
+ id: manifest.id,
2528
+ cron: manifest.cron,
2529
+ description: manifest.description,
2530
+ handler: async () => {
2531
+ logger.info({ task: manifest.id }, "Scheduled task firing");
2532
+ if (this.onScheduledTask) {
2533
+ await this.onScheduledTask(manifest);
2534
+ }
2535
+ }
2536
+ });
1870
2537
  }
1871
- async send(content, _targetId, elapsedMs) {
1872
- const timeStr = elapsedMs != null ? chalk2.dim(` (${(elapsedMs / 1e3).toFixed(1)}s)`) : "";
1873
- const rendered = renderMarkdown(content);
1874
- console.log("");
1875
- console.log(chalk2.cyan(` ${this.agentName}:`) + timeStr);
1876
- const indented = rendered.split("\n").map((line) => ` ${line}`).join("\n");
1877
- console.log(indented);
1878
- console.log("");
1879
- this.showPrompt();
2538
+ addDelayedTask(manifest) {
2539
+ this.taskManifests.set(manifest.id, manifest);
2540
+ const delayMs = (manifest.delaySeconds || 60) * 1e3;
2541
+ const timer = setTimeout(async () => {
2542
+ try {
2543
+ logger.info({ task: manifest.id }, "Delayed task firing");
2544
+ if (this.onScheduledTask) {
2545
+ await this.onScheduledTask(manifest);
2546
+ }
2547
+ } catch (err) {
2548
+ logger.error({ task: manifest.id, err }, "Delayed task error");
2549
+ } finally {
2550
+ this.delayedTasks.delete(manifest.id);
2551
+ this.taskManifests.delete(manifest.id);
2552
+ this.persistSchedules();
2553
+ }
2554
+ }, delayMs);
2555
+ this.delayedTasks.set(manifest.id, timer);
2556
+ logger.info({ id: manifest.id, delaySeconds: manifest.delaySeconds }, "Delayed task scheduled");
1880
2557
  }
1881
- async sendFile(filePath, _targetId) {
1882
- const resolved = path.resolve(filePath);
1883
- if (!fs.existsSync(resolved)) {
1884
- console.log(chalk2.red(` File not found: ${filePath}`));
1885
- return;
2558
+ removeTask(id) {
2559
+ const task = this.tasks.get(id);
2560
+ if (task) {
2561
+ task.stop();
2562
+ this.tasks.delete(id);
1886
2563
  }
1887
- const stat = fs.statSync(resolved);
1888
- const sizeStr = stat.size > 1024 * 1024 ? `${(stat.size / (1024 * 1024)).toFixed(1)}MB` : stat.size > 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
1889
- console.log("");
1890
- console.log(chalk2.cyan(` ${this.agentName}:`) + chalk2.dim(" (file)"));
1891
- console.log(chalk2.dim(` path: ${resolved}`));
1892
- console.log(chalk2.dim(` size: ${sizeStr}`));
1893
- console.log("");
1894
- this.showPrompt();
1895
- }
1896
- async stream(content, _targetId) {
1897
- console.log("");
1898
- process.stdout.write(chalk2.cyan(` ${this.agentName}: `));
1899
- let full = "";
1900
- for await (const chunk of content) {
1901
- process.stdout.write(chunk);
1902
- full += chunk;
2564
+ const timer = this.delayedTasks.get(id);
2565
+ if (timer) {
2566
+ clearTimeout(timer);
2567
+ this.delayedTasks.delete(id);
1903
2568
  }
1904
- console.log("\n");
1905
- this.showPrompt();
1906
- return full;
2569
+ this.taskManifests.delete(id);
1907
2570
  }
1908
- async typing(_targetId) {
1909
- process.stdout.write(chalk2.dim(` ${this.agentName} is thinking...\r`));
2571
+ getManifests() {
2572
+ return [...this.taskManifests.values()];
1910
2573
  }
1911
- showPrompt() {
1912
- if (this.rl) {
1913
- this.rl.setPrompt(" You: ");
1914
- this.rl.prompt();
2574
+ restorePersistedTasks() {
2575
+ const persisted = loadSchedules();
2576
+ for (const manifest of persisted) {
2577
+ if (manifest.delaySeconds) {
2578
+ const executeAt = manifest.executeAt ? new Date(manifest.executeAt) : null;
2579
+ const now = Date.now();
2580
+ if (executeAt && executeAt.getTime() > now) {
2581
+ const remainingMs = executeAt.getTime() - now;
2582
+ manifest.delaySeconds = Math.ceil(remainingMs / 1e3);
2583
+ this.addDelayedTask(manifest);
2584
+ } else {
2585
+ logger.info({ id: manifest.id }, "Delayed task already expired, skipping");
2586
+ }
2587
+ } else if (manifest.cron && cron.validate(manifest.cron)) {
2588
+ this.addPersistedTask(manifest);
2589
+ } else {
2590
+ logger.warn({ id: manifest.id, cron: manifest.cron }, "Skipping invalid task");
2591
+ }
2592
+ }
2593
+ if (persisted.length > 0) {
2594
+ logger.info({ count: persisted.length }, "Restored persisted scheduled tasks");
1915
2595
  }
1916
2596
  }
1917
- async prompt(question) {
1918
- return new Promise((resolve13) => {
1919
- this.rl?.question(question, (answer) => resolve13(answer.trim()));
1920
- });
2597
+ persistSchedules() {
2598
+ saveSchedules(this.getManifests());
1921
2599
  }
1922
- async askPermission(prompt) {
1923
- return new Promise((resolve13) => {
1924
- console.log("");
1925
- console.log(chalk2.yellow(` \u26A0 ${prompt}`));
1926
- this.rl?.question(chalk2.yellow(" > "), (answer) => {
1927
- resolve13(answer.trim());
1928
- });
1929
- });
2600
+ stopAll() {
2601
+ this.stopHeartbeat();
2602
+ for (const [, task] of this.tasks) {
2603
+ task.stop();
2604
+ }
2605
+ for (const [, timer] of this.delayedTasks) {
2606
+ clearTimeout(timer);
2607
+ }
2608
+ this.tasks.clear();
2609
+ this.delayedTasks.clear();
2610
+ this.taskManifests.clear();
1930
2611
  }
1931
2612
  };
1932
2613
 
@@ -1936,16 +2617,16 @@ import path2 from "path";
1936
2617
  import { Bot, InputFile, InlineKeyboard } from "grammy";
1937
2618
  import { autoRetry } from "@grammyjs/auto-retry";
1938
2619
  var MAX_MESSAGE_LENGTH = 4096;
2620
+ var ACCESS_ACTION_PREFIX = "tg_access";
1939
2621
  var TelegramChannel = class extends BaseChannel {
1940
2622
  constructor(config) {
1941
2623
  super();
1942
2624
  this.config = config;
1943
- this.ownerChatId = config.channels.telegram.pairedChatId ?? null;
1944
2625
  }
1945
2626
  config;
1946
2627
  type = "telegram";
1947
2628
  bot = null;
1948
- ownerChatId = null;
2629
+ lastActiveChatId = null;
1949
2630
  typingInterval = null;
1950
2631
  chatCommandContext;
1951
2632
  pendingApprovals = /* @__PURE__ */ new Map();
@@ -1963,30 +2644,41 @@ var TelegramChannel = class extends BaseChannel {
1963
2644
  bot.on("message:text", async (ctx) => {
1964
2645
  const chatId = ctx.chat.id;
1965
2646
  const userId = ctx.from?.id;
2647
+ const username = ctx.from?.username;
2648
+ const firstName = ctx.from?.first_name;
1966
2649
  const text = ctx.message.text?.trim() || "";
2650
+ const command = this.getCommandName(text);
1967
2651
  if (!userId) return;
1968
2652
  if (ctx.chat.type !== "private") {
1969
2653
  await this.sendDirectMessage(chatId, "This bot is only available in private one-to-one chats.");
1970
2654
  return;
1971
2655
  }
1972
- if (!this.isPaired()) {
1973
- await this.handleUnpairedMessage(userId, chatId, text, ctx.from?.username);
2656
+ if (command === "/start" || command === "/pair") {
2657
+ await this.handleAccessRequest(userId, chatId, username, firstName);
1974
2658
  return;
1975
2659
  }
1976
- if (!this.isAuthorizedUser(userId)) {
1977
- await this.sendDirectMessage(chatId, "This bot is not available to you.");
2660
+ const approvedUser = findTelegramApprovedUser(this.config, userId);
2661
+ if (!approvedUser) {
2662
+ const pending = findTelegramPendingRequest(this.config, userId);
2663
+ if (pending) {
2664
+ await this.sendDirectMessage(chatId, this.getPendingStatusMessage());
2665
+ } else {
2666
+ await this.sendDirectMessage(chatId, "This bot is not available to you. Send /start to request access.");
2667
+ }
1978
2668
  return;
1979
2669
  }
1980
- this.ownerChatId = chatId;
2670
+ this.lastActiveChatId = chatId;
1981
2671
  logger.info({ chatId, text: ctx.message.text?.slice(0, 50) }, "Telegram message received");
1982
- const command = text.toLowerCase();
1983
- if (command === "/start" || command === "/pair") {
1984
- await this.sendDirectMessage(chatId, this.getPairingStatusMessage());
1985
- return;
1986
- }
1987
2672
  if (command === "/unpair") {
1988
- this.unpair();
1989
- await this.sendDirectMessage(chatId, "Telegram pairing removed. Send /start to pair this Mercury instance again.");
2673
+ if (!this.isAdminUser(userId)) {
2674
+ await this.sendDirectMessage(chatId, "Only Telegram admins can reset Telegram access.");
2675
+ return;
2676
+ }
2677
+ this.resetAccess();
2678
+ await this.sendDirectMessage(
2679
+ chatId,
2680
+ "Telegram access reset. New users can send /start to request access. The first request must be approved from the Mercury CLI."
2681
+ );
1990
2682
  return;
1991
2683
  }
1992
2684
  const msg = {
@@ -2003,6 +2695,10 @@ var TelegramChannel = class extends BaseChannel {
2003
2695
  });
2004
2696
  bot.on("callback_query:data", async (ctx) => {
2005
2697
  const data = ctx.callbackQuery.data;
2698
+ if (data.startsWith(`${ACCESS_ACTION_PREFIX}:`)) {
2699
+ await this.handleAccessCallback(ctx, data);
2700
+ return;
2701
+ }
2006
2702
  const resolver = this.pendingApprovals.get(data);
2007
2703
  if (!resolver) {
2008
2704
  await ctx.answerCallbackQuery({ text: "Expired" });
@@ -2017,19 +2713,33 @@ var TelegramChannel = class extends BaseChannel {
2017
2713
  logger.error({ err: err.message }, "Telegram bot error");
2018
2714
  });
2019
2715
  this.bot = bot;
2020
- await bot.start({
2021
- onStart: async (info) => {
2022
- logger.info({ bot: info.username }, "Telegram bot started \u2014 long polling active");
2023
- this.ready = true;
2024
- await this.registerCommands();
2025
- }
2716
+ await new Promise((resolve13, reject) => {
2717
+ let settled = false;
2718
+ void bot.start({
2719
+ onStart: async (info) => {
2720
+ logger.info({ bot: info.username }, "Telegram bot started \u2014 long polling active");
2721
+ this.ready = true;
2722
+ await this.registerCommands();
2723
+ if (!settled) {
2724
+ settled = true;
2725
+ resolve13();
2726
+ }
2727
+ }
2728
+ }).catch((err) => {
2729
+ if (!settled) {
2730
+ settled = true;
2731
+ reject(err);
2732
+ return;
2733
+ }
2734
+ logger.error({ err: err.message }, "Telegram bot start loop failed after startup");
2735
+ });
2026
2736
  });
2027
2737
  }
2028
2738
  async registerCommands() {
2029
2739
  if (!this.bot) return;
2030
2740
  const commands = [
2031
- { command: "start", description: "Pair this Telegram account to Mercury" },
2032
- { command: "pair", description: "Pair this Telegram account to Mercury" },
2741
+ { command: "start", description: "Request Telegram access to this Mercury instance" },
2742
+ { command: "pair", description: "Request Telegram access to this Mercury instance" },
2033
2743
  { command: "help", description: "Show capabilities and commands manual" },
2034
2744
  { command: "status", description: "Show agent config, budget, and uptime" },
2035
2745
  { command: "tools", description: "List all loaded tools" },
@@ -2039,7 +2749,7 @@ var TelegramChannel = class extends BaseChannel {
2039
2749
  { command: "budget_reset", description: "Reset token usage to zero" },
2040
2750
  { command: "budget_set", description: "Set new daily token budget" },
2041
2751
  { command: "stream", description: "Toggle text streaming on/off" },
2042
- { command: "unpair", description: "Remove Telegram pairing for this Mercury instance" }
2752
+ { command: "unpair", description: "Reset all Telegram access for this Mercury instance" }
2043
2753
  ];
2044
2754
  try {
2045
2755
  await this.bot.api.setMyCommands(commands);
@@ -2054,9 +2764,9 @@ var TelegramChannel = class extends BaseChannel {
2054
2764
  this.stopTypingLoop();
2055
2765
  }
2056
2766
  async send(content, targetId, elapsedMs) {
2057
- const chatId = this.parseChatId(targetId);
2058
- if (!chatId || !this.bot) {
2059
- logger.warn({ targetId, chatId }, "Telegram send: no valid chat ID");
2767
+ const chatIds = this.resolveTargetChatIds(targetId);
2768
+ if (chatIds.length === 0 || !this.bot) {
2769
+ logger.warn({ targetId, chatIds }, "Telegram send: no valid chat IDs");
2060
2770
  return;
2061
2771
  }
2062
2772
  const timeSuffix = elapsedMs != null ? `
@@ -2064,69 +2774,79 @@ var TelegramChannel = class extends BaseChannel {
2064
2774
  const fullContent = content + timeSuffix;
2065
2775
  const html = mdToTelegram(fullContent);
2066
2776
  const chunks = this.splitMessage(html, MAX_MESSAGE_LENGTH);
2067
- for (const chunk of chunks) {
2068
- try {
2069
- await this.bot.api.sendMessage(chatId, chunk, { parse_mode: "HTML" });
2070
- } catch (err) {
2071
- logger.warn({ err: err.message }, "HTML parse failed, sending as plain text");
2777
+ for (const chatId of chatIds) {
2778
+ for (const chunk of chunks) {
2072
2779
  try {
2073
- await this.bot.api.sendMessage(chatId, this.stripHtml(chunk));
2074
- } catch (err2) {
2075
- logger.error({ err: err2.message }, "Telegram send failed");
2780
+ await this.bot.api.sendMessage(chatId, chunk, { parse_mode: "HTML" });
2781
+ } catch (err) {
2782
+ logger.warn({ err: err.message, chatId }, "HTML parse failed, sending as plain text");
2783
+ try {
2784
+ await this.bot.api.sendMessage(chatId, this.stripHtml(chunk));
2785
+ } catch (err2) {
2786
+ logger.error({ err: err2.message, chatId }, "Telegram send failed");
2787
+ }
2076
2788
  }
2077
2789
  }
2078
2790
  }
2079
2791
  }
2080
2792
  async sendFile(filePath, targetId) {
2081
- const chatId = this.parseChatId(targetId);
2082
- if (!chatId || !this.bot) {
2083
- logger.warn({ targetId, chatId }, "Telegram sendFile: no valid chat ID");
2793
+ const chatIds = this.resolveTargetChatIds(targetId);
2794
+ if (chatIds.length === 0 || !this.bot) {
2795
+ logger.warn({ targetId, chatIds }, "Telegram sendFile: no valid chat IDs");
2084
2796
  return;
2085
2797
  }
2086
2798
  const resolved = path2.resolve(filePath);
2087
2799
  if (!fs2.existsSync(resolved)) {
2088
- await this.bot.api.sendMessage(chatId, `File not found: ${filePath}`);
2800
+ for (const chatId of chatIds) {
2801
+ await this.bot.api.sendMessage(chatId, `File not found: ${filePath}`).catch(() => {
2802
+ });
2803
+ }
2089
2804
  return;
2090
2805
  }
2091
- const inputFile = new InputFile(resolved);
2092
2806
  const filename = path2.basename(resolved);
2093
2807
  const ext = path2.extname(resolved).toLowerCase();
2094
- try {
2095
- if (this.isImageFile(ext)) {
2096
- await this.bot.api.sendPhoto(chatId, inputFile, { caption: filename });
2097
- } else if (this.isAudioFile(ext)) {
2098
- await this.bot.api.sendAudio(chatId, inputFile, { title: filename });
2099
- } else if (this.isVideoFile(ext)) {
2100
- await this.bot.api.sendVideo(chatId, inputFile, { caption: filename });
2101
- } else {
2102
- await this.bot.api.sendDocument(chatId, inputFile, { caption: filename });
2808
+ for (const chatId of chatIds) {
2809
+ const inputFile = new InputFile(resolved);
2810
+ try {
2811
+ if (this.isImageFile(ext)) {
2812
+ await this.bot.api.sendPhoto(chatId, inputFile, { caption: filename });
2813
+ } else if (this.isAudioFile(ext)) {
2814
+ await this.bot.api.sendAudio(chatId, inputFile, { title: filename });
2815
+ } else if (this.isVideoFile(ext)) {
2816
+ await this.bot.api.sendVideo(chatId, inputFile, { caption: filename });
2817
+ } else {
2818
+ await this.bot.api.sendDocument(chatId, inputFile, { caption: filename });
2819
+ }
2820
+ logger.info({ file: resolved, chatId }, "File sent via Telegram");
2821
+ } catch (err) {
2822
+ logger.error({ err: err.message, file: resolved, chatId }, "Telegram sendFile failed");
2823
+ await this.bot.api.sendMessage(chatId, `Failed to send file: ${err.message}`).catch(() => {
2824
+ });
2103
2825
  }
2104
- logger.info({ file: resolved, chatId }, "File sent via Telegram");
2105
- } catch (err) {
2106
- logger.error({ err: err.message, file: resolved }, "Telegram sendFile failed");
2107
- await this.bot.api.sendMessage(chatId, `Failed to send file: ${err.message}`).catch(() => {
2108
- });
2109
2826
  }
2110
2827
  }
2111
2828
  async stream(content, targetId) {
2112
- const chatId = this.parseChatId(targetId);
2113
- if (!chatId || !this.bot) return "";
2829
+ const chatIds = this.resolveTargetChatIds(targetId);
2830
+ if (chatIds.length === 0 || !this.bot) return "";
2114
2831
  let full = "";
2115
2832
  for await (const chunk of content) {
2116
2833
  full += chunk;
2117
2834
  }
2118
2835
  const html = mdToTelegram(full);
2119
- try {
2120
- await this.bot.api.sendMessage(chatId, html, { parse_mode: "HTML" });
2121
- } catch (err) {
2122
- await this.bot.api.sendMessage(chatId, this.stripHtml(html));
2836
+ for (const chatId of chatIds) {
2837
+ try {
2838
+ await this.bot.api.sendMessage(chatId, html, { parse_mode: "HTML" });
2839
+ } catch (err) {
2840
+ await this.bot.api.sendMessage(chatId, this.stripHtml(html)).catch(() => {
2841
+ });
2842
+ }
2123
2843
  }
2124
2844
  return full;
2125
2845
  }
2126
2846
  async typing(targetId) {
2127
- const chatId = this.parseChatId(targetId);
2128
- if (!chatId || !this.bot) return;
2129
- await this.bot.api.sendChatAction(chatId, "typing");
2847
+ const chatIds = this.resolveTargetChatIds(targetId);
2848
+ if (chatIds.length === 0 || !this.bot) return;
2849
+ await this.bot.api.sendChatAction(chatIds[0], "typing");
2130
2850
  }
2131
2851
  startTypingLoop(chatId) {
2132
2852
  this.stopTypingLoop();
@@ -2200,7 +2920,8 @@ var TelegramChannel = class extends BaseChannel {
2200
2920
  }
2201
2921
  }
2202
2922
  async askPermission(prompt, targetId) {
2203
- const chatId = this.parseChatId(targetId);
2923
+ const chatIds = this.resolveTargetChatIds(targetId);
2924
+ const chatId = chatIds[0];
2204
2925
  if (!chatId || !this.bot) return "no";
2205
2926
  const id = `perm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2206
2927
  const keyboard = new InlineKeyboard().text("Allow", `${id}:yes`).text("Always", `${id}:always`).text("Deny", `${id}:no`);
@@ -2215,17 +2936,189 @@ var TelegramChannel = class extends BaseChannel {
2215
2936
  reply_markup: keyboard
2216
2937
  });
2217
2938
  }
2218
- return new Promise((resolve13) => {
2219
- this.pendingApprovals.set(`${id}:yes`, () => resolve13("yes"));
2220
- this.pendingApprovals.set(`${id}:always`, () => resolve13("always"));
2221
- this.pendingApprovals.set(`${id}:no`, () => resolve13("no"));
2222
- setTimeout(() => {
2223
- this.pendingApprovals.delete(`${id}:yes`);
2224
- this.pendingApprovals.delete(`${id}:always`);
2225
- this.pendingApprovals.delete(`${id}:no`);
2226
- resolve13("no");
2227
- }, 12e4);
2228
- });
2939
+ return new Promise((resolve13) => {
2940
+ this.pendingApprovals.set(`${id}:yes`, () => resolve13("yes"));
2941
+ this.pendingApprovals.set(`${id}:always`, () => resolve13("always"));
2942
+ this.pendingApprovals.set(`${id}:no`, () => resolve13("no"));
2943
+ setTimeout(() => {
2944
+ this.pendingApprovals.delete(`${id}:yes`);
2945
+ this.pendingApprovals.delete(`${id}:always`);
2946
+ this.pendingApprovals.delete(`${id}:no`);
2947
+ resolve13("no");
2948
+ }, 12e4);
2949
+ });
2950
+ }
2951
+ async handleAccessRequest(userId, chatId, username, firstName) {
2952
+ const approvedUser = findTelegramApprovedUser(this.config, userId);
2953
+ if (approvedUser) {
2954
+ await this.sendDirectMessage(chatId, this.getApprovedStatusMessage(approvedUser));
2955
+ return;
2956
+ }
2957
+ const existingRequest = findTelegramPendingRequest(this.config, userId);
2958
+ if (existingRequest) {
2959
+ await this.sendDirectMessage(chatId, this.getPendingStatusMessage(existingRequest));
2960
+ return;
2961
+ }
2962
+ if (!hasTelegramAdmins(this.config) && this.config.channels.telegram.pending.length > 0) {
2963
+ await this.sendDirectMessage(
2964
+ chatId,
2965
+ "Initial Telegram pairing is already in progress for another user. Ask the Mercury operator to finish setup or reset Telegram access first."
2966
+ );
2967
+ return;
2968
+ }
2969
+ const request = addTelegramPendingRequest(this.config, {
2970
+ userId,
2971
+ chatId,
2972
+ username,
2973
+ firstName,
2974
+ pairingCode: hasTelegramAdmins(this.config) ? void 0 : this.generatePairingCode()
2975
+ });
2976
+ saveConfig(this.config);
2977
+ logger.info({ chatId, userId, username }, "Telegram access request recorded");
2978
+ await this.sendDirectMessage(chatId, this.getPendingStatusMessage(request));
2979
+ if (!hasTelegramAdmins(this.config)) {
2980
+ return;
2981
+ }
2982
+ await this.notifyAdminsOfPendingRequest(request);
2983
+ }
2984
+ async notifyAdminsOfPendingRequest(request) {
2985
+ if (!this.bot) return;
2986
+ const keyboard = new InlineKeyboard().text("Approve", `${ACCESS_ACTION_PREFIX}:approve:${request.userId}`).text("Reject", `${ACCESS_ACTION_PREFIX}:reject:${request.userId}`);
2987
+ const username = request.username ? ` (@${request.username})` : "";
2988
+ const firstName = request.firstName ? ` (${request.firstName})` : "";
2989
+ const message = [
2990
+ "Telegram access request pending approval.",
2991
+ "",
2992
+ `User ID: ${request.userId}${username}${firstName}`,
2993
+ `Requested: ${new Date(request.requestedAt).toLocaleString()}`,
2994
+ "",
2995
+ "Use the buttons below to approve or reject this user."
2996
+ ].join("\n");
2997
+ for (const admin of getTelegramAdmins(this.config)) {
2998
+ try {
2999
+ await this.bot.api.sendMessage(admin.chatId, mdToTelegram(message), {
3000
+ parse_mode: "HTML",
3001
+ reply_markup: keyboard
3002
+ });
3003
+ } catch {
3004
+ await this.bot.api.sendMessage(admin.chatId, message, {
3005
+ reply_markup: keyboard
3006
+ }).catch(() => {
3007
+ });
3008
+ }
3009
+ }
3010
+ }
3011
+ async handleAccessCallback(ctx, data) {
3012
+ const actorUserId = ctx.from?.id;
3013
+ const actorChatId = ctx.chat?.id;
3014
+ if (!actorUserId || !actorChatId) {
3015
+ await ctx.answerCallbackQuery({ text: "Unavailable" });
3016
+ return;
3017
+ }
3018
+ if (!this.isAdminUser(actorUserId)) {
3019
+ await ctx.answerCallbackQuery({ text: "Admins only" });
3020
+ return;
3021
+ }
3022
+ const [, action, rawUserId] = data.split(":");
3023
+ const requestUserId = Number(rawUserId);
3024
+ if (!requestUserId) {
3025
+ await ctx.answerCallbackQuery({ text: "Invalid request" });
3026
+ return;
3027
+ }
3028
+ const request = findTelegramPendingRequest(this.config, requestUserId);
3029
+ if (!request) {
3030
+ await ctx.answerCallbackQuery({ text: "Already handled" });
3031
+ await ctx.editMessageReplyMarkup({ reply_markup: void 0 }).catch(() => {
3032
+ });
3033
+ return;
3034
+ }
3035
+ if (action === "approve") {
3036
+ const approved = approveTelegramPendingRequest(this.config, requestUserId, "member");
3037
+ if (!approved) {
3038
+ await ctx.answerCallbackQuery({ text: "Already handled" });
3039
+ return;
3040
+ }
3041
+ saveConfig(this.config);
3042
+ await ctx.answerCallbackQuery({ text: "Approved" });
3043
+ await ctx.editMessageReplyMarkup({ reply_markup: void 0 }).catch(() => {
3044
+ });
3045
+ await this.sendDirectMessage(
3046
+ request.chatId,
3047
+ `Telegram access approved. You can now chat with Mercury.
3048
+
3049
+ Telegram access: ${getTelegramAccessSummary(this.config)}`
3050
+ );
3051
+ await this.sendDirectMessage(actorChatId, `Approved Telegram access for ${this.formatRequestLabel(request)}.`);
3052
+ return;
3053
+ }
3054
+ if (action === "reject") {
3055
+ const rejected = rejectTelegramPendingRequest(this.config, requestUserId);
3056
+ if (!rejected) {
3057
+ await ctx.answerCallbackQuery({ text: "Already handled" });
3058
+ return;
3059
+ }
3060
+ saveConfig(this.config);
3061
+ await ctx.answerCallbackQuery({ text: "Rejected" });
3062
+ await ctx.editMessageReplyMarkup({ reply_markup: void 0 }).catch(() => {
3063
+ });
3064
+ await this.sendDirectMessage(
3065
+ request.chatId,
3066
+ "Your Telegram access request was rejected. This bot is not available to you."
3067
+ );
3068
+ await this.sendDirectMessage(actorChatId, `Rejected Telegram access for ${this.formatRequestLabel(request)}.`);
3069
+ return;
3070
+ }
3071
+ await ctx.answerCallbackQuery({ text: "Unknown action" });
3072
+ }
3073
+ resolveTargetChatIds(targetId) {
3074
+ if (!targetId || targetId === "notification") {
3075
+ return getTelegramApprovedChatIds(this.config);
3076
+ }
3077
+ if (targetId.startsWith("telegram:")) {
3078
+ const raw = Number(targetId.split(":")[1]);
3079
+ return isNaN(raw) ? [] : [raw];
3080
+ }
3081
+ const num = Number(targetId);
3082
+ return isNaN(num) ? [] : [num];
3083
+ }
3084
+ isAdminUser(userId) {
3085
+ return !!findTelegramAdmin(this.config, userId);
3086
+ }
3087
+ getCommandName(text) {
3088
+ return text.trim().split(/\s+/)[0]?.toLowerCase() || "";
3089
+ }
3090
+ getPendingStatusMessage(request) {
3091
+ if (!hasTelegramAdmins(this.config)) {
3092
+ const pairingCode = request?.pairingCode ?? "unknown";
3093
+ return [
3094
+ "Your Telegram pairing request has been recorded.",
3095
+ "",
3096
+ `Pairing code: ${pairingCode}`,
3097
+ "",
3098
+ "Enter this code in the Mercury terminal to finish setup."
3099
+ ].join("\n");
3100
+ }
3101
+ return "Your Telegram access request has been recorded and is waiting for approval from a Telegram admin.";
3102
+ }
3103
+ getApprovedStatusMessage(user) {
3104
+ const role = this.isAdminUser(user.userId) ? "admin" : "member";
3105
+ return `You are already approved as a Telegram ${role}.
3106
+
3107
+ Telegram access: ${getTelegramAccessSummary(this.config)}`;
3108
+ }
3109
+ formatRequestLabel(request) {
3110
+ const username = request.username ? ` (@${request.username})` : "";
3111
+ const firstName = request.firstName ? ` ${request.firstName}` : "";
3112
+ return `${request.userId}${username}${firstName}`;
3113
+ }
3114
+ resetAccess() {
3115
+ clearTelegramAccess(this.config);
3116
+ saveConfig(this.config);
3117
+ this.lastActiveChatId = null;
3118
+ logger.info("Telegram access reset");
3119
+ }
3120
+ generatePairingCode() {
3121
+ return Math.floor(1e5 + Math.random() * 9e5).toString();
2229
3122
  }
2230
3123
  escapeHtml(text) {
2231
3124
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -2259,50 +3152,6 @@ var TelegramChannel = class extends BaseChannel {
2259
3152
  isVideoFile(ext) {
2260
3153
  return [".mp4", ".mov", ".avi", ".mkv", ".webm"].includes(ext);
2261
3154
  }
2262
- parseChatId(targetId) {
2263
- if (!targetId) return this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null;
2264
- if (targetId.startsWith("telegram:")) {
2265
- const raw = Number(targetId.split(":")[1]);
2266
- return isNaN(raw) ? this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null : raw;
2267
- }
2268
- if (targetId === "notification") return this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null;
2269
- const num = Number(targetId);
2270
- return isNaN(num) ? this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null : num;
2271
- }
2272
- isPaired() {
2273
- return typeof this.config.channels.telegram.pairedUserId === "number";
2274
- }
2275
- isAuthorizedUser(userId) {
2276
- return this.config.channels.telegram.pairedUserId === userId;
2277
- }
2278
- async handleUnpairedMessage(userId, chatId, text, username) {
2279
- const command = text.toLowerCase();
2280
- if (command === "/start" || command === "/pair") {
2281
- setTelegramPairing(this.config, userId, chatId, username);
2282
- saveConfig(this.config);
2283
- this.ownerChatId = chatId;
2284
- logger.info({ chatId, userId, username }, "Telegram paired to owner");
2285
- await this.sendDirectMessage(chatId, this.getPairingStatusMessage(true));
2286
- return;
2287
- }
2288
- await this.sendDirectMessage(
2289
- chatId,
2290
- "This Mercury instance is not paired yet. Send /start to pair this bot to your Telegram account."
2291
- );
2292
- }
2293
- getPairingStatusMessage(newlyPaired = false) {
2294
- const username = this.config.channels.telegram.pairedUsername ? ` (@${this.config.channels.telegram.pairedUsername})` : "";
2295
- const prefix = newlyPaired ? "Telegram paired successfully." : "This Telegram account is already paired.";
2296
- return `${prefix}
2297
-
2298
- Owner user ID: ${this.config.channels.telegram.pairedUserId}${username}`;
2299
- }
2300
- unpair() {
2301
- clearTelegramPairing(this.config);
2302
- saveConfig(this.config);
2303
- this.ownerChatId = null;
2304
- logger.info("Telegram pairing cleared");
2305
- }
2306
3155
  async sendDirectMessage(chatId, content) {
2307
3156
  if (!this.bot) return;
2308
3157
  try {
@@ -3116,7 +3965,7 @@ import { existsSync as existsSync12, statSync as statSync2 } from "fs";
3116
3965
  import { resolve as resolve9, basename, isAbsolute as isAbsolute7 } from "path";
3117
3966
  function createSendFileTool(permissions, getCwd, sendFile) {
3118
3967
  return tool7({
3119
- description: "Send a file to the user. On Telegram the file is uploaded as an attachment. On CLI the file path and size are displayed. The path must be within an allowed read scope.",
3968
+ description: "Send a file to the user. On Telegram the file is uploaded as an attachment to the relevant approved recipients. On CLI the file path and size are displayed. The path must be within an allowed read scope.",
3120
3969
  parameters: z7.object({
3121
3970
  path: z7.string().describe("Absolute or relative path to the file to send")
3122
3971
  }),
@@ -3154,9 +4003,9 @@ import { tool as tool8 } from "ai";
3154
4003
  import { z as z8 } from "zod";
3155
4004
  function createSendMessageTool(sendMessage) {
3156
4005
  return tool8({
3157
- description: "Send a message to the paired user through the configured outbound channel. Currently this sends only to the paired Telegram owner. Use this only when the user explicitly asks you to send something to Telegram or asks for scheduled results to be sent there.",
4006
+ description: "Send a message through the configured outbound channel. For Telegram this sends to the approved Telegram recipients. Use this only when the user explicitly asks you to send something to Telegram or asks for scheduled results to be sent there.",
3158
4007
  parameters: z8.object({
3159
- content: z8.string().describe("The message content to send to the paired Telegram owner")
4008
+ content: z8.string().describe("The message content to send to the approved Telegram recipients")
3160
4009
  }),
3161
4010
  execute: async ({ content }) => {
3162
4011
  const trimmed = content.trim();
@@ -3165,7 +4014,7 @@ function createSendMessageTool(sendMessage) {
3165
4014
  }
3166
4015
  try {
3167
4016
  await sendMessage(trimmed);
3168
- return "Message sent to the paired Telegram owner.";
4017
+ return "Message sent to the approved Telegram recipients.";
3169
4018
  } catch (err) {
3170
4019
  return `Error sending message: ${err.message}`;
3171
4020
  }
@@ -4332,15 +5181,15 @@ Describe what this skill enables Mercury to do. When invoked via the use_skill t
4332
5181
  };
4333
5182
 
4334
5183
  // src/utils/manual.ts
4335
- import chalk3 from "chalk";
5184
+ import chalk4 from "chalk";
4336
5185
  function getManual() {
4337
5186
  const sections = [];
4338
5187
  sections.push("");
4339
- sections.push(chalk3.bold.cyan(" MERCURY \u2014 Capabilities & Commands"));
4340
- sections.push(chalk3.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5188
+ sections.push(chalk4.bold.cyan(" MERCURY \u2014 Capabilities & Commands"));
5189
+ sections.push(chalk4.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4341
5190
  sections.push("");
4342
- sections.push(chalk3.bold.white(" Built-in Tools"));
4343
- sections.push(chalk3.dim(" Tools Mercury can use during conversations."));
5191
+ sections.push(chalk4.bold.white(" Built-in Tools"));
5192
+ sections.push(chalk4.dim(" Tools Mercury can use during conversations."));
4344
5193
  sections.push("");
4345
5194
  const tools = [
4346
5195
  ["read_file", "Read file contents", "path (required)"],
@@ -4349,7 +5198,7 @@ function getManual() {
4349
5198
  ["edit_file", "Replace specific text in a file", "path, old_string, new_string"],
4350
5199
  ["list_dir", "List directory contents", "path"],
4351
5200
  ["delete_file", "Delete a file", "path"],
4352
- ["send_message", "Send a message to the paired Telegram owner", "content"],
5201
+ ["send_message", "Send a message to approved Telegram users", "content"],
4353
5202
  ["run_command", "Execute a shell command", "command"],
4354
5203
  ["approve_command", "Permanently approve a command type", 'command (e.g. "curl")'],
4355
5204
  ["fetch_url", "Fetch a URL and return content", "url, format? (text/markdown)"],
@@ -4368,12 +5217,12 @@ function getManual() {
4368
5217
  ["budget_status", "Check token budget", "\u2014"]
4369
5218
  ];
4370
5219
  for (const [name, desc, params] of tools) {
4371
- sections.push(` ${chalk3.cyan(name.padEnd(24))} ${desc}`);
4372
- sections.push(` ${" ".repeat(24)} ${chalk3.dim(params)}`);
5220
+ sections.push(` ${chalk4.cyan(name.padEnd(24))} ${desc}`);
5221
+ sections.push(` ${" ".repeat(24)} ${chalk4.dim(params)}`);
4373
5222
  }
4374
5223
  sections.push("");
4375
- sections.push(chalk3.bold.white(" CLI Commands"));
4376
- sections.push(chalk3.dim(" Run these from your terminal (no API calls consumed)."));
5224
+ sections.push(chalk4.bold.white(" CLI Commands"));
5225
+ sections.push(chalk4.dim(" Run these from your terminal (no API calls consumed)."));
4377
5226
  sections.push("");
4378
5227
  const commands = [
4379
5228
  ["mercury up", "Start persistently (install service + daemon)"],
@@ -4386,7 +5235,13 @@ function getManual() {
4386
5235
  ["mercury doctor", "Reconfigure settings (Enter keeps current)"],
4387
5236
  ["mercury setup", "Re-run the setup wizard"],
4388
5237
  ["mercury status", "Show config and daemon status"],
4389
- ["mercury telegram unpair", "Clear the paired Telegram owner"],
5238
+ ["mercury telegram list", "Show Telegram admins, members, and pending requests"],
5239
+ ["mercury telegram approve <code|id>", "Approve the first Telegram pairing code or a later Telegram request"],
5240
+ ["mercury telegram reject <id>", "Reject a pending Telegram request"],
5241
+ ["mercury telegram remove <id>", "Remove an approved Telegram user"],
5242
+ ["mercury telegram promote <id>", "Promote a Telegram member to admin"],
5243
+ ["mercury telegram demote <id>", "Demote a Telegram admin to member"],
5244
+ ["mercury telegram unpair", "Reset all Telegram access"],
4390
5245
  ["mercury help", "Show this manual"],
4391
5246
  ["mercury service install", "Install as system service (auto-start)"],
4392
5247
  ["mercury service uninstall", "Uninstall system service"],
@@ -4394,29 +5249,40 @@ function getManual() {
4394
5249
  ["mercury --verbose", "Start with debug logging on stderr"]
4395
5250
  ];
4396
5251
  for (const [cmd, desc] of commands) {
4397
- sections.push(` ${chalk3.white(cmd.padEnd(26))} ${desc}`);
5252
+ sections.push(` ${chalk4.white(cmd.padEnd(26))} ${desc}`);
4398
5253
  }
4399
5254
  sections.push("");
4400
- sections.push(chalk3.bold.white(" In-Chat Commands"));
4401
- sections.push(chalk3.dim(" Type these during a conversation (no API calls)."));
5255
+ sections.push(chalk4.bold.white(" In-Chat Commands"));
5256
+ sections.push(chalk4.dim(" Type these during a conversation (no API calls)."));
4402
5257
  sections.push("");
4403
5258
  const chat = [
4404
- ["/start", "Pair this Telegram account to Mercury"],
4405
- ["/pair", "Pair this Telegram account to Mercury"],
5259
+ ["/start", "Start Telegram pairing or request Telegram access"],
5260
+ ["/pair", "Start Telegram pairing or request Telegram access"],
5261
+ ["/", "Open the CLI command picker with arrow-key navigation"],
5262
+ ["/menu", "Open the CLI command picker with arrow-key navigation"],
4406
5263
  ["/help", "Show this manual"],
4407
5264
  ["/status", "Show config and budget info"],
5265
+ ["/telegram", "CLI chat only: open the Telegram management menu"],
5266
+ ["/telegram pending", "CLI chat only: list pending Telegram requests"],
5267
+ ["/telegram users", "CLI chat only: list approved Telegram users"],
5268
+ ["/telegram approve <code|id>", "CLI chat only: approve the first pairing code or a later request"],
5269
+ ["/telegram reject <id>", "CLI chat only: reject a pending Telegram request"],
5270
+ ["/telegram remove <id>", "CLI chat only: remove an approved Telegram user"],
5271
+ ["/telegram promote <id>", "CLI chat only: promote a Telegram member to admin"],
5272
+ ["/telegram demote <id>", "CLI chat only: demote a Telegram admin to member"],
5273
+ ["/telegram reset", "CLI chat only: reset all Telegram access"],
4408
5274
  ["/tools", "List currently loaded tools"],
4409
5275
  ["/skills", "List installed skills"],
4410
5276
  ["/stream", "Toggle text streaming on/off (Telegram)"],
4411
5277
  ["/stream on", "Enable streaming (live text updates)"],
4412
5278
  ["/stream off", "Disable streaming (single message)"],
4413
- ["/unpair", "Remove Telegram pairing for this Mercury instance"]
5279
+ ["/unpair", "Reset all Telegram access for this Mercury instance (admins only)"]
4414
5280
  ];
4415
5281
  for (const [cmd, desc] of chat) {
4416
- sections.push(` ${chalk3.white(cmd.padEnd(16))} ${desc}`);
5282
+ sections.push(` ${chalk4.white(cmd.padEnd(16))} ${desc}`);
4417
5283
  }
4418
5284
  sections.push("");
4419
- sections.push(chalk3.bold.white(" Permissions"));
5285
+ sections.push(chalk4.bold.white(" Permissions"));
4420
5286
  sections.push("");
4421
5287
  const perms = [
4422
5288
  "Commands are blocked (never run), auto-approved, or need approval.",
@@ -4425,10 +5291,10 @@ function getManual() {
4425
5291
  "File access is scoped \u2014 new paths need approval (y/n/always)."
4426
5292
  ];
4427
5293
  for (const p of perms) {
4428
- sections.push(` ${chalk3.dim("\u2022")} ${p}`);
5294
+ sections.push(` ${chalk4.dim("\u2022")} ${p}`);
4429
5295
  }
4430
5296
  sections.push("");
4431
- sections.push(chalk3.bold.white(" Skills"));
5297
+ sections.push(chalk4.bold.white(" Skills"));
4432
5298
  sections.push("");
4433
5299
  const skillInfo = [
4434
5300
  "Skills live in ~/.mercury/skills/<name>/SKILL.md",
@@ -4437,10 +5303,10 @@ function getManual() {
4437
5303
  'Schedule: "remind me daily at 9am to run daily-digest skill"'
4438
5304
  ];
4439
5305
  for (const s of skillInfo) {
4440
- sections.push(` ${chalk3.dim("\u2022")} ${s}`);
5306
+ sections.push(` ${chalk4.dim("\u2022")} ${s}`);
4441
5307
  }
4442
5308
  sections.push("");
4443
- sections.push(chalk3.bold.white(" Scheduling"));
5309
+ sections.push(chalk4.bold.white(" Scheduling"));
4444
5310
  sections.push("");
4445
5311
  const schedInfo = [
4446
5312
  'Recurring: "every day at 9am remind me to\u2026"',
@@ -4448,10 +5314,10 @@ function getManual() {
4448
5314
  "Tasks persist to ~/.mercury/schedules.yaml"
4449
5315
  ];
4450
5316
  for (const s of schedInfo) {
4451
- sections.push(` ${chalk3.dim("\u2022")} ${s}`);
5317
+ sections.push(` ${chalk4.dim("\u2022")} ${s}`);
4452
5318
  }
4453
5319
  sections.push("");
4454
- sections.push(chalk3.bold.white(" Configuration"));
5320
+ sections.push(chalk4.bold.white(" Configuration"));
4455
5321
  sections.push("");
4456
5322
  const configInfo = [
4457
5323
  ["~/.mercury/mercury.yaml", "Main config (providers, channels, budget)"],
@@ -4463,10 +5329,10 @@ function getManual() {
4463
5329
  ["~/.mercury/memory/", "Short-term, long-term, episodic memory"]
4464
5330
  ];
4465
5331
  for (const [path3, desc] of configInfo) {
4466
- sections.push(` ${chalk3.dim(path3.padEnd(36))} ${desc}`);
5332
+ sections.push(` ${chalk4.dim(path3.padEnd(36))} ${desc}`);
4467
5333
  }
4468
5334
  sections.push("");
4469
- sections.push(chalk3.dim(" mercury.cosmicstack.org"));
5335
+ sections.push(chalk4.dim(" mercury.cosmicstack.org"));
4470
5336
  sections.push("");
4471
5337
  return sections.join("\n");
4472
5338
  }
@@ -4476,7 +5342,7 @@ import { spawn } from "child_process";
4476
5342
  import { existsSync as existsSync16, readFileSync as readFileSync11, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, mkdirSync as mkdirSync9, openSync } from "fs";
4477
5343
  import { join as join9 } from "path";
4478
5344
  import process2 from "process";
4479
- import chalk4 from "chalk";
5345
+ import chalk5 from "chalk";
4480
5346
  var PID_FILE = "daemon.pid";
4481
5347
  var LOG_FILE = "daemon.log";
4482
5348
  function pidPath() {
@@ -4512,8 +5378,8 @@ function getDaemonStatus() {
4512
5378
  function startBackground() {
4513
5379
  const status = getDaemonStatus();
4514
5380
  if (status.running && status.pid) {
4515
- console.log(chalk4.yellow(` Mercury is already running (PID: ${status.pid})`));
4516
- console.log(chalk4.dim(` Use \`mercury stop\` to stop it first.`));
5381
+ console.log(chalk5.yellow(` Mercury is already running (PID: ${status.pid})`));
5382
+ console.log(chalk5.dim(` Use \`mercury stop\` to stop it first.`));
4517
5383
  console.log("");
4518
5384
  process2.exit(1);
4519
5385
  }
@@ -4539,21 +5405,21 @@ function startBackground() {
4539
5405
  child.unref();
4540
5406
  writeFileSync11(pidPath(), String(child.pid));
4541
5407
  console.log("");
4542
- console.log(chalk4.green(` Mercury started in background (PID: ${child.pid})`));
4543
- console.log(chalk4.dim(` Logs: ${logFile}`));
4544
- console.log(chalk4.dim(` Use \`mercury stop\` to stop.`));
4545
- console.log(chalk4.dim(` Use \`mercury logs\` to view logs.`));
5408
+ console.log(chalk5.green(` Mercury started in background (PID: ${child.pid})`));
5409
+ console.log(chalk5.dim(` Logs: ${logFile}`));
5410
+ console.log(chalk5.dim(` Use \`mercury stop\` to stop.`));
5411
+ console.log(chalk5.dim(` Use \`mercury logs\` to view logs.`));
4546
5412
  console.log("");
4547
5413
  }
4548
5414
  function stopDaemon() {
4549
5415
  const status = getDaemonStatus();
4550
5416
  if (!status.pid) {
4551
- console.log(chalk4.yellow(" Mercury is not running as a daemon."));
5417
+ console.log(chalk5.yellow(" Mercury is not running as a daemon."));
4552
5418
  console.log("");
4553
5419
  process2.exit(0);
4554
5420
  }
4555
5421
  if (!status.running) {
4556
- console.log(chalk4.yellow(` Stale PID file found (PID: ${status.pid} is not running). Cleaning up.`));
5422
+ console.log(chalk5.yellow(` Stale PID file found (PID: ${status.pid} is not running). Cleaning up.`));
4557
5423
  try {
4558
5424
  unlinkSync3(pidPath());
4559
5425
  } catch {
@@ -4567,9 +5433,9 @@ function stopDaemon() {
4567
5433
  } else {
4568
5434
  process2.kill(status.pid, "SIGTERM");
4569
5435
  }
4570
- console.log(chalk4.green(` Mercury stopped (PID: ${status.pid})`));
5436
+ console.log(chalk5.green(` Mercury stopped (PID: ${status.pid})`));
4571
5437
  } catch {
4572
- console.log(chalk4.red(` Failed to stop PID ${status.pid}. You may need to kill it manually.`));
5438
+ console.log(chalk5.red(` Failed to stop PID ${status.pid}. You may need to kill it manually.`));
4573
5439
  }
4574
5440
  try {
4575
5441
  unlinkSync3(pidPath());
@@ -4580,7 +5446,7 @@ function stopDaemon() {
4580
5446
  function restartDaemon() {
4581
5447
  const status = getDaemonStatus();
4582
5448
  if (status.running && status.pid) {
4583
- console.log(chalk4.yellow(` Stopping Mercury (PID: ${status.pid})...`));
5449
+ console.log(chalk5.yellow(` Stopping Mercury (PID: ${status.pid})...`));
4584
5450
  try {
4585
5451
  if (process2.platform === "win32") {
4586
5452
  process2.kill(status.pid);
@@ -4593,20 +5459,20 @@ function restartDaemon() {
4593
5459
  unlinkSync3(pidPath());
4594
5460
  } catch {
4595
5461
  }
4596
- console.log(chalk4.green(" Mercury stopped."));
5462
+ console.log(chalk5.green(" Mercury stopped."));
4597
5463
  } else if (status.pid) {
4598
5464
  try {
4599
5465
  unlinkSync3(pidPath());
4600
5466
  } catch {
4601
5467
  }
4602
5468
  }
4603
- console.log(chalk4.yellow(" Starting Mercury..."));
5469
+ console.log(chalk5.yellow(" Starting Mercury..."));
4604
5470
  startBackground();
4605
5471
  }
4606
5472
  function showLogs() {
4607
5473
  const logFile = logPath();
4608
5474
  if (!existsSync16(logFile)) {
4609
- console.log(chalk4.dim(" No daemon log file found."));
5475
+ console.log(chalk5.dim(" No daemon log file found."));
4610
5476
  console.log("");
4611
5477
  return;
4612
5478
  }
@@ -4654,7 +5520,7 @@ function tryAutoDaemonize() {
4654
5520
  import { existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4 } from "fs";
4655
5521
  import { join as join10 } from "path";
4656
5522
  import { homedir as homedir3 } from "os";
4657
- import chalk5 from "chalk";
5523
+ import chalk6 from "chalk";
4658
5524
  import { execSync as execSync8 } from "child_process";
4659
5525
  var SERVICE_DESC = "Mercury \u2014 Soul-Driven AI Agent";
4660
5526
  var WIN_TASK_NAME = "MercuryAgent";
@@ -4689,7 +5555,7 @@ function installService() {
4689
5555
  } else if (platform === "win32") {
4690
5556
  installWindows();
4691
5557
  } else {
4692
- console.log(chalk5.red(` Unsupported platform: ${platform}`));
5558
+ console.log(chalk6.red(` Unsupported platform: ${platform}`));
4693
5559
  process.exit(1);
4694
5560
  }
4695
5561
  }
@@ -4702,7 +5568,7 @@ function uninstallService() {
4702
5568
  } else if (platform === "win32") {
4703
5569
  uninstallWindows();
4704
5570
  } else {
4705
- console.log(chalk5.red(` Unsupported platform: ${platform}`));
5571
+ console.log(chalk6.red(` Unsupported platform: ${platform}`));
4706
5572
  process.exit(1);
4707
5573
  }
4708
5574
  }
@@ -4766,22 +5632,22 @@ function installMac() {
4766
5632
  try {
4767
5633
  execSync8(`launchctl load ${plistPath}`, { stdio: "inherit" });
4768
5634
  } catch {
4769
- console.log(chalk5.yellow(" launchctl load failed. Try running:"));
4770
- console.log(chalk5.dim(` launchctl load ${plistPath}`));
5635
+ console.log(chalk6.yellow(" launchctl load failed. Try running:"));
5636
+ console.log(chalk6.dim(` launchctl load ${plistPath}`));
4771
5637
  }
4772
5638
  console.log("");
4773
- console.log(chalk5.green(" Mercury service installed (macOS LaunchAgent)"));
4774
- console.log(chalk5.dim(` Plist: ${plistPath}`));
4775
- console.log(chalk5.dim(` Logs: ${logPath2}`));
4776
- console.log(chalk5.dim(" Auto-starts on login. Auto-restarts on crash."));
5639
+ console.log(chalk6.green(" Mercury service installed (macOS LaunchAgent)"));
5640
+ console.log(chalk6.dim(` Plist: ${plistPath}`));
5641
+ console.log(chalk6.dim(` Logs: ${logPath2}`));
5642
+ console.log(chalk6.dim(" Auto-starts on login. Auto-restarts on crash."));
4777
5643
  console.log("");
4778
- console.log(chalk5.dim(" Uninstall: mercury service uninstall"));
5644
+ console.log(chalk6.dim(" Uninstall: mercury service uninstall"));
4779
5645
  console.log("");
4780
5646
  }
4781
5647
  function uninstallMac() {
4782
5648
  const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
4783
5649
  if (!existsSync17(plistPath)) {
4784
- console.log(chalk5.yellow(" Mercury service is not installed."));
5650
+ console.log(chalk6.yellow(" Mercury service is not installed."));
4785
5651
  console.log("");
4786
5652
  process.exit(0);
4787
5653
  }
@@ -4792,28 +5658,28 @@ function uninstallMac() {
4792
5658
  try {
4793
5659
  unlinkSync4(plistPath);
4794
5660
  } catch {
4795
- console.log(chalk5.yellow(" Failed to remove plist file. Remove manually:"));
4796
- console.log(chalk5.dim(` rm ${plistPath}`));
5661
+ console.log(chalk6.yellow(" Failed to remove plist file. Remove manually:"));
5662
+ console.log(chalk6.dim(` rm ${plistPath}`));
4797
5663
  }
4798
5664
  console.log("");
4799
- console.log(chalk5.green(" Mercury service uninstalled"));
5665
+ console.log(chalk6.green(" Mercury service uninstalled"));
4800
5666
  console.log("");
4801
5667
  }
4802
5668
  function showMacStatus() {
4803
5669
  const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
4804
5670
  if (!existsSync17(plistPath)) {
4805
- console.log(chalk5.yellow(" Mercury service is not installed."));
4806
- console.log(chalk5.dim(" Run `mercury service install` to set it up."));
5671
+ console.log(chalk6.yellow(" Mercury service is not installed."));
5672
+ console.log(chalk6.dim(" Run `mercury service install` to set it up."));
4807
5673
  console.log("");
4808
5674
  return;
4809
5675
  }
4810
5676
  try {
4811
5677
  const output = execSync8("launchctl list | grep com.cosmicstack.mercury", { encoding: "utf-8" }).trim();
4812
- console.log(` ${chalk5.green("Service installed and loaded")}`);
4813
- console.log(chalk5.dim(` ${output}`));
5678
+ console.log(` ${chalk6.green("Service installed and loaded")}`);
5679
+ console.log(chalk6.dim(` ${output}`));
4814
5680
  } catch {
4815
- console.log(` ${chalk5.yellow("Service installed but not loaded")}`);
4816
- console.log(chalk5.dim(` Plist: ${plistPath}`));
5681
+ console.log(` ${chalk6.yellow("Service installed but not loaded")}`);
5682
+ console.log(chalk6.dim(` Plist: ${plistPath}`));
4817
5683
  }
4818
5684
  console.log("");
4819
5685
  }
@@ -4849,30 +5715,30 @@ WantedBy=default.target`;
4849
5715
  execSync8("systemctl --user enable mercury.service", { stdio: "inherit" });
4850
5716
  execSync8("systemctl --user start mercury.service", { stdio: "inherit" });
4851
5717
  } catch (err) {
4852
- console.log(chalk5.yellow(" systemd commands failed. Try running manually:"));
4853
- console.log(chalk5.dim(" systemctl --user daemon-reload"));
4854
- console.log(chalk5.dim(" systemctl --user enable mercury.service"));
4855
- console.log(chalk5.dim(" systemctl --user start mercury.service"));
5718
+ console.log(chalk6.yellow(" systemd commands failed. Try running manually:"));
5719
+ console.log(chalk6.dim(" systemctl --user daemon-reload"));
5720
+ console.log(chalk6.dim(" systemctl --user enable mercury.service"));
5721
+ console.log(chalk6.dim(" systemctl --user start mercury.service"));
4856
5722
  }
4857
5723
  try {
4858
5724
  execSync8(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
4859
5725
  } catch {
4860
- console.log(chalk5.yellow(" Enable linger failed (needed for boot-without-login). Try:"));
4861
- console.log(chalk5.dim(` sudo loginctl enable-linger ${process.env.USER || "$USER"}`));
5726
+ console.log(chalk6.yellow(" Enable linger failed (needed for boot-without-login). Try:"));
5727
+ console.log(chalk6.dim(` sudo loginctl enable-linger ${process.env.USER || "$USER"}`));
4862
5728
  }
4863
5729
  console.log("");
4864
- console.log(chalk5.green(" Mercury service installed (systemd --user)"));
4865
- console.log(chalk5.dim(` Service: ${servicePath}`));
4866
- console.log(chalk5.dim(` Logs: ${join10(home, "daemon.log")}`));
4867
- console.log(chalk5.dim(" Auto-starts on login. Auto-restarts on crash (5s delay)."));
5730
+ console.log(chalk6.green(" Mercury service installed (systemd --user)"));
5731
+ console.log(chalk6.dim(` Service: ${servicePath}`));
5732
+ console.log(chalk6.dim(` Logs: ${join10(home, "daemon.log")}`));
5733
+ console.log(chalk6.dim(" Auto-starts on login. Auto-restarts on crash (5s delay)."));
4868
5734
  console.log("");
4869
- console.log(chalk5.dim(" Uninstall: mercury service uninstall"));
5735
+ console.log(chalk6.dim(" Uninstall: mercury service uninstall"));
4870
5736
  console.log("");
4871
5737
  }
4872
5738
  function uninstallLinux() {
4873
5739
  const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
4874
5740
  if (!existsSync17(servicePath)) {
4875
- console.log(chalk5.yellow(" Mercury service is not installed."));
5741
+ console.log(chalk6.yellow(" Mercury service is not installed."));
4876
5742
  console.log("");
4877
5743
  process.exit(0);
4878
5744
  }
@@ -4884,22 +5750,22 @@ function uninstallLinux() {
4884
5750
  try {
4885
5751
  unlinkSync4(servicePath);
4886
5752
  } catch {
4887
- console.log(chalk5.yellow(" Failed to remove service file. Remove manually:"));
4888
- console.log(chalk5.dim(` rm ${servicePath}`));
5753
+ console.log(chalk6.yellow(" Failed to remove service file. Remove manually:"));
5754
+ console.log(chalk6.dim(` rm ${servicePath}`));
4889
5755
  }
4890
5756
  try {
4891
5757
  execSync8("systemctl --user daemon-reload", { stdio: "inherit" });
4892
5758
  } catch {
4893
5759
  }
4894
5760
  console.log("");
4895
- console.log(chalk5.green(" Mercury service uninstalled"));
5761
+ console.log(chalk6.green(" Mercury service uninstalled"));
4896
5762
  console.log("");
4897
5763
  }
4898
5764
  function showLinuxStatus() {
4899
5765
  const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
4900
5766
  if (!existsSync17(servicePath)) {
4901
- console.log(chalk5.yellow(" Mercury service is not installed."));
4902
- console.log(chalk5.dim(" Run `mercury service install` to set it up."));
5767
+ console.log(chalk6.yellow(" Mercury service is not installed."));
5768
+ console.log(chalk6.dim(" Run `mercury service install` to set it up."));
4903
5769
  console.log("");
4904
5770
  return;
4905
5771
  }
@@ -4907,8 +5773,8 @@ function showLinuxStatus() {
4907
5773
  const output = execSync8("systemctl --user status mercury.service", { encoding: "utf-8" }).trim();
4908
5774
  console.log(output);
4909
5775
  } catch (err) {
4910
- console.log(chalk5.yellow(" Could not get service status:"));
4911
- console.log(chalk5.dim(` ${err.message || err}`));
5776
+ console.log(chalk6.yellow(" Could not get service status:"));
5777
+ console.log(chalk6.dim(` ${err.message || err}`));
4912
5778
  }
4913
5779
  console.log("");
4914
5780
  }
@@ -4924,33 +5790,33 @@ function installWindows() {
4924
5790
  { stdio: "inherit", shell: "cmd.exe" }
4925
5791
  );
4926
5792
  } catch {
4927
- console.log(chalk5.yellow(" schtasks create failed. Try running from an Administrator cmd:"));
4928
- console.log(chalk5.dim(` schtasks /create /tn "${WIN_TASK_NAME}" /tr "${cmd}" /sc onlogon /rl limited /f`));
5793
+ console.log(chalk6.yellow(" schtasks create failed. Try running from an Administrator cmd:"));
5794
+ console.log(chalk6.dim(` schtasks /create /tn "${WIN_TASK_NAME}" /tr "${cmd}" /sc onlogon /rl limited /f`));
4929
5795
  }
4930
5796
  try {
4931
5797
  execSync8(`schtasks /run /tn "${WIN_TASK_NAME}"`, { stdio: "inherit", shell: "cmd.exe" });
4932
5798
  } catch {
4933
- console.log(chalk5.yellow(" Task created but failed to start immediately. It will start on next login."));
5799
+ console.log(chalk6.yellow(" Task created but failed to start immediately. It will start on next login."));
4934
5800
  }
4935
5801
  console.log("");
4936
- console.log(chalk5.green(" Mercury service installed (Windows Task Scheduler)"));
4937
- console.log(chalk5.dim(` Task: ${WIN_TASK_NAME}`));
4938
- console.log(chalk5.dim(` Trigger: on logon`));
4939
- console.log(chalk5.dim(` Logs: ${logPath2}`));
4940
- console.log(chalk5.dim(" Auto-starts on login. Use --daemon flag for crash recovery."));
5802
+ console.log(chalk6.green(" Mercury service installed (Windows Task Scheduler)"));
5803
+ console.log(chalk6.dim(` Task: ${WIN_TASK_NAME}`));
5804
+ console.log(chalk6.dim(` Trigger: on logon`));
5805
+ console.log(chalk6.dim(` Logs: ${logPath2}`));
5806
+ console.log(chalk6.dim(" Auto-starts on login. Use --daemon flag for crash recovery."));
4941
5807
  console.log("");
4942
- console.log(chalk5.dim(" Uninstall: mercury service uninstall"));
5808
+ console.log(chalk6.dim(" Uninstall: mercury service uninstall"));
4943
5809
  console.log("");
4944
5810
  }
4945
5811
  function uninstallWindows() {
4946
5812
  try {
4947
5813
  execSync8(`schtasks /delete /tn "${WIN_TASK_NAME}" /f`, { stdio: "inherit", shell: "cmd.exe" });
4948
5814
  console.log("");
4949
- console.log(chalk5.green(" Mercury service uninstalled"));
5815
+ console.log(chalk6.green(" Mercury service uninstalled"));
4950
5816
  console.log("");
4951
5817
  } catch {
4952
- console.log(chalk5.yellow(" Task not found or failed to delete. Remove manually:"));
4953
- console.log(chalk5.dim(` schtasks /delete /tn "${WIN_TASK_NAME}" /f`));
5818
+ console.log(chalk6.yellow(" Task not found or failed to delete. Remove manually:"));
5819
+ console.log(chalk6.dim(` schtasks /delete /tn "${WIN_TASK_NAME}" /f`));
4954
5820
  console.log("");
4955
5821
  }
4956
5822
  }
@@ -4963,8 +5829,8 @@ function showWindowsStatus() {
4963
5829
  console.log(output);
4964
5830
  console.log("");
4965
5831
  } catch {
4966
- console.log(chalk5.yellow(" Mercury service is not installed."));
4967
- console.log(chalk5.dim(" Run `mercury service install` to set it up."));
5832
+ console.log(chalk6.yellow(" Mercury service is not installed."));
5833
+ console.log(chalk6.dim(" Run `mercury service install` to set it up."));
4968
5834
  console.log("");
4969
5835
  }
4970
5836
  }
@@ -4999,11 +5865,208 @@ function sleep(ms) {
4999
5865
  return new Promise((resolve13) => setTimeout(resolve13, ms));
5000
5866
  }
5001
5867
 
5868
+ // src/utils/provider-models.ts
5869
+ var MAX_MODEL_OPTIONS = 7;
5870
+ var OPENAI_PREFERRED_MODELS = [
5871
+ "gpt-5.2",
5872
+ "gpt-5.2-chat-latest",
5873
+ "gpt-5.2-pro",
5874
+ "gpt-5-mini",
5875
+ "gpt-5-nano",
5876
+ "gpt-4.1",
5877
+ "gpt-4.1-mini",
5878
+ "gpt-oss-120b",
5879
+ "gpt-oss-20b"
5880
+ ];
5881
+ var ANTHROPIC_PREFERRED_MODELS = [
5882
+ "claude-sonnet-4-20250514",
5883
+ "claude-opus-4-20250514",
5884
+ "claude-3-7-sonnet-latest",
5885
+ "claude-3-5-sonnet-latest",
5886
+ "claude-3-5-haiku-latest"
5887
+ ];
5888
+ var DEEPSEEK_PREFERRED_MODELS = [
5889
+ "deepseek-chat",
5890
+ "deepseek-reasoner"
5891
+ ];
5892
+ var GROK_PREFERRED_MODELS = [
5893
+ "grok-4",
5894
+ "grok-4-latest",
5895
+ "grok-4.20",
5896
+ "grok-3",
5897
+ "grok-3-latest"
5898
+ ];
5899
+ var OLLAMA_CLOUD_PREFERRED_MODELS = [
5900
+ "gpt-oss:120b",
5901
+ "gpt-oss:120b-cloud",
5902
+ "gpt-oss:20b"
5903
+ ];
5904
+ var OLLAMA_LOCAL_PREFERRED_MODELS = [
5905
+ "gpt-oss:20b",
5906
+ "gpt-oss:120b"
5907
+ ];
5908
+ var ProviderModelFetchError = class extends Error {
5909
+ constructor(message) {
5910
+ super(message);
5911
+ this.name = "ProviderModelFetchError";
5912
+ }
5913
+ };
5914
+ function trimTrailingSlash(value) {
5915
+ return value.replace(/\/+$/, "");
5916
+ }
5917
+ async function fetchJson(url, init, invalidMessage) {
5918
+ let response;
5919
+ try {
5920
+ response = await fetch(url, {
5921
+ ...init,
5922
+ signal: AbortSignal.timeout(1e4)
5923
+ });
5924
+ } catch {
5925
+ throw new ProviderModelFetchError(invalidMessage);
5926
+ }
5927
+ if (!response.ok) {
5928
+ throw new ProviderModelFetchError(invalidMessage);
5929
+ }
5930
+ try {
5931
+ return await response.json();
5932
+ } catch {
5933
+ throw new ProviderModelFetchError("Mercury could not read the model list returned by this provider.");
5934
+ }
5935
+ }
5936
+ function uniq(values) {
5937
+ return [...new Set(values.filter(Boolean))];
5938
+ }
5939
+ function prioritizeModels(models, preferred) {
5940
+ const preferredSet = new Set(preferred);
5941
+ const preferredMatches = preferred.filter((model) => models.includes(model));
5942
+ const others = models.filter((model) => !preferredSet.has(model)).sort((a, b) => a.localeCompare(b));
5943
+ return [...preferredMatches, ...others];
5944
+ }
5945
+ function limitModels(models) {
5946
+ return models.slice(0, MAX_MODEL_OPTIONS);
5947
+ }
5948
+ function isOpenAIChatModel(id) {
5949
+ const lower = id.toLowerCase();
5950
+ if (lower.includes("image") || lower.includes("audio") || lower.includes("tts") || lower.includes("transcribe") || lower.includes("embedding") || lower.includes("moderation") || lower.includes("realtime") || lower.includes("whisper") || lower.includes("search") || lower.includes("computer")) {
5951
+ return false;
5952
+ }
5953
+ return lower.startsWith("gpt-") || /^o\d/.test(lower);
5954
+ }
5955
+ function chooseRecommendedModel(provider, models, currentModel) {
5956
+ const preferredByProvider = {
5957
+ deepseek: DEEPSEEK_PREFERRED_MODELS,
5958
+ openai: OPENAI_PREFERRED_MODELS,
5959
+ anthropic: ANTHROPIC_PREFERRED_MODELS,
5960
+ grok: GROK_PREFERRED_MODELS,
5961
+ ollamaCloud: OLLAMA_CLOUD_PREFERRED_MODELS,
5962
+ ollamaLocal: OLLAMA_LOCAL_PREFERRED_MODELS
5963
+ };
5964
+ for (const candidate of preferredByProvider[provider]) {
5965
+ if (models.includes(candidate)) {
5966
+ return candidate;
5967
+ }
5968
+ }
5969
+ if (currentModel && models.includes(currentModel)) {
5970
+ return currentModel;
5971
+ }
5972
+ return models[0];
5973
+ }
5974
+ function buildModelCatalog(provider, models, currentModel) {
5975
+ const filtered = uniq(models);
5976
+ if (filtered.length === 0) {
5977
+ throw new ProviderModelFetchError("Mercury could not find any supported chat models for this provider.");
5978
+ }
5979
+ const recommendedModel = chooseRecommendedModel(provider, filtered, currentModel);
5980
+ const preferredByProvider = {
5981
+ deepseek: DEEPSEEK_PREFERRED_MODELS,
5982
+ openai: OPENAI_PREFERRED_MODELS,
5983
+ anthropic: ANTHROPIC_PREFERRED_MODELS,
5984
+ grok: GROK_PREFERRED_MODELS,
5985
+ ollamaCloud: OLLAMA_CLOUD_PREFERRED_MODELS,
5986
+ ollamaLocal: OLLAMA_LOCAL_PREFERRED_MODELS
5987
+ };
5988
+ const withoutRecommended = filtered.filter((model) => model !== recommendedModel);
5989
+ const prioritized = prioritizeModels(withoutRecommended, preferredByProvider[provider]);
5990
+ return {
5991
+ recommendedModel,
5992
+ models: limitModels(prioritized)
5993
+ };
5994
+ }
5995
+ async function fetchOpenAICompatModels(provider, config) {
5996
+ const data = await fetchJson(
5997
+ `${trimTrailingSlash(config.baseUrl)}/models`,
5998
+ {
5999
+ headers: {
6000
+ Authorization: `Bearer ${config.apiKey}`
6001
+ }
6002
+ },
6003
+ `Mercury could not fetch models for this ${provider === "grok" ? "Grok" : provider === "deepseek" ? "DeepSeek" : "OpenAI"} key. Please re-enter it.`
6004
+ );
6005
+ const ids = (data.data ?? []).map((model) => model.id?.trim() ?? "").filter((id) => {
6006
+ if (provider === "deepseek") {
6007
+ return id.startsWith("deepseek-");
6008
+ }
6009
+ return isOpenAIChatModel(id);
6010
+ });
6011
+ return buildModelCatalog(provider, ids, config.model);
6012
+ }
6013
+ async function fetchAnthropicModels(config) {
6014
+ const data = await fetchJson(
6015
+ "https://api.anthropic.com/v1/models",
6016
+ {
6017
+ headers: {
6018
+ "x-api-key": config.apiKey,
6019
+ "anthropic-version": "2023-06-01"
6020
+ }
6021
+ },
6022
+ "Mercury could not fetch models for this Anthropic key. Please re-enter it."
6023
+ );
6024
+ const ids = (data.data ?? []).map((model) => model.id?.trim() ?? "").filter((id) => id.startsWith("claude-"));
6025
+ return buildModelCatalog("anthropic", ids, config.model);
6026
+ }
6027
+ async function fetchGrokModels(config) {
6028
+ const data = await fetchJson(
6029
+ `${trimTrailingSlash(config.baseUrl)}/language-models`,
6030
+ {
6031
+ headers: {
6032
+ Authorization: `Bearer ${config.apiKey}`
6033
+ }
6034
+ },
6035
+ "Mercury could not fetch models for this Grok key. Please re-enter it."
6036
+ );
6037
+ const ids = (data.data ?? []).filter((model) => model.output_modalities?.includes("text") || model.output_modalities == null).map((model) => model.id?.trim() ?? "").filter((id) => id.startsWith("grok-"));
6038
+ return buildModelCatalog("grok", ids, config.model);
6039
+ }
6040
+ async function fetchOllamaModels(provider, config) {
6041
+ const headers = config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : void 0;
6042
+ const data = await fetchJson(
6043
+ `${trimTrailingSlash(config.baseUrl)}/tags`,
6044
+ {
6045
+ headers
6046
+ },
6047
+ provider === "ollamaCloud" ? "Mercury could not fetch models for this Ollama Cloud key. Please re-enter it." : "Mercury could not fetch models from this Ollama Local server. Please check the base URL and try again."
6048
+ );
6049
+ const ids = (data.models ?? []).map((model) => model.model?.trim() || model.name?.trim() || "").filter(Boolean);
6050
+ return buildModelCatalog(provider, ids, config.model);
6051
+ }
6052
+ async function fetchProviderModelCatalog(provider, config) {
6053
+ if (provider === "anthropic") {
6054
+ return fetchAnthropicModels(config);
6055
+ }
6056
+ if (provider === "grok") {
6057
+ return fetchGrokModels(config);
6058
+ }
6059
+ if (provider === "ollamaCloud" || provider === "ollamaLocal") {
6060
+ return fetchOllamaModels(provider, config);
6061
+ }
6062
+ return fetchOpenAICompatModels(provider, config);
6063
+ }
6064
+
5002
6065
  // src/index.ts
5003
6066
  var __dirname = dirname3(fileURLToPath(import.meta.url));
5004
6067
  var pkgVersion = JSON.parse(readFileSync12(join11(__dirname, "..", "package.json"), "utf8")).version;
5005
6068
  function hr() {
5006
- console.log(chalk6.dim("\u2500".repeat(50)));
6069
+ console.log(chalk7.dim("\u2500".repeat(50)));
5007
6070
  }
5008
6071
  var MERCURY_ASCII = [
5009
6072
  " __ _____________ ________ ________ __",
@@ -5015,26 +6078,26 @@ var MERCURY_ASCII = [
5015
6078
  function banner() {
5016
6079
  console.log("");
5017
6080
  for (const line of MERCURY_ASCII) {
5018
- console.log(chalk6.bold.cyan(` ${line}`));
6081
+ console.log(chalk7.bold.cyan(` ${line}`));
5019
6082
  }
5020
6083
  console.log("");
5021
- console.log(chalk6.white(" an AI agent for personal tasks"));
5022
- console.log(chalk6.dim(` v${pkgVersion} \xB7 by Cosmic Stack \xB7 mercury.cosmicstack.org`));
6084
+ console.log(chalk7.white(" an AI agent for personal tasks"));
6085
+ console.log(chalk7.dim(` v${pkgVersion} \xB7 by Cosmic Stack \xB7 mercury.cosmicstack.org`));
5023
6086
  console.log("");
5024
6087
  }
5025
6088
  function splashScreen() {
5026
6089
  console.log("");
5027
6090
  for (const line of MERCURY_ASCII) {
5028
- console.log(chalk6.bold.cyan(` ${line}`));
6091
+ console.log(chalk7.bold.cyan(` ${line}`));
5029
6092
  }
5030
6093
  console.log("");
5031
- console.log(chalk6.dim(" an AI agent for personal tasks"));
5032
- console.log(chalk6.cyan(" by Cosmic Stack"));
5033
- console.log(chalk6.dim(" mercury.cosmicstack.org"));
6094
+ console.log(chalk7.dim(" an AI agent for personal tasks"));
6095
+ console.log(chalk7.cyan(" by Cosmic Stack"));
6096
+ console.log(chalk7.dim(" mercury.cosmicstack.org"));
5034
6097
  console.log("");
5035
6098
  }
5036
6099
  async function ask(prompt) {
5037
- const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
6100
+ const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
5038
6101
  return new Promise((resolve13) => {
5039
6102
  rl.question(prompt, (answer) => {
5040
6103
  rl.close();
@@ -5083,14 +6146,14 @@ async function chooseProvidersToConfigure(config, isReconfig) {
5083
6146
  for (let i = 0; i < PROVIDER_OPTIONS.length; i++) {
5084
6147
  const option = PROVIDER_OPTIONS[i];
5085
6148
  const status = configured.includes(option.key) ? " (configured)" : "";
5086
- console.log(chalk6.white(` ${i + 1}. ${option.label}${status}`));
6149
+ console.log(chalk7.white(` ${i + 1}. ${option.label}${status}`));
5087
6150
  }
5088
6151
  console.log("");
5089
- const prompt = isReconfig ? chalk6.white(" Choose providers to configure [comma-separated, Enter keeps current]: ") : chalk6.white(" Choose providers to configure [comma-separated, Enter for DeepSeek]: ");
6152
+ const prompt = isReconfig ? chalk7.white(" Choose providers to configure [comma-separated, Enter keeps current]: ") : chalk7.white(" Choose providers to configure [comma-separated, Enter for DeepSeek]: ");
5090
6153
  const input = await ask(prompt);
5091
6154
  const parsed = parseProviderSelection(input);
5092
6155
  if (parsed === null) {
5093
- console.log(chalk6.red(" Please choose valid provider numbers, like `1` or `1,3,5`."));
6156
+ console.log(chalk7.red(" Please choose valid provider numbers, like `1` or `1,3,5`."));
5094
6157
  console.log("");
5095
6158
  continue;
5096
6159
  }
@@ -5106,23 +6169,23 @@ async function chooseDefaultProvider(config) {
5106
6169
  }
5107
6170
  if (configured.length === 1) {
5108
6171
  config.providers.default = configured[0];
5109
- console.log(chalk6.dim(` Default provider set to ${getProviderLabel(configured[0])}`));
6172
+ console.log(chalk7.dim(` Default provider set to ${getProviderLabel(configured[0])}`));
5110
6173
  return;
5111
6174
  }
5112
6175
  const suggested = configured.includes("deepseek") ? "deepseek" : configured[0];
5113
6176
  console.log("");
5114
- console.log(chalk6.bold.white(" Default Provider"));
5115
- console.log(chalk6.dim(" Select the LLM provider Mercury should use first."));
6177
+ console.log(chalk7.bold.white(" Default Provider"));
6178
+ console.log(chalk7.dim(" Select the LLM provider Mercury should use first."));
5116
6179
  console.log("");
5117
6180
  for (let i = 0; i < configured.length; i++) {
5118
6181
  const provider = configured[i];
5119
6182
  const recommended = provider === suggested ? " (recommended)" : "";
5120
6183
  const current = provider === config.providers.default ? " (current)" : "";
5121
- console.log(chalk6.white(` ${i + 1}. ${getProviderLabel(provider)}${recommended}${current}`));
6184
+ console.log(chalk7.white(` ${i + 1}. ${getProviderLabel(provider)}${recommended}${current}`));
5122
6185
  }
5123
6186
  console.log("");
5124
6187
  while (true) {
5125
- const choice = await ask(chalk6.white(` Choose [1-${configured.length}] [Enter for ${getProviderLabel(suggested)}]: `));
6188
+ const choice = await ask(chalk7.white(` Choose [1-${configured.length}] [Enter for ${getProviderLabel(suggested)}]: `));
5126
6189
  if (!choice) {
5127
6190
  config.providers.default = suggested;
5128
6191
  return;
@@ -5132,7 +6195,7 @@ async function chooseDefaultProvider(config) {
5132
6195
  config.providers.default = configured[num - 1];
5133
6196
  return;
5134
6197
  }
5135
- console.log(chalk6.red(" Please choose a valid number from the list above."));
6198
+ console.log(chalk7.red(" Please choose a valid number from the list above."));
5136
6199
  }
5137
6200
  }
5138
6201
  function looksLikeToken(value, minLength = 20) {
@@ -5172,18 +6235,117 @@ function validateModelName(value) {
5172
6235
  if (/\s/.test(value)) return "Model name cannot contain spaces.";
5173
6236
  return null;
5174
6237
  }
6238
+ async function chooseProviderModel(providerLabel, recommendedModel, models) {
6239
+ const selection = await selectWithArrowKeys(
6240
+ `${providerLabel} Models`,
6241
+ [
6242
+ {
6243
+ value: "__default__",
6244
+ label: `Use provider default (${recommendedModel})`
6245
+ },
6246
+ ...models.map((model) => ({
6247
+ value: model,
6248
+ label: model
6249
+ })),
6250
+ {
6251
+ value: "__custom__",
6252
+ label: "Enter a custom model name"
6253
+ }
6254
+ ]
6255
+ );
6256
+ if (!selection || selection === "__default__") {
6257
+ return recommendedModel;
6258
+ }
6259
+ if (selection !== "__custom__") {
6260
+ return selection;
6261
+ }
6262
+ while (true) {
6263
+ const customModel = await ask(chalk7.white(` ${providerLabel} model [Enter or "none" for ${recommendedModel}]: `));
6264
+ if (!customModel || customModel.toLowerCase() === "none") {
6265
+ return recommendedModel;
6266
+ }
6267
+ const error = validateModelName(customModel);
6268
+ if (!error) {
6269
+ return customModel;
6270
+ }
6271
+ console.log(chalk7.red(` ${error}`));
6272
+ }
6273
+ }
6274
+ async function promptApiKeyWithModelSelection(config, provider, providerLabel, prompt, isReconfig) {
6275
+ const existingConfig = config.providers[provider];
6276
+ while (true) {
6277
+ const value = await ask(prompt);
6278
+ if (!value) {
6279
+ if (isReconfig && existingConfig.apiKey) {
6280
+ return {
6281
+ apiKey: existingConfig.apiKey,
6282
+ model: existingConfig.model,
6283
+ skipped: true
6284
+ };
6285
+ }
6286
+ return { skipped: true };
6287
+ }
6288
+ const formatError = validateApiKey(provider, value);
6289
+ if (formatError) {
6290
+ console.log(chalk7.red(` ${formatError}`));
6291
+ continue;
6292
+ }
6293
+ console.log(chalk7.dim(` Validating ${providerLabel} and fetching models...`));
6294
+ try {
6295
+ const catalog = await fetchProviderModelCatalog(provider, {
6296
+ ...existingConfig,
6297
+ apiKey: value
6298
+ });
6299
+ const model = await chooseProviderModel(
6300
+ providerLabel,
6301
+ catalog.recommendedModel,
6302
+ catalog.models
6303
+ );
6304
+ return { apiKey: value, model, skipped: false };
6305
+ } catch (error) {
6306
+ const message = error instanceof ProviderModelFetchError ? error.message : `Mercury could not fetch models for ${providerLabel}. Please re-enter the key.`;
6307
+ console.log(chalk7.red(` ${message}`));
6308
+ }
6309
+ }
6310
+ }
6311
+ async function promptOllamaLocalModelSelection(config) {
6312
+ const existingConfig = config.providers.ollamaLocal;
6313
+ while (true) {
6314
+ const baseUrl = await promptValidatedValue(
6315
+ chalk7.white(` Ollama Local base URL [${existingConfig.baseUrl}]: `),
6316
+ validateBaseUrl,
6317
+ existingConfig.baseUrl
6318
+ );
6319
+ console.log(chalk7.dim(" Fetching Ollama Local models..."));
6320
+ try {
6321
+ const catalog = await fetchProviderModelCatalog("ollamaLocal", {
6322
+ ...existingConfig,
6323
+ baseUrl
6324
+ });
6325
+ const model = await chooseProviderModel(
6326
+ "Ollama Local",
6327
+ catalog.recommendedModel,
6328
+ catalog.models
6329
+ );
6330
+ return { baseUrl, model, skipped: false };
6331
+ } catch (error) {
6332
+ const message = error instanceof ProviderModelFetchError ? error.message : "Mercury could not fetch Ollama Local models. Please check the base URL and try again.";
6333
+ console.log(chalk7.red(` ${message}`));
6334
+ }
6335
+ }
6336
+ }
5175
6337
  async function promptValidatedValue(prompt, validator, existingValue, options) {
5176
6338
  while (true) {
5177
6339
  const value = await ask(prompt);
5178
6340
  if (!value) {
5179
6341
  if (existingValue) return existingValue;
5180
6342
  if (options?.allowSkip) return void 0;
5181
- console.log(chalk6.red(" A value is required here."));
6343
+ console.log(chalk7.red(" A value is required here."));
5182
6344
  continue;
5183
6345
  }
5184
6346
  const error = validator(value);
5185
6347
  if (!error) return value;
5186
- console.log(chalk6.red(` ${error}`));
6348
+ console.log(chalk7.red(` ${error}`));
5187
6349
  }
5188
6350
  }
5189
6351
  function appendToEnv(key, value) {
@@ -5205,43 +6367,103 @@ function parseGithubRepo(input) {
5205
6367
  if (shortMatch) return { owner: shortMatch[1], repo: shortMatch[2] };
5206
6368
  return null;
5207
6369
  }
6370
+ function formatTelegramUser(user) {
6371
+ const username = user.username ? ` (@${user.username})` : "";
6372
+ const firstName = user.firstName ? ` ${user.firstName}` : "";
6373
+ return `${user.userId}${username}${firstName}`;
6374
+ }
6375
+ function printTelegramAccessState(config) {
6376
+ const admins = config.channels.telegram.admins;
6377
+ const members = config.channels.telegram.members;
6378
+ const pending = config.channels.telegram.pending;
6379
+ const pendingSummary = pending.length > 0 ? pending.map((entry) => {
6380
+ const code = entry.pairingCode ? ` [code: ${entry.pairingCode}]` : "";
6381
+ return `${formatTelegramUser(entry)}${code}`;
6382
+ }).join(", ") : "";
6383
+ console.log("");
6384
+ console.log(` Telegram Access: ${chalk7.white(getTelegramAccessSummary(config))}`);
6385
+ console.log(` Admins: ${admins.length > 0 ? chalk7.green(admins.map(formatTelegramUser).join(", ")) : chalk7.dim("none")}`);
6386
+ console.log(` Members: ${members.length > 0 ? chalk7.green(members.map(formatTelegramUser).join(", ")) : chalk7.dim("none")}`);
6387
+ console.log(` Pending: ${pending.length > 0 ? chalk7.yellow(pendingSummary) : chalk7.dim("none")}`);
6388
+ }
6389
+ function restartDaemonIfRunning(message) {
6390
+ const daemon = getDaemonStatus();
6391
+ if (!daemon.running) return;
6392
+ if (message) {
6393
+ console.log(chalk7.dim(` ${message}`));
6394
+ }
6395
+ restartDaemon();
6396
+ }
6397
+ async function completeInitialTelegramPairing(config) {
6398
+ if (!config.channels.telegram.enabled || !config.channels.telegram.botToken || hasTelegramAdmins(config)) {
6399
+ return;
6400
+ }
6401
+ console.log("");
6402
+ console.log(chalk7.bold.white(" Telegram Pairing"));
6403
+ console.log(chalk7.dim(" 1. Open Telegram and message your bot."));
6404
+ console.log(chalk7.dim(" 2. Send /start to receive your pairing code in Telegram."));
6405
+ console.log(chalk7.dim(" 3. Paste that pairing code below to finish setup."));
6406
+ console.log("");
6407
+ const telegram = new TelegramChannel(config);
6408
+ try {
6409
+ await telegram.start();
6410
+ while (true) {
6411
+ const pairingCode = await ask(chalk7.white(" Telegram Pairing Code: "));
6412
+ if (!pairingCode) {
6413
+ console.log(chalk7.red(" Telegram pairing code is required to continue."));
6414
+ continue;
6415
+ }
6416
+ const approved = approveTelegramPendingRequestByPairingCode(config, pairingCode);
6417
+ if (!approved) {
6418
+ console.log(chalk7.red(" That pairing code is not valid yet. Send /start in Telegram, then paste the exact code here."));
6419
+ continue;
6420
+ }
6421
+ saveConfig(config);
6422
+ console.log(chalk7.green(` \u2713 Telegram paired. First admin: ${formatTelegramUser(approved)}.`));
6423
+ console.log("");
6424
+ break;
6425
+ }
6426
+ } finally {
6427
+ await telegram.stop();
6428
+ }
6429
+ }
5208
6430
  async function configure(existingConfig) {
5209
6431
  const isReconfig = !!existingConfig;
5210
6432
  const config = existingConfig ?? loadConfig();
5211
6433
  if (isReconfig) {
5212
6434
  banner();
5213
- console.log(chalk6.yellow(" Reconfiguring Mercury \u2014 press Enter to keep current value."));
6435
+ console.log(chalk7.yellow(" Reconfiguring Mercury \u2014 press Enter to keep current value."));
5214
6436
  } else {
5215
6437
  splashScreen();
5216
- console.log(chalk6.yellow(" First run detected \u2014 let's set you up."));
6438
+ console.log(chalk7.yellow(" First run detected \u2014 let's set you up."));
5217
6439
  }
5218
6440
  hr();
5219
6441
  console.log("");
5220
- console.log(chalk6.bold.white(" Identity"));
6442
+ console.log(chalk7.bold.white(" Identity"));
5221
6443
  console.log("");
5222
6444
  if (isReconfig) {
5223
- const ownerName = await ask(chalk6.white(` Your name [${config.identity.owner}]: `));
6445
+ const ownerName = await ask(chalk7.white(` Your name [${config.identity.owner}]: `));
5224
6446
  if (ownerName) config.identity.owner = ownerName;
5225
- const agentName = await ask(chalk6.white(` Agent name [${config.identity.name}]: `));
6447
+ const agentName = await ask(chalk7.white(` Agent name [${config.identity.name}]: `));
5226
6448
  if (agentName) config.identity.name = agentName;
5227
6449
  } else {
5228
- const ownerName = await ask(chalk6.white(" Your name: "));
6450
+ const ownerName = await ask(chalk7.white(" Your name: "));
5229
6451
  if (!ownerName) {
5230
- console.log(chalk6.red(" Name is required."));
6452
+ console.log(chalk7.red(" Name is required."));
5231
6453
  process.exit(1);
5232
6454
  }
5233
6455
  config.identity.owner = ownerName;
5234
- const agentName = await ask(chalk6.white(` Agent name [${config.identity.name}]: `));
6456
+ const agentName = await ask(chalk7.white(` Agent name [${config.identity.name}]: `));
5235
6457
  if (agentName) config.identity.name = agentName;
5236
6458
  }
5237
6459
  config.identity.creator = config.identity.creator || "Cosmic Stack";
5238
6460
  hr();
5239
6461
  console.log("");
5240
- console.log(chalk6.bold.white(" LLM Providers"));
6462
+ console.log(chalk7.bold.white(" LLM Providers"));
5241
6463
  if (isReconfig) {
5242
- console.log(chalk6.dim(" Choose which providers to configure now. Existing values are shown where available."));
6464
+ console.log(chalk7.dim(" Choose which providers to configure now. Existing values are shown where available."));
5243
6465
  } else {
5244
- console.log(chalk6.dim(" Choose one or more providers. Press Enter to configure DeepSeek by default."));
6466
+ console.log(chalk7.dim(" Choose one or more providers. Press Enter to configure DeepSeek by default."));
5245
6467
  }
5246
6468
  console.log("");
5247
6469
  while (true) {
@@ -5250,92 +6472,97 @@ async function configure(existingConfig) {
5250
6472
  for (const provider of selectedProviders) {
5251
6473
  if (provider === "deepseek") {
5252
6474
  const mask = isReconfig && config.providers.deepseek.apiKey ? ` [${maskKey(config.providers.deepseek.apiKey)}]` : "";
5253
- const key = await promptValidatedValue(
5254
- chalk6.white(` DeepSeek API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
5255
- (value) => validateApiKey("deepseek", value),
5256
- isReconfig ? config.providers.deepseek.apiKey : void 0,
5257
- { allowSkip: true }
6475
+ const result = await promptApiKeyWithModelSelection(
6476
+ config,
6477
+ "deepseek",
6478
+ "DeepSeek",
6479
+ chalk7.white(` DeepSeek API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
6480
+ isReconfig
5258
6481
  );
5259
- if (key) {
5260
- config.providers.deepseek.apiKey = key;
6482
+ if (!result.skipped && result.apiKey && result.model) {
6483
+ config.providers.deepseek.apiKey = result.apiKey;
6484
+ config.providers.deepseek.model = result.model;
5261
6485
  config.providers.deepseek.enabled = true;
5262
6486
  }
5263
6487
  continue;
5264
6488
  }
5265
6489
  if (provider === "openai") {
5266
6490
  const mask = isReconfig && config.providers.openai.apiKey ? ` [${maskKey(config.providers.openai.apiKey)}]` : "";
5267
- const key = await promptValidatedValue(
5268
- chalk6.white(` OpenAI API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
5269
- (value) => validateApiKey("openai", value),
5270
- isReconfig ? config.providers.openai.apiKey : void 0,
5271
- { allowSkip: true }
6491
+ const result = await promptApiKeyWithModelSelection(
6492
+ config,
6493
+ "openai",
6494
+ "OpenAI",
6495
+ chalk7.white(` OpenAI API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
6496
+ isReconfig
5272
6497
  );
5273
- if (key) {
5274
- config.providers.openai.apiKey = key;
6498
+ if (!result.skipped && result.apiKey && result.model) {
6499
+ config.providers.openai.apiKey = result.apiKey;
6500
+ config.providers.openai.model = result.model;
5275
6501
  config.providers.openai.enabled = true;
5276
6502
  }
5277
6503
  continue;
5278
6504
  }
5279
6505
  if (provider === "anthropic") {
5280
6506
  const mask = isReconfig && config.providers.anthropic.apiKey ? ` [${maskKey(config.providers.anthropic.apiKey)}]` : "";
5281
- const key = await promptValidatedValue(
5282
- chalk6.white(` Anthropic API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
5283
- (value) => validateApiKey("anthropic", value),
5284
- isReconfig ? config.providers.anthropic.apiKey : void 0,
5285
- { allowSkip: true }
6507
+ const result = await promptApiKeyWithModelSelection(
6508
+ config,
6509
+ "anthropic",
6510
+ "Anthropic",
6511
+ chalk7.white(` Anthropic API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
6512
+ isReconfig
5286
6513
  );
5287
- if (key) {
5288
- config.providers.anthropic.apiKey = key;
6514
+ if (!result.skipped && result.apiKey && result.model) {
6515
+ config.providers.anthropic.apiKey = result.apiKey;
6516
+ config.providers.anthropic.model = result.model;
5289
6517
  config.providers.anthropic.enabled = true;
5290
6518
  }
5291
6519
  continue;
5292
6520
  }
5293
6521
  if (provider === "grok") {
5294
6522
  const mask = isReconfig && config.providers.grok.apiKey ? ` [${maskKey(config.providers.grok.apiKey)}]` : "";
5295
- const key = await promptValidatedValue(
5296
- chalk6.white(` Grok API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
5297
- (value) => validateApiKey("grok", value),
5298
- isReconfig ? config.providers.grok.apiKey : void 0,
5299
- { allowSkip: true }
6523
+ const result = await promptApiKeyWithModelSelection(
6524
+ config,
6525
+ "grok",
6526
+ "Grok",
6527
+ chalk7.white(` Grok API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
6528
+ isReconfig
5300
6529
  );
5301
- if (key) {
5302
- config.providers.grok.apiKey = key;
6530
+ if (!result.skipped && result.apiKey && result.model) {
6531
+ config.providers.grok.apiKey = result.apiKey;
6532
+ config.providers.grok.model = result.model;
5303
6533
  config.providers.grok.enabled = true;
5304
6534
  }
5305
6535
  continue;
5306
6536
  }
5307
6537
  if (provider === "ollamaCloud") {
5308
6538
  const mask = isReconfig && config.providers.ollamaCloud.apiKey ? ` [${maskKey(config.providers.ollamaCloud.apiKey)}]` : "";
5309
- const key = await promptValidatedValue(
5310
- chalk6.white(` Ollama Cloud API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
5311
- (value) => validateApiKey("ollamaCloud", value),
5312
- isReconfig ? config.providers.ollamaCloud.apiKey : void 0,
5313
- { allowSkip: true }
6539
+ const result = await promptApiKeyWithModelSelection(
6540
+ config,
6541
+ "ollamaCloud",
6542
+ "Ollama Cloud",
6543
+ chalk7.white(` Ollama Cloud API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
6544
+ isReconfig
5314
6545
  );
5315
- if (key) {
5316
- config.providers.ollamaCloud.apiKey = key;
6546
+ if (!result.skipped && result.apiKey && result.model) {
6547
+ config.providers.ollamaCloud.apiKey = result.apiKey;
6548
+ config.providers.ollamaCloud.model = result.model;
5317
6549
  config.providers.ollamaCloud.enabled = true;
5318
6550
  }
5319
6551
  continue;
5320
6552
  }
5321
6553
  if (provider === "ollamaLocal") {
5322
- config.providers.ollamaLocal.baseUrl = await promptValidatedValue(
5323
- chalk6.white(` Ollama Local base URL [${config.providers.ollamaLocal.baseUrl}]: `),
5324
- validateBaseUrl,
5325
- config.providers.ollamaLocal.baseUrl
5326
- );
5327
- config.providers.ollamaLocal.model = await promptValidatedValue(
5328
- chalk6.white(` Ollama Local model [${config.providers.ollamaLocal.model}]: `),
5329
- validateModelName,
5330
- config.providers.ollamaLocal.model
5331
- );
5332
- config.providers.ollamaLocal.enabled = true;
6554
+ const result = await promptOllamaLocalModelSelection(config);
6555
+ if (!result.skipped && result.baseUrl && result.model) {
6556
+ config.providers.ollamaLocal.baseUrl = result.baseUrl;
6557
+ config.providers.ollamaLocal.model = result.model;
6558
+ config.providers.ollamaLocal.enabled = true;
6559
+ }
5333
6560
  }
5334
6561
  }
5335
6562
  const configuredProviders = getConfiguredProviderNames(config);
5336
6563
  if (configuredProviders.length === 0) {
5337
- console.log(chalk6.red(" You need to configure at least one LLM provider to continue."));
5338
- console.log(chalk6.dim(" Let\u2019s try that step again."));
6564
+ console.log(chalk7.red(" You need to configure at least one LLM provider to continue."));
6565
+ console.log(chalk7.dim(" Let\u2019s try that step again."));
5339
6566
  console.log("");
5340
6567
  continue;
5341
6568
  }
@@ -5344,78 +6571,81 @@ async function configure(existingConfig) {
5344
6571
  }
5345
6572
  hr();
5346
6573
  console.log("");
5347
- console.log(chalk6.bold.white(" Telegram (optional)"));
6574
+ console.log(chalk7.bold.white(" Telegram (optional)"));
5348
6575
  if (isReconfig) {
5349
- console.log(chalk6.dim(' Leave empty to keep current value. Enter "none" to disable.'));
6576
+ console.log(chalk7.dim(' Leave empty to keep current value. Enter "none" to disable.'));
5350
6577
  } else {
5351
- console.log(chalk6.dim(" Leave empty to skip. You can add it later."));
5352
- console.log(chalk6.dim(" To create a bot token:"));
5353
- console.log(chalk6.dim(" 1. Open Telegram and message @BotFather"));
5354
- console.log(chalk6.dim(" 2. Run /newbot and follow the prompts"));
5355
- console.log(chalk6.dim(" 3. Copy the bot token BotFather gives you"));
5356
- console.log(chalk6.dim(" 4. Paste that token here"));
6578
+ console.log(chalk7.dim(" Leave empty to skip. You can add it later."));
6579
+ console.log(chalk7.dim(" To create a bot token:"));
6580
+ console.log(chalk7.dim(" 1. Open Telegram and message @BotFather"));
6581
+ console.log(chalk7.dim(" 2. Run /newbot and follow the prompts"));
6582
+ console.log(chalk7.dim(" 3. Copy the bot token BotFather gives you"));
6583
+ console.log(chalk7.dim(" 4. Paste that token here"));
6584
+ console.log(chalk7.dim(" After setup, users send /start to request access."));
6585
+ console.log(chalk7.dim(" The first Telegram user gets a pairing code, and you approve that code from the CLI."));
5357
6586
  }
5358
6587
  console.log("");
5359
6588
  const tgMask = isReconfig && config.channels.telegram.botToken ? ` [${maskKey(config.channels.telegram.botToken)}]` : "";
5360
- const telegramToken = await ask(chalk6.white(` Telegram Bot Token${tgMask}: `));
6589
+ const telegramToken = await ask(chalk7.white(` Telegram Bot Token${tgMask}: `));
5361
6590
  if (isReconfig && telegramToken.toLowerCase() === "none") {
5362
6591
  config.channels.telegram.enabled = false;
5363
6592
  config.channels.telegram.botToken = "";
5364
- clearTelegramPairing(config);
6593
+ clearTelegramAccess(config);
5365
6594
  } else if (telegramToken) {
5366
6595
  if (telegramToken !== config.channels.telegram.botToken) {
5367
- clearTelegramPairing(config);
6596
+ clearTelegramAccess(config);
5368
6597
  }
5369
6598
  config.channels.telegram.botToken = telegramToken;
5370
6599
  config.channels.telegram.enabled = true;
5371
6600
  }
6601
+ await completeInitialTelegramPairing(config);
5372
6602
  hr();
5373
6603
  console.log("");
5374
- console.log(chalk6.bold.white(" GitHub Integration (optional)"));
5375
- console.log(chalk6.dim(" Connect Mercury to GitHub so it can create PRs, manage issues,"));
5376
- console.log(chalk6.dim(" review code, and co-author commits on your behalf."));
5377
- console.log(chalk6.dim(" Leave empty to skip. You can add it later with mercury doctor."));
6604
+ console.log(chalk7.bold.white(" GitHub Integration (optional)"));
6605
+ console.log(chalk7.dim(" Connect Mercury to GitHub so it can create PRs, manage issues,"));
6606
+ console.log(chalk7.dim(" review code, and co-author commits on your behalf."));
6607
+ console.log(chalk7.dim(" Leave empty to skip. You can add it later with mercury doctor."));
5378
6608
  console.log("");
5379
6609
  const ghUserCurrent = isReconfig && config.github.username ? ` [${config.github.username}]` : "";
5380
- const ghUsername = await ask(chalk6.white(` 1. Your GitHub username${ghUserCurrent}: `));
6610
+ const ghUsername = await ask(chalk7.white(` 1. Your GitHub username${ghUserCurrent}: `));
5381
6611
  if (ghUsername) config.github.username = ghUsername;
5382
6612
  if (!config.github.email) {
5383
6613
  config.github.email = "mercury@cosmicstack.org";
5384
6614
  }
5385
6615
  console.log("");
5386
- console.log(chalk6.dim(" You need a Personal Access Token (PAT) with repo access."));
5387
- console.log(chalk6.dim(" Fine-grained (recommended): github.com/settings/personal-access-tokens/new"));
5388
- console.log(chalk6.dim(" \u2192 Permissions: Contents (R/W), Pull requests (R/W), Issues (R/W)"));
5389
- console.log(chalk6.dim(" Classic: github.com/settings/tokens/new"));
5390
- console.log(chalk6.dim(" \u2192 Scope: repo (full control)"));
6616
+ console.log(chalk7.dim(" You need a Personal Access Token (PAT) with repo access."));
6617
+ console.log(chalk7.dim(" Fine-grained (recommended): github.com/settings/personal-access-tokens/new"));
6618
+ console.log(chalk7.dim(" \u2192 Permissions: Contents (R/W), Pull requests (R/W), Issues (R/W)"));
6619
+ console.log(chalk7.dim(" Classic: github.com/settings/tokens/new"));
6620
+ console.log(chalk7.dim(" \u2192 Scope: repo (full control)"));
5391
6621
  const ghTokenCurrent = process.env.GITHUB_TOKEN ? ` [${maskKey(process.env.GITHUB_TOKEN)}]` : "";
5392
- const ghToken = await ask(chalk6.white(` 2. GitHub PAT${ghTokenCurrent}: `));
6622
+ const ghToken = await ask(chalk7.white(` 2. GitHub PAT${ghTokenCurrent}: `));
5393
6623
  if (ghToken) {
5394
6624
  appendToEnv("GITHUB_TOKEN", ghToken);
5395
6625
  }
5396
6626
  if (config.github.username || process.env.GITHUB_TOKEN) {
5397
6627
  console.log("");
5398
- console.log(chalk6.dim(' Set a default repo so you can say "create an issue" without'));
5399
- console.log(chalk6.dim(" specifying the repo every time. Enter owner/name or a full URL."));
5400
- console.log(chalk6.dim(" Example: hotheadhacker/mercury-agent"));
5401
- console.log(chalk6.dim(" Example: https://github.com/hotheadhacker/mercury-agent"));
6628
+ console.log(chalk7.dim(' Set a default repo so you can say "create an issue" without'));
6629
+ console.log(chalk7.dim(" specifying the repo every time. Enter owner/name or a full URL."));
6630
+ console.log(chalk7.dim(" Example: hotheadhacker/mercury-agent"));
6631
+ console.log(chalk7.dim(" Example: https://github.com/hotheadhacker/mercury-agent"));
5402
6632
  const ghOwnerCurrent = isReconfig && config.github.defaultOwner ? ` [${config.github.defaultOwner}/${config.github.defaultRepo}]` : "";
5403
- const ghRepoInput = await ask(chalk6.white(` 3. Default repo${ghOwnerCurrent}: `));
6633
+ const ghRepoInput = await ask(chalk7.white(` 3. Default repo${ghOwnerCurrent}: `));
5404
6634
  if (ghRepoInput) {
5405
6635
  const parsed = parseGithubRepo(ghRepoInput);
5406
6636
  if (parsed) {
5407
6637
  config.github.defaultOwner = parsed.owner;
5408
6638
  config.github.defaultRepo = parsed.repo;
5409
6639
  } else {
5410
- console.log(chalk6.yellow(" Could not parse repo. Use format: owner/repo or a GitHub URL."));
6640
+ console.log(chalk7.yellow(" Could not parse repo. Use format: owner/repo or a GitHub URL."));
5411
6641
  }
5412
6642
  }
5413
6643
  }
5414
6644
  hr();
5415
6645
  console.log("");
5416
- console.log(chalk6.bold.white(" Token Budget"));
6646
+ console.log(chalk7.bold.white(" Token Budget"));
5417
6647
  console.log("");
5418
- const budgetPrompt = isReconfig ? chalk6.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `) : chalk6.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `);
6648
+ const budgetPrompt = isReconfig ? chalk7.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `) : chalk7.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `);
5419
6649
  const budgetStr = await ask(budgetPrompt);
5420
6650
  if (budgetStr) {
5421
6651
  const budget = parseInt(budgetStr.replace(/,/g, ""), 10);
@@ -5427,14 +6657,14 @@ async function configure(existingConfig) {
5427
6657
  saveConfig(config);
5428
6658
  const home = getMercuryHome();
5429
6659
  console.log("");
5430
- console.log(chalk6.green(` \u2713 Config saved to ${home}/mercury.yaml`));
5431
- console.log(chalk6.green(` \u2713 Soul files seeded in ${home}/soul/`));
5432
- console.log(chalk6.green(` \u2713 Memory stored in ${home}/memory/`));
5433
- console.log(chalk6.green(` \u2713 Permissions seeded in ${home}/permissions.yaml`));
5434
- console.log(chalk6.green(` \u2713 Skills directory ready in ${home}/skills/`));
6660
+ console.log(chalk7.green(` \u2713 Config saved to ${home}/mercury.yaml`));
6661
+ console.log(chalk7.green(` \u2713 Soul files seeded in ${home}/soul/`));
6662
+ console.log(chalk7.green(` \u2713 Memory stored in ${home}/memory/`));
6663
+ console.log(chalk7.green(` \u2713 Permissions seeded in ${home}/permissions.yaml`));
6664
+ console.log(chalk7.green(` \u2713 Skills directory ready in ${home}/skills/`));
5435
6665
  console.log("");
5436
- console.log(chalk6.cyan(` ${config.identity.name} is ready. Run \`mercury start\` to chat.`));
5437
- console.log(chalk6.dim(" mercury.cosmicstack.org"));
6666
+ console.log(chalk7.cyan(` ${config.identity.name} is ready. Run \`mercury start\` to chat.`));
6667
+ console.log(chalk7.dim(" mercury.cosmicstack.org"));
5438
6668
  console.log("");
5439
6669
  }
5440
6670
  function autoDaemonize() {
@@ -5442,22 +6672,22 @@ function autoDaemonize() {
5442
6672
  if (daemon.running) {
5443
6673
  return;
5444
6674
  }
5445
- console.log(chalk6.dim(" Setting up background mode..."));
6675
+ console.log(chalk7.dim(" Setting up background mode..."));
5446
6676
  try {
5447
6677
  if (!isServiceInstalled()) {
5448
6678
  installService();
5449
6679
  }
5450
6680
  } catch {
5451
- console.log(chalk6.dim(" Service install skipped (can run `mercury service install` later)."));
6681
+ console.log(chalk7.dim(" Service install skipped (can run `mercury service install` later)."));
5452
6682
  }
5453
6683
  const ok = tryAutoDaemonize();
5454
6684
  if (ok) {
5455
6685
  const status = getDaemonStatus();
5456
- console.log(chalk6.green(` \u2713 Mercury is running in background (PID: ${status.pid})`));
5457
- console.log(chalk6.green(" \u2713 Auto-starts on login. Auto-restarts on crash."));
5458
- console.log(chalk6.dim(" Use `mercury stop` to stop. `mercury restart` to restart."));
6686
+ console.log(chalk7.green(` \u2713 Mercury is running in background (PID: ${status.pid})`));
6687
+ console.log(chalk7.green(" \u2713 Auto-starts on login. Auto-restarts on crash."));
6688
+ console.log(chalk7.dim(" Use `mercury stop` to stop. `mercury restart` to restart."));
5459
6689
  } else {
5460
- console.log(chalk6.dim(" Background mode not available. Run `mercury up` to set it up."));
6690
+ console.log(chalk7.dim(" Background mode not available. Run `mercury up` to set it up."));
5461
6691
  }
5462
6692
  console.log("");
5463
6693
  }
@@ -5467,7 +6697,7 @@ async function runAgent(isDaemon = false) {
5467
6697
  const name = config.identity.name;
5468
6698
  if (!isDaemon) {
5469
6699
  banner();
5470
- console.log(chalk6.white(` ${name} is waking up...`));
6700
+ console.log(chalk7.white(` ${name} is waking up...`));
5471
6701
  console.log("");
5472
6702
  } else {
5473
6703
  logger.info(`${name} is waking up (daemon mode)...`);
@@ -5479,19 +6709,25 @@ async function runAgent(isDaemon = false) {
5479
6709
  logger.error("No LLM providers available. Run `mercury doctor` to configure providers.");
5480
6710
  return;
5481
6711
  }
5482
- console.log(chalk6.red(" No LLM providers available. Run `mercury doctor` to configure providers."));
6712
+ console.log(chalk7.red(" No LLM providers available. Run `mercury doctor` to configure providers."));
5483
6713
  process.exit(1);
5484
6714
  }
5485
6715
  const available = providers.listAvailable();
6716
+ const providerLabels = available.map((provider) => getProviderLabel(provider));
6717
+ const providerModels = available.map((provider) => {
6718
+ const key = provider;
6719
+ return `${getProviderLabel(key)}: ${config.providers[key].model}`;
6720
+ });
5486
6721
  if (!isDaemon) {
5487
- console.log(chalk6.dim(` Providers: ${available.join(", ")}`));
6722
+ console.log(chalk7.dim(` Providers: ${providerLabels.join(", ")}`));
6723
+ console.log(chalk7.dim(` Models: ${providerModels.join(" | ")}`));
5488
6724
  } else {
5489
6725
  logger.info({ providers: available }, "Providers loaded");
5490
6726
  }
5491
6727
  const skillLoader = new SkillLoader();
5492
6728
  const skills = skillLoader.discover();
5493
6729
  if (!isDaemon) {
5494
- console.log(chalk6.dim(` Skills: ${skills.length > 0 ? skills.map((s) => s.name).join(", ") : "none installed"}`));
6730
+ console.log(chalk7.dim(` Skills: ${skills.length > 0 ? skills.map((s) => s.name).join(", ") : "none installed"}`));
5495
6731
  }
5496
6732
  const scheduler = new Scheduler(config);
5497
6733
  const identity = new Identity();
@@ -5508,22 +6744,30 @@ async function runAgent(isDaemon = false) {
5508
6744
  manual: () => getManual()
5509
6745
  });
5510
6746
  capabilities.setSendFileHandler(async (filePath) => {
5511
- const msg = channels.getActiveChannels().includes("telegram") ? channels.get("telegram") : channels.get("cli");
5512
- if (msg) {
5513
- await msg.sendFile(filePath);
6747
+ const { channelId, channelType } = capabilities.getChannelContext();
6748
+ const telegram = channels.get("telegram");
6749
+ if (channelType === "telegram" && telegram) {
6750
+ await telegram.sendFile(filePath, channelId);
6751
+ return;
6752
+ }
6753
+ if (config.channels.telegram.enabled && telegram && getTelegramApprovedUsers(config).length > 0) {
6754
+ await telegram.sendFile(filePath);
6755
+ return;
6756
+ }
6757
+ const cli = channels.get("cli");
6758
+ if (cli) {
6759
+ await cli.sendFile(filePath);
5514
6760
  }
5515
6761
  });
5516
6762
  capabilities.setSendMessageHandler(async (content) => {
5517
6763
  const telegram = channels.get("telegram");
5518
- const pairedChatId = config.channels.telegram.pairedChatId;
5519
- const pairedUserId = config.channels.telegram.pairedUserId;
5520
6764
  if (!config.channels.telegram.enabled || !telegram) {
5521
6765
  throw new Error("Telegram is not configured. Add a bot token in setup or run `mercury doctor`.");
5522
6766
  }
5523
- if (pairedChatId == null || pairedUserId == null) {
5524
- throw new Error("Telegram is not paired. Complete the pairing flow with /start or /pair from the Telegram owner account.");
6767
+ if (getTelegramApprovedUsers(config).length === 0) {
6768
+ throw new Error("Telegram has no approved users. Ask someone to send /start, then approve the request from Mercury.");
5525
6769
  }
5526
- await telegram.send(content, `telegram:${pairedChatId}`);
6770
+ await telegram.send(content);
5527
6771
  });
5528
6772
  if (process.env.GITHUB_TOKEN) {
5529
6773
  setGitHubToken(process.env.GITHUB_TOKEN);
@@ -5558,17 +6802,13 @@ async function runAgent(isDaemon = false) {
5558
6802
  const activeCh = channels.getActiveChannels();
5559
6803
  const toolNames = capabilities.getToolNames();
5560
6804
  if (!isDaemon) {
5561
- console.log(chalk6.dim(` Channels: ${activeCh.join(", ")}`));
5562
- console.log(chalk6.dim(` Tools: ${toolNames.join(", ")}`));
5563
- console.log(chalk6.dim(` Permissions: ${getMercuryHome()}/permissions.yaml`));
5564
- console.log(chalk6.dim(` Schedules: ${getMercuryHome()}/schedules.yaml`));
5565
6805
  if (config.identity.creator) {
5566
- console.log(chalk6.dim(` Creator: ${config.identity.creator}`));
6806
+ console.log(chalk7.dim(` Creator: ${config.identity.creator}`));
5567
6807
  }
5568
6808
  hr();
5569
6809
  console.log("");
5570
- console.log(chalk6.green(` ${name} is live. Type a message and press Enter.`));
5571
- console.log(chalk6.dim(" Ctrl+C to exit \xB7 /help for commands"));
6810
+ console.log(chalk7.green(` ${name} is live. Type a message and press Enter.`));
6811
+ console.log(chalk7.dim(" Ctrl+C to exit \xB7 /help for commands"));
5572
6812
  console.log("");
5573
6813
  } else {
5574
6814
  logger.info({ channels: activeCh, tools: toolNames }, "Mercury is live (daemon mode)");
@@ -5576,7 +6816,7 @@ async function runAgent(isDaemon = false) {
5576
6816
  const shutdown = async () => {
5577
6817
  if (!isDaemon) {
5578
6818
  console.log("");
5579
- console.log(chalk6.dim(` ${name} is shutting down...`));
6819
+ console.log(chalk7.dim(` ${name} is shutting down...`));
5580
6820
  } else {
5581
6821
  logger.info("Mercury is shutting down (daemon mode)");
5582
6822
  }
@@ -5623,17 +6863,17 @@ program.command("up").description("Ensure Mercury is running persistently \u2014
5623
6863
  const daemon = getDaemonStatus();
5624
6864
  if (daemon.running && daemon.pid) {
5625
6865
  console.log("");
5626
- console.log(chalk6.green(` Mercury is already running (PID: ${daemon.pid})`));
5627
- console.log(chalk6.dim(` Logs: ${daemon.logPath}`));
6866
+ console.log(chalk7.green(` Mercury is already running (PID: ${daemon.pid})`));
6867
+ console.log(chalk7.dim(` Logs: ${daemon.logPath}`));
5628
6868
  console.log("");
5629
6869
  return;
5630
6870
  }
5631
6871
  if (!isServiceInstalled()) {
5632
6872
  console.log("");
5633
- console.log(chalk6.cyan(" Installing Mercury as a system service..."));
6873
+ console.log(chalk7.cyan(" Installing Mercury as a system service..."));
5634
6874
  installService();
5635
6875
  }
5636
- console.log(chalk6.cyan(" Starting Mercury in background..."));
6876
+ console.log(chalk7.cyan(" Starting Mercury in background..."));
5637
6877
  startBackground();
5638
6878
  });
5639
6879
  program.command("logs").description("Show recent daemon logs").action(() => {
@@ -5660,43 +6900,175 @@ program.command("status").description("Show current configuration and daemon sta
5660
6900
  const skills = skillLoader.discover();
5661
6901
  const daemon = getDaemonStatus();
5662
6902
  banner();
5663
- console.log(` Name: ${chalk6.cyan(config.identity.name)}`);
5664
- console.log(` Owner: ${chalk6.white(config.identity.owner || "(not set)")}`);
6903
+ console.log(` Name: ${chalk7.cyan(config.identity.name)}`);
6904
+ console.log(` Owner: ${chalk7.white(config.identity.owner || "(not set)")}`);
5665
6905
  if (config.identity.creator) {
5666
- console.log(` Creator: ${chalk6.white(config.identity.creator)}`);
5667
- }
5668
- console.log(` Provider: ${chalk6.white(getProviderLabel(config.providers.default))}`);
5669
- console.log(` Telegram: ${config.channels.telegram.enabled ? chalk6.green("enabled") : chalk6.dim("disabled")}`);
5670
- console.log(` Telegram Pairing: ${config.channels.telegram.pairedUserId != null ? chalk6.green(`paired to user ${config.channels.telegram.pairedUserId}${config.channels.telegram.pairedUsername ? ` (@${config.channels.telegram.pairedUsername})` : ""}`) : chalk6.dim("unpaired")}`);
5671
- console.log(` Skills: ${skills.length > 0 ? chalk6.green(skills.map((s) => s.name).join(", ")) : chalk6.dim("none")}`);
5672
- console.log(` Budget: ${chalk6.white(config.tokens.dailyBudget.toLocaleString())} tokens/day`);
5673
- console.log(` Setup: ${isSetupComplete() ? chalk6.green("complete") : chalk6.red("not done")}`);
5674
- console.log(` Daemon: ${daemon.running ? chalk6.green(`running (PID: ${daemon.pid})`) : chalk6.dim("not running")}`);
5675
- console.log(` Home: ${chalk6.dim(home)}`);
6906
+ console.log(` Creator: ${chalk7.white(config.identity.creator)}`);
6907
+ }
6908
+ console.log(` Provider: ${chalk7.white(getProviderLabel(config.providers.default))}`);
6909
+ console.log(` Telegram: ${config.channels.telegram.enabled ? chalk7.green("enabled") : chalk7.dim("disabled")}`);
6910
+ console.log(` Telegram Access: ${chalk7.white(getTelegramAccessSummary(config))}`);
6911
+ console.log(` Skills: ${skills.length > 0 ? chalk7.green(skills.map((s) => s.name).join(", ")) : chalk7.dim("none")}`);
6912
+ console.log(` Budget: ${chalk7.white(config.tokens.dailyBudget.toLocaleString())} tokens/day`);
6913
+ console.log(` Setup: ${isSetupComplete() ? chalk7.green("complete") : chalk7.red("not done")}`);
6914
+ console.log(` Daemon: ${daemon.running ? chalk7.green(`running (PID: ${daemon.pid})`) : chalk7.dim("not running")}`);
6915
+ console.log(` Home: ${chalk7.dim(home)}`);
6916
+ printTelegramAccessState(config);
5676
6917
  console.log("");
5677
6918
  });
5678
6919
  program.command("help").description("Show capabilities and commands manual").action(() => {
5679
6920
  console.log(getManual());
5680
6921
  });
5681
- var telegramCmd = program.command("telegram").description("Manage Telegram pairing and access");
5682
- telegramCmd.command("unpair").description("Clear the paired Telegram owner for this Mercury instance").action(() => {
6922
+ var telegramCmd = program.command("telegram").description("Manage Telegram access approvals and admins");
6923
+ telegramCmd.command("list").description("Show approved Telegram users and pending access requests").action(() => {
5683
6924
  const config = loadConfig();
5684
- const daemon = getDaemonStatus();
5685
- if (config.channels.telegram.pairedUserId == null) {
6925
+ console.log("");
6926
+ printTelegramAccessState(config);
6927
+ console.log("");
6928
+ });
6929
+ telegramCmd.command("approve <codeOrUserId>").description("Approve a pending Telegram access request by pairing code or user ID").action((codeOrUserId) => {
6930
+ const config = loadConfig();
6931
+ const hasAdmins = hasTelegramAdmins(config);
6932
+ if (!hasAdmins) {
6933
+ const approved2 = approveTelegramPendingRequestByPairingCode(config, codeOrUserId.trim());
6934
+ if (!approved2) {
6935
+ console.log("");
6936
+ console.log(chalk7.red(` No pending first-time Telegram pairing found for code ${codeOrUserId}.`));
6937
+ console.log("");
6938
+ return;
6939
+ }
6940
+ saveConfig(config);
6941
+ console.log("");
6942
+ console.log(chalk7.green(` \u2713 Approved first Telegram admin ${formatTelegramUser(approved2)}.`));
6943
+ restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
6944
+ console.log("");
6945
+ return;
6946
+ }
6947
+ const targetUserId = Number(codeOrUserId);
6948
+ if (isNaN(targetUserId)) {
6949
+ console.log("");
6950
+ console.log(chalk7.red(" Please provide a numeric Telegram user ID once Telegram already has an admin."));
6951
+ console.log("");
6952
+ return;
6953
+ }
6954
+ const approved = approveTelegramPendingRequest(config, targetUserId, "member");
6955
+ if (!approved) {
5686
6956
  console.log("");
5687
- console.log(chalk6.dim(" Telegram is already unpaired."));
6957
+ console.log(chalk7.red(` No pending Telegram request found for user ${codeOrUserId}.`));
5688
6958
  console.log("");
5689
6959
  return;
5690
6960
  }
5691
- clearTelegramPairing(config);
5692
6961
  saveConfig(config);
5693
6962
  console.log("");
5694
- console.log(chalk6.green(" \u2713 Telegram pairing cleared."));
5695
- if (daemon.running) {
5696
- console.log(chalk6.dim(" Restarting the background daemon to apply the change immediately..."));
5697
- restartDaemon();
5698
- } else {
5699
- console.log(chalk6.dim(" The next private Telegram user to send /start will pair this Mercury instance."));
6963
+ console.log(chalk7.green(` \u2713 Approved Telegram member ${formatTelegramUser(approved)}.`));
6964
+ restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
6965
+ console.log("");
6966
+ });
6967
+ telegramCmd.command("reject <userId>").description("Reject a pending Telegram access request").action((userId) => {
6968
+ const config = loadConfig();
6969
+ const targetUserId = Number(userId);
6970
+ if (isNaN(targetUserId)) {
6971
+ console.log("");
6972
+ console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
6973
+ console.log("");
6974
+ return;
6975
+ }
6976
+ const rejected = rejectTelegramPendingRequest(config, targetUserId);
6977
+ if (!rejected) {
6978
+ console.log("");
6979
+ console.log(chalk7.red(` No pending Telegram request found for user ${userId}.`));
6980
+ console.log("");
6981
+ return;
6982
+ }
6983
+ saveConfig(config);
6984
+ console.log("");
6985
+ console.log(chalk7.green(` \u2713 Rejected Telegram request for ${formatTelegramUser(rejected)}.`));
6986
+ restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
6987
+ console.log("");
6988
+ });
6989
+ telegramCmd.command("remove <userId>").description("Remove an approved Telegram admin or member").action((userId) => {
6990
+ const config = loadConfig();
6991
+ const targetUserId = Number(userId);
6992
+ if (isNaN(targetUserId)) {
6993
+ console.log("");
6994
+ console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
6995
+ console.log("");
6996
+ return;
6997
+ }
6998
+ const removed = removeTelegramUser(config, targetUserId);
6999
+ if (!removed) {
7000
+ console.log("");
7001
+ console.log(chalk7.red(` No approved Telegram user found for ${userId}.`));
7002
+ console.log("");
7003
+ return;
7004
+ }
7005
+ saveConfig(config);
7006
+ console.log("");
7007
+ console.log(chalk7.green(` \u2713 Removed Telegram access for ${formatTelegramUser(removed)}.`));
7008
+ restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
7009
+ console.log("");
7010
+ });
7011
+ telegramCmd.command("promote <userId>").description("Promote an approved Telegram member to admin").action((userId) => {
7012
+ const config = loadConfig();
7013
+ const targetUserId = Number(userId);
7014
+ if (isNaN(targetUserId)) {
7015
+ console.log("");
7016
+ console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
7017
+ console.log("");
7018
+ return;
7019
+ }
7020
+ const promoted = promoteTelegramUserToAdmin(config, targetUserId);
7021
+ if (!promoted) {
7022
+ console.log("");
7023
+ console.log(chalk7.red(` No Telegram member found for ${userId}.`));
7024
+ console.log("");
7025
+ return;
7026
+ }
7027
+ saveConfig(config);
7028
+ console.log("");
7029
+ console.log(chalk7.green(` \u2713 Promoted ${formatTelegramUser(promoted)} to Telegram admin.`));
7030
+ restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
7031
+ console.log("");
7032
+ });
7033
+ telegramCmd.command("demote <userId>").description("Demote a Telegram admin to member").action((userId) => {
7034
+ const config = loadConfig();
7035
+ const targetUserId = Number(userId);
7036
+ if (isNaN(targetUserId)) {
7037
+ console.log("");
7038
+ console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
7039
+ console.log("");
7040
+ return;
7041
+ }
7042
+ const demoted = demoteTelegramAdmin(config, targetUserId);
7043
+ if (!demoted) {
7044
+ console.log("");
7045
+ console.log(chalk7.red(" Could not demote that Telegram admin. Mercury must keep at least one admin."));
7046
+ console.log("");
7047
+ return;
7048
+ }
7049
+ saveConfig(config);
7050
+ console.log("");
7051
+ console.log(chalk7.green(` \u2713 Demoted ${formatTelegramUser(demoted)} to Telegram member.`));
7052
+ restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
7053
+ console.log("");
7054
+ });
7055
+ telegramCmd.command("unpair").description("Reset all Telegram access for this Mercury instance").action(() => {
7056
+ const config = loadConfig();
7057
+ const hasAnyAccess = getTelegramApprovedUsers(config).length > 0 || getTelegramPendingRequests(config).length > 0;
7058
+ if (!hasAnyAccess) {
7059
+ console.log("");
7060
+ console.log(chalk7.dim(" Telegram access is already empty."));
7061
+ console.log("");
7062
+ return;
7063
+ }
7064
+ clearTelegramAccess(config);
7065
+ saveConfig(config);
7066
+ console.log("");
7067
+ console.log(chalk7.green(" \u2713 Telegram access reset."));
7068
+ restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
7069
+ if (!getDaemonStatus().running) {
7070
+ console.log(chalk7.dim(" New private Telegram users can send /start to request access."));
7071
+ console.log(chalk7.dim(" The first request must be approved from the CLI with `mercury telegram approve <pairing-code>`."));
5700
7072
  }
5701
7073
  console.log("");
5702
7074
  });