@cosmicstack/mercury-agent 0.4.1 → 0.5.1
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/README.md +18 -8
- package/dist/index.js +2441 -1028
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
9
|
-
import
|
|
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: {
|
|
@@ -112,7 +115,7 @@ function getDefaultConfig() {
|
|
|
112
115
|
intervalMinutes: getEnvNum("HEARTBEAT_INTERVAL_MINUTES", 60)
|
|
113
116
|
},
|
|
114
117
|
tokens: {
|
|
115
|
-
dailyBudget: getEnvNum("DAILY_TOKEN_BUDGET",
|
|
118
|
+
dailyBudget: getEnvNum("DAILY_TOKEN_BUDGET", 1e6)
|
|
116
119
|
}
|
|
117
120
|
};
|
|
118
121
|
}
|
|
@@ -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
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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,217 +891,734 @@ var Lifecycle = class {
|
|
|
761
891
|
}
|
|
762
892
|
};
|
|
763
893
|
|
|
764
|
-
// src/
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
776
|
-
|
|
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
|
-
|
|
804
|
-
this.
|
|
910
|
+
emit(message) {
|
|
911
|
+
this.messageHandler?.(message);
|
|
805
912
|
}
|
|
806
913
|
};
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const
|
|
876
|
-
const
|
|
877
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
|
|
880
|
-
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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 (
|
|
908
|
-
|
|
909
|
-
|
|
1197
|
+
if (key.name === "up") {
|
|
1198
|
+
activeIndex = (activeIndex - 1 + options.length) % options.length;
|
|
1199
|
+
render();
|
|
910
1200
|
return;
|
|
911
1201
|
}
|
|
912
|
-
if (
|
|
913
|
-
|
|
914
|
-
|
|
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 (
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
936
|
-
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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 = 16;
|
|
1400
|
+
aborted = false;
|
|
1401
|
+
record(toolName, params) {
|
|
1402
|
+
const paramsKey = JSON.stringify(params).slice(0, 100);
|
|
1403
|
+
this.recentCalls.push({ tool: toolName, params: paramsKey });
|
|
1404
|
+
if (this.recentCalls.length > this.maxEntries) {
|
|
1405
|
+
this.recentCalls.shift();
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
detect() {
|
|
1409
|
+
if (this.recentCalls.length < 3) return null;
|
|
1410
|
+
const last = this.recentCalls[this.recentCalls.length - 1];
|
|
1411
|
+
let identicalCount = 0;
|
|
1412
|
+
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
1413
|
+
if (this.recentCalls[i].tool === last.tool && this.recentCalls[i].params === last.params) {
|
|
1414
|
+
identicalCount++;
|
|
1415
|
+
} else {
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (identicalCount >= 3) {
|
|
1420
|
+
this.aborted = true;
|
|
1421
|
+
return {
|
|
1422
|
+
tool: last.tool,
|
|
1423
|
+
count: identicalCount,
|
|
1424
|
+
message: `You called "${last.tool}" ${identicalCount} times with identical parameters and got the same result. Stop repeating this call entirely.`
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
const lastTool = last.tool;
|
|
1428
|
+
let sameToolCount = 0;
|
|
1429
|
+
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
1430
|
+
if (this.recentCalls[i].tool === lastTool) {
|
|
1431
|
+
sameToolCount++;
|
|
1432
|
+
} else {
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (sameToolCount >= 3) {
|
|
1437
|
+
this.aborted = true;
|
|
1438
|
+
return {
|
|
1439
|
+
tool: lastTool,
|
|
1440
|
+
count: sameToolCount,
|
|
1441
|
+
message: `You called "${lastTool}" ${sameToolCount} times in a row with slightly different parameters and it isn't working. Stop \u2014 the approach is wrong. Step back, tell the user what you tried and what failed, and suggest alternatives instead of retrying.`
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
isAborted() {
|
|
1447
|
+
return this.aborted;
|
|
1448
|
+
}
|
|
1449
|
+
reset() {
|
|
1450
|
+
this.recentCalls = [];
|
|
1451
|
+
this.aborted = false;
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
var MAX_STEPS = 10;
|
|
1455
|
+
var Agent = class {
|
|
1456
|
+
constructor(config, providers, identity, shortTerm, longTerm, episodic, channels, tokenBudget, capabilities, scheduler) {
|
|
1457
|
+
this.config = config;
|
|
1458
|
+
this.providers = providers;
|
|
1459
|
+
this.identity = identity;
|
|
1460
|
+
this.shortTerm = shortTerm;
|
|
1461
|
+
this.longTerm = longTerm;
|
|
1462
|
+
this.episodic = episodic;
|
|
1463
|
+
this.channels = channels;
|
|
1464
|
+
this.tokenBudget = tokenBudget;
|
|
1465
|
+
this.lifecycle = new Lifecycle();
|
|
1466
|
+
this.scheduler = scheduler;
|
|
1467
|
+
this.capabilities = capabilities;
|
|
1468
|
+
this.telegramStreaming = config.channels.telegram.streaming ?? true;
|
|
1469
|
+
this.scheduler.setOnScheduledTask(async (manifest) => this.handleScheduledTask(manifest));
|
|
1470
|
+
this.channels.onIncomingMessage((msg) => this.enqueueMessage(msg));
|
|
1471
|
+
this.scheduler.onHeartbeat(async () => {
|
|
1472
|
+
await this.heartbeat();
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
config;
|
|
1476
|
+
providers;
|
|
1477
|
+
identity;
|
|
1478
|
+
shortTerm;
|
|
1479
|
+
longTerm;
|
|
1480
|
+
episodic;
|
|
1481
|
+
channels;
|
|
1482
|
+
tokenBudget;
|
|
1483
|
+
lifecycle;
|
|
1484
|
+
scheduler;
|
|
1485
|
+
capabilities;
|
|
1486
|
+
running = false;
|
|
1487
|
+
messageQueue = [];
|
|
1488
|
+
processing = false;
|
|
1489
|
+
telegramStreaming;
|
|
1490
|
+
enqueueMessage(msg) {
|
|
1491
|
+
logger.info({ from: msg.channelType, content: msg.content.slice(0, 50) }, "Message enqueued");
|
|
1492
|
+
this.messageQueue.push(msg);
|
|
1493
|
+
this.processQueue();
|
|
1494
|
+
}
|
|
1495
|
+
async processQueue() {
|
|
1496
|
+
if (this.processing) return;
|
|
1497
|
+
if (this.messageQueue.length === 0) return;
|
|
1498
|
+
if (!this.lifecycle.is("idle")) return;
|
|
1499
|
+
this.processing = true;
|
|
1500
|
+
while (this.messageQueue.length > 0) {
|
|
1501
|
+
const msg = this.messageQueue.shift();
|
|
1502
|
+
try {
|
|
1503
|
+
await this.handleMessage(msg);
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
logger.error({ err, msg: msg.content.slice(0, 50) }, "Failed to handle message");
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
this.processing = false;
|
|
1509
|
+
}
|
|
1510
|
+
async birth() {
|
|
1511
|
+
this.lifecycle.transition("birthing");
|
|
1512
|
+
logger.info({ name: this.config.identity.name }, "Mercury is being born...");
|
|
1513
|
+
this.lifecycle.transition("onboarding");
|
|
1514
|
+
}
|
|
1515
|
+
async wake() {
|
|
1516
|
+
this.lifecycle.transition("onboarding");
|
|
1517
|
+
this.lifecycle.transition("idle");
|
|
1518
|
+
this.scheduler.restorePersistedTasks();
|
|
1519
|
+
this.scheduler.startHeartbeat();
|
|
1520
|
+
await this.channels.startAll();
|
|
1521
|
+
this.running = true;
|
|
1522
|
+
const activeChannels = this.channels.getActiveChannels();
|
|
1523
|
+
const toolNames = this.capabilities.getToolNames();
|
|
1524
|
+
logger.info({ channels: activeChannels, tools: toolNames }, "Mercury is awake");
|
|
1525
|
+
}
|
|
1526
|
+
async sleep() {
|
|
1527
|
+
this.running = false;
|
|
1528
|
+
this.scheduler.stopAll();
|
|
1529
|
+
await this.channels.stopAll();
|
|
1530
|
+
this.lifecycle.transition("sleeping");
|
|
1531
|
+
logger.info("Mercury is sleeping");
|
|
1532
|
+
}
|
|
1533
|
+
async handleMessage(msg) {
|
|
1534
|
+
this.lifecycle.transition("thinking");
|
|
1535
|
+
const startTime = Date.now();
|
|
1536
|
+
const isInternal = msg.channelType === "internal";
|
|
1537
|
+
const isScheduled = msg.senderId === "system" && msg.channelType !== "internal";
|
|
1538
|
+
if (isInternal || isScheduled) {
|
|
1539
|
+
this.capabilities.permissions.setAutoApproveAll(true);
|
|
1540
|
+
}
|
|
1541
|
+
try {
|
|
1542
|
+
const trimmed = msg.content.trim();
|
|
1543
|
+
if (trimmed.startsWith("/budget")) {
|
|
1544
|
+
const subcommand = trimmed.slice("/budget".length).trim();
|
|
1545
|
+
await this.handleBudgetCommand(subcommand || "status", msg.channelType, msg.channelId);
|
|
1546
|
+
this.lifecycle.transition("idle");
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (trimmed === "/budget_override") {
|
|
1550
|
+
await this.handleBudgetCommand("override", msg.channelType, msg.channelId);
|
|
1551
|
+
this.lifecycle.transition("idle");
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
if (trimmed === "/budget_reset") {
|
|
1555
|
+
await this.handleBudgetCommand("reset", msg.channelType, msg.channelId);
|
|
1556
|
+
this.lifecycle.transition("idle");
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (trimmed.startsWith("/budget_set")) {
|
|
1560
|
+
const args = trimmed.slice("/budget_set".length).trim();
|
|
1561
|
+
await this.handleBudgetCommand("set " + args, msg.channelType, msg.channelId);
|
|
1562
|
+
this.lifecycle.transition("idle");
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
if (trimmed.startsWith("/stream")) {
|
|
1566
|
+
const sub = trimmed.slice("/stream".length).trim().toLowerCase();
|
|
1567
|
+
if (sub === "off") {
|
|
1568
|
+
this.telegramStreaming = false;
|
|
1569
|
+
} else if (sub === "on") {
|
|
1570
|
+
this.telegramStreaming = true;
|
|
1571
|
+
} else {
|
|
1572
|
+
this.telegramStreaming = !this.telegramStreaming;
|
|
1573
|
+
}
|
|
1574
|
+
const ch = this.channels.get(msg.channelType);
|
|
1575
|
+
if (ch) await ch.send(
|
|
1576
|
+
this.telegramStreaming ? "Telegram streaming enabled. Responses will appear progressively." : "Telegram streaming disabled. Responses will arrive as a single message.",
|
|
1577
|
+
msg.channelId
|
|
1578
|
+
);
|
|
1579
|
+
this.lifecycle.transition("idle");
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (await this.handleChatCommand(trimmed, msg.channelType, msg.channelId)) {
|
|
1583
|
+
this.lifecycle.transition("idle");
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
if (this.tokenBudget.isOverBudget()) {
|
|
1587
|
+
const channel2 = this.channels.getChannelForMessage(msg);
|
|
1588
|
+
if (channel2 && msg.channelType !== "internal") {
|
|
1589
|
+
if (msg.channelType === "cli") {
|
|
1590
|
+
if (["1", "2", "3", "4"].includes(trimmed)) {
|
|
1591
|
+
await this.handleBudgetCommand(trimmed, msg.channelType, msg.channelId);
|
|
1592
|
+
this.lifecycle.transition("idle");
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
await this.handleBudgetOverrideCLI(channel2, msg);
|
|
1596
|
+
} else {
|
|
1597
|
+
await channel2.send(
|
|
1598
|
+
`I've exceeded my daily token budget (${this.tokenBudget.getStatusText()}).
|
|
1599
|
+
|
|
1600
|
+
You can override this:
|
|
1601
|
+
\u2022 /budget override \u2014 allow one more request
|
|
1602
|
+
\u2022 /budget reset \u2014 reset usage to zero
|
|
1603
|
+
\u2022 /budget set <number> \u2014 change daily budget`,
|
|
1604
|
+
msg.channelId
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
this.lifecycle.transition("idle");
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const systemPrompt = this.buildSystemPrompt();
|
|
1612
|
+
const recentMemory = this.shortTerm.getRecent(msg.channelId, 10);
|
|
1613
|
+
const relevantFacts = this.longTerm.search(msg.content, 3);
|
|
1614
|
+
const messages = [];
|
|
1615
|
+
const recentSteps = this.shortTerm.getRecent(msg.channelId, 4);
|
|
1616
|
+
let loopWarning = null;
|
|
1617
|
+
if (recentSteps.length >= 3) {
|
|
1618
|
+
const toolCallPattern = /\[Using: (.+?)\]/g;
|
|
1619
|
+
const toolCalls = [];
|
|
1620
|
+
for (const m of recentSteps) {
|
|
1621
|
+
if (m.role === "assistant") {
|
|
975
1622
|
let match;
|
|
976
1623
|
while ((match = toolCallPattern.exec(m.content)) !== null) {
|
|
977
1624
|
toolCalls.push(match[1]);
|
|
@@ -981,7 +1628,7 @@ You can override this:
|
|
|
981
1628
|
if (toolCalls.length >= 3) {
|
|
982
1629
|
const last3 = toolCalls.slice(-3);
|
|
983
1630
|
if (last3[0] === last3[1] && last3[1] === last3[2]) {
|
|
984
|
-
loopWarning = `[SYSTEM WARNING]
|
|
1631
|
+
loopWarning = `[SYSTEM WARNING] In previous turns you called ${last3[0]} repeatedly. Do NOT call it again. If something failed, explain the failure to the user and suggest alternatives.`;
|
|
985
1632
|
}
|
|
986
1633
|
}
|
|
987
1634
|
}
|
|
@@ -1019,6 +1666,8 @@ You can override this:
|
|
|
1019
1666
|
let lastError = null;
|
|
1020
1667
|
let streamedText = "";
|
|
1021
1668
|
const loopDetector = new ToolCallLoopDetector();
|
|
1669
|
+
const loopAbortController = new AbortController();
|
|
1670
|
+
let loopWarningSent = false;
|
|
1022
1671
|
const canStream = msg.channelType === "cli" || msg.channelType === "telegram" && this.telegramStreaming;
|
|
1023
1672
|
for (const provider of fallbackIterator) {
|
|
1024
1673
|
try {
|
|
@@ -1030,6 +1679,7 @@ You can override this:
|
|
|
1030
1679
|
messages,
|
|
1031
1680
|
tools: this.capabilities.getTools(),
|
|
1032
1681
|
maxSteps: MAX_STEPS,
|
|
1682
|
+
abortSignal: loopAbortController.signal,
|
|
1033
1683
|
onStepFinish: async ({ toolCalls }) => {
|
|
1034
1684
|
if (toolCalls && toolCalls.length > 0) {
|
|
1035
1685
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
@@ -1039,7 +1689,13 @@ You can override this:
|
|
|
1039
1689
|
}
|
|
1040
1690
|
const loop = loopDetector.detect();
|
|
1041
1691
|
if (loop) {
|
|
1042
|
-
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected");
|
|
1692
|
+
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected \u2014 aborting generation");
|
|
1693
|
+
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
1694
|
+
loopWarningSent = true;
|
|
1695
|
+
await channel.send(`\u26A0 Loop detected \u2014 ${loop.tool} called ${loop.count}x in a row. Stopping to save tokens.`, msg.channelId).catch(() => {
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
loopAbortController.abort();
|
|
1043
1699
|
}
|
|
1044
1700
|
await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
|
|
1045
1701
|
});
|
|
@@ -1074,6 +1730,7 @@ You can override this:
|
|
|
1074
1730
|
messages,
|
|
1075
1731
|
tools: this.capabilities.getTools(),
|
|
1076
1732
|
maxSteps: MAX_STEPS,
|
|
1733
|
+
abortSignal: loopAbortController.signal,
|
|
1077
1734
|
onStepFinish: async ({ toolCalls, text }) => {
|
|
1078
1735
|
if (toolCalls && toolCalls.length > 0) {
|
|
1079
1736
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
@@ -1083,7 +1740,13 @@ You can override this:
|
|
|
1083
1740
|
}
|
|
1084
1741
|
const loop = loopDetector.detect();
|
|
1085
1742
|
if (loop) {
|
|
1086
|
-
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected");
|
|
1743
|
+
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected \u2014 aborting generation");
|
|
1744
|
+
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
1745
|
+
loopWarningSent = true;
|
|
1746
|
+
await channel.send(`\u26A0 Loop detected \u2014 ${loop.tool} called ${loop.count}x in a row. Stopping to save tokens.`, msg.channelId).catch(() => {
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
loopAbortController.abort();
|
|
1087
1750
|
}
|
|
1088
1751
|
if (channel && msg.channelType !== "internal") {
|
|
1089
1752
|
await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
|
|
@@ -1097,6 +1760,16 @@ You can override this:
|
|
|
1097
1760
|
this.providers.markSuccess(provider.name);
|
|
1098
1761
|
break;
|
|
1099
1762
|
} catch (err) {
|
|
1763
|
+
if (loopDetector.isAborted()) {
|
|
1764
|
+
logger.info("Generation aborted due to loop detection \u2014 using partial response");
|
|
1765
|
+
if (!result && streamedText) {
|
|
1766
|
+
result = { text: streamedText, usage: void 0 };
|
|
1767
|
+
}
|
|
1768
|
+
if (usedProvider) {
|
|
1769
|
+
this.providers.markSuccess(usedProvider.name);
|
|
1770
|
+
}
|
|
1771
|
+
break;
|
|
1772
|
+
}
|
|
1100
1773
|
lastError = err;
|
|
1101
1774
|
logger.warn({ provider: provider.name, err: err.message }, "Provider failed, trying fallback");
|
|
1102
1775
|
if (channel && msg.channelType !== "internal") {
|
|
@@ -1364,7 +2037,8 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
|
|
|
1364
2037
|
}
|
|
1365
2038
|
}
|
|
1366
2039
|
async handleChatCommand(content, channelType, channelId) {
|
|
1367
|
-
const
|
|
2040
|
+
const trimmed = content.trim();
|
|
2041
|
+
const cmd = trimmed.toLowerCase();
|
|
1368
2042
|
const channel = this.channels.get(channelType);
|
|
1369
2043
|
if (!channel) return false;
|
|
1370
2044
|
const ctx = this.capabilities.getChatCommandContext();
|
|
@@ -1376,42 +2050,211 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
|
|
|
1376
2050
|
if (cmd === "/status") {
|
|
1377
2051
|
const config = ctx.config();
|
|
1378
2052
|
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
2053
|
const lines = [
|
|
1381
2054
|
`**${config.identity.name}** \u2014 Status`,
|
|
1382
2055
|
`Owner: ${config.identity.owner || "(not set)"}`,
|
|
1383
2056
|
`Provider: ${config.providers.default}`,
|
|
1384
2057
|
`Telegram: ${config.channels.telegram.enabled ? "enabled" : "disabled"}`,
|
|
1385
|
-
`Telegram
|
|
2058
|
+
`Telegram access: ${getTelegramAccessSummary(config)}`,
|
|
1386
2059
|
`Budget: ${budget.getStatusText()}`,
|
|
1387
2060
|
`Skills: ${ctx.skillNames().length > 0 ? ctx.skillNames().join(", ") : "none"}`
|
|
1388
2061
|
];
|
|
1389
2062
|
await channel.send(lines.join("\n"), channelId);
|
|
1390
2063
|
return true;
|
|
1391
2064
|
}
|
|
1392
|
-
if (cmd
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
2065
|
+
if (cmd.startsWith("/telegram")) {
|
|
2066
|
+
if (channelType !== "cli") {
|
|
2067
|
+
await channel.send("`/telegram` is only available from the Mercury CLI chat.", channelId);
|
|
2068
|
+
return true;
|
|
2069
|
+
}
|
|
2070
|
+
const config = ctx.config();
|
|
2071
|
+
const rawSubcommand = trimmed.slice("/telegram".length).trim();
|
|
2072
|
+
if (!rawSubcommand && channel instanceof CLIChannel) {
|
|
2073
|
+
await channel.withMenu(async (select) => {
|
|
2074
|
+
await this.openCliTelegramMenu(channel, channelId, select);
|
|
2075
|
+
});
|
|
2076
|
+
return true;
|
|
2077
|
+
}
|
|
2078
|
+
const parts = rawSubcommand.split(/\s+/).filter(Boolean);
|
|
2079
|
+
const action = parts[0]?.toLowerCase() || "help";
|
|
2080
|
+
const formatTelegramUser2 = (user) => {
|
|
2081
|
+
const username = user.username ? ` (@${user.username})` : "";
|
|
2082
|
+
const firstName = user.firstName ? ` ${user.firstName}` : "";
|
|
2083
|
+
const pairingCode = user.pairingCode ? ` [code: ${user.pairingCode}]` : "";
|
|
2084
|
+
return `${user.userId}${username}${firstName}${pairingCode}`;
|
|
2085
|
+
};
|
|
2086
|
+
const sendTelegramOverview = async () => {
|
|
1407
2087
|
const lines = [
|
|
1408
|
-
|
|
2088
|
+
"**Telegram Management**",
|
|
1409
2089
|
"",
|
|
1410
|
-
|
|
2090
|
+
`Access: ${getTelegramAccessSummary(config)}`,
|
|
2091
|
+
`Admins: ${config.channels.telegram.admins.length > 0 ? config.channels.telegram.admins.map(formatTelegramUser2).join(", ") : "none"}`,
|
|
2092
|
+
`Members: ${config.channels.telegram.members.length > 0 ? config.channels.telegram.members.map(formatTelegramUser2).join(", ") : "none"}`,
|
|
2093
|
+
`Pending: ${config.channels.telegram.pending.length > 0 ? config.channels.telegram.pending.map(formatTelegramUser2).join(", ") : "none"}`,
|
|
2094
|
+
"",
|
|
2095
|
+
"Commands:",
|
|
2096
|
+
"\u2022 `/telegram pending`",
|
|
2097
|
+
"\u2022 `/telegram users`",
|
|
2098
|
+
"\u2022 `/telegram approve <pairing-code|user-id>`",
|
|
2099
|
+
"\u2022 `/telegram reject <user-id>`",
|
|
2100
|
+
"\u2022 `/telegram remove <user-id>`",
|
|
2101
|
+
"\u2022 `/telegram promote <user-id>`",
|
|
2102
|
+
"\u2022 `/telegram demote <user-id>`",
|
|
2103
|
+
"\u2022 `/telegram reset`"
|
|
1411
2104
|
];
|
|
1412
2105
|
await channel.send(lines.join("\n"), channelId);
|
|
2106
|
+
};
|
|
2107
|
+
if (action === "help" || action === "status") {
|
|
2108
|
+
await sendTelegramOverview();
|
|
2109
|
+
return true;
|
|
1413
2110
|
}
|
|
1414
|
-
|
|
2111
|
+
if (action === "pending") {
|
|
2112
|
+
const pending = getTelegramPendingRequests(config);
|
|
2113
|
+
const lines = [
|
|
2114
|
+
"**Telegram Pending Requests**",
|
|
2115
|
+
"",
|
|
2116
|
+
pending.length > 0 ? pending.map(formatTelegramUser2).join("\n") : "No pending Telegram requests."
|
|
2117
|
+
];
|
|
2118
|
+
await channel.send(lines.join("\n"), channelId);
|
|
2119
|
+
return true;
|
|
2120
|
+
}
|
|
2121
|
+
if (action === "users") {
|
|
2122
|
+
const approved = getTelegramApprovedUsers(config);
|
|
2123
|
+
const lines = [
|
|
2124
|
+
"**Telegram Approved Users**",
|
|
2125
|
+
"",
|
|
2126
|
+
`Admins: ${config.channels.telegram.admins.length > 0 ? config.channels.telegram.admins.map(formatTelegramUser2).join(", ") : "none"}`,
|
|
2127
|
+
`Members: ${config.channels.telegram.members.length > 0 ? config.channels.telegram.members.map(formatTelegramUser2).join(", ") : "none"}`,
|
|
2128
|
+
"",
|
|
2129
|
+
`Total approved: ${approved.length}`
|
|
2130
|
+
];
|
|
2131
|
+
await channel.send(lines.join("\n"), channelId);
|
|
2132
|
+
return true;
|
|
2133
|
+
}
|
|
2134
|
+
if (action === "approve") {
|
|
2135
|
+
const value = parts[1];
|
|
2136
|
+
if (!value) {
|
|
2137
|
+
await channel.send("Usage: `/telegram approve <pairing-code|user-id>`", channelId);
|
|
2138
|
+
return true;
|
|
2139
|
+
}
|
|
2140
|
+
let approved = approveTelegramPendingRequestByPairingCode(config, value);
|
|
2141
|
+
let resultLabel = value;
|
|
2142
|
+
if (!approved) {
|
|
2143
|
+
const userId = Number(value);
|
|
2144
|
+
if (!isNaN(userId)) {
|
|
2145
|
+
approved = approveTelegramPendingRequest(config, userId, "member");
|
|
2146
|
+
resultLabel = userId.toString();
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
if (!approved) {
|
|
2150
|
+
await channel.send(`No pending Telegram request found for \`${resultLabel}\`.`, channelId);
|
|
2151
|
+
return true;
|
|
2152
|
+
}
|
|
2153
|
+
saveConfig(config);
|
|
2154
|
+
await channel.send(`Approved Telegram user ${formatTelegramUser2(approved)}.`, channelId);
|
|
2155
|
+
return true;
|
|
2156
|
+
}
|
|
2157
|
+
if (action === "reject") {
|
|
2158
|
+
const value = Number(parts[1]);
|
|
2159
|
+
if (isNaN(value)) {
|
|
2160
|
+
await channel.send("Usage: `/telegram reject <user-id>`", channelId);
|
|
2161
|
+
return true;
|
|
2162
|
+
}
|
|
2163
|
+
const rejected = rejectTelegramPendingRequest(config, value);
|
|
2164
|
+
if (!rejected) {
|
|
2165
|
+
await channel.send(`No pending Telegram request found for \`${value}\`.`, channelId);
|
|
2166
|
+
return true;
|
|
2167
|
+
}
|
|
2168
|
+
saveConfig(config);
|
|
2169
|
+
await channel.send(`Rejected Telegram request for ${formatTelegramUser2(rejected)}.`, channelId);
|
|
2170
|
+
return true;
|
|
2171
|
+
}
|
|
2172
|
+
if (action === "remove") {
|
|
2173
|
+
const value = Number(parts[1]);
|
|
2174
|
+
if (isNaN(value)) {
|
|
2175
|
+
await channel.send("Usage: `/telegram remove <user-id>`", channelId);
|
|
2176
|
+
return true;
|
|
2177
|
+
}
|
|
2178
|
+
const removed = removeTelegramUser(config, value);
|
|
2179
|
+
if (!removed) {
|
|
2180
|
+
await channel.send(`No approved Telegram user found for \`${value}\`.`, channelId);
|
|
2181
|
+
return true;
|
|
2182
|
+
}
|
|
2183
|
+
saveConfig(config);
|
|
2184
|
+
await channel.send(`Removed Telegram access for ${formatTelegramUser2(removed)}.`, channelId);
|
|
2185
|
+
return true;
|
|
2186
|
+
}
|
|
2187
|
+
if (action === "promote") {
|
|
2188
|
+
const value = Number(parts[1]);
|
|
2189
|
+
if (isNaN(value)) {
|
|
2190
|
+
await channel.send("Usage: `/telegram promote <user-id>`", channelId);
|
|
2191
|
+
return true;
|
|
2192
|
+
}
|
|
2193
|
+
const promoted = promoteTelegramUserToAdmin(config, value);
|
|
2194
|
+
if (!promoted) {
|
|
2195
|
+
await channel.send(`No Telegram member found for \`${value}\`.`, channelId);
|
|
2196
|
+
return true;
|
|
2197
|
+
}
|
|
2198
|
+
saveConfig(config);
|
|
2199
|
+
await channel.send(`Promoted ${formatTelegramUser2(promoted)} to Telegram admin.`, channelId);
|
|
2200
|
+
return true;
|
|
2201
|
+
}
|
|
2202
|
+
if (action === "demote") {
|
|
2203
|
+
const value = Number(parts[1]);
|
|
2204
|
+
if (isNaN(value)) {
|
|
2205
|
+
await channel.send("Usage: `/telegram demote <user-id>`", channelId);
|
|
2206
|
+
return true;
|
|
2207
|
+
}
|
|
2208
|
+
const demoted = demoteTelegramAdmin(config, value);
|
|
2209
|
+
if (!demoted) {
|
|
2210
|
+
await channel.send("Could not demote that Telegram admin. Mercury must keep at least one admin.", channelId);
|
|
2211
|
+
return true;
|
|
2212
|
+
}
|
|
2213
|
+
saveConfig(config);
|
|
2214
|
+
await channel.send(`Demoted ${formatTelegramUser2(demoted)} to Telegram member.`, channelId);
|
|
2215
|
+
return true;
|
|
2216
|
+
}
|
|
2217
|
+
if (action === "reset" || action === "unpair") {
|
|
2218
|
+
config.channels.telegram.admins = [];
|
|
2219
|
+
config.channels.telegram.members = [];
|
|
2220
|
+
config.channels.telegram.pending = [];
|
|
2221
|
+
saveConfig(config);
|
|
2222
|
+
await channel.send("Telegram access reset. New users can send /start to begin pairing again.", channelId);
|
|
2223
|
+
return true;
|
|
2224
|
+
}
|
|
2225
|
+
await channel.send(
|
|
2226
|
+
`Unknown Telegram command "${action}". Try \`/telegram\`, \`/telegram pending\`, or \`/telegram users\`.`,
|
|
2227
|
+
channelId
|
|
2228
|
+
);
|
|
2229
|
+
return true;
|
|
2230
|
+
}
|
|
2231
|
+
if ((cmd === "/" || cmd === "/menu") && channelType === "cli" && channel instanceof CLIChannel) {
|
|
2232
|
+
await this.openCliCommandMenu(channel, channelId);
|
|
2233
|
+
return true;
|
|
2234
|
+
}
|
|
2235
|
+
if (cmd === "/tools") {
|
|
2236
|
+
const tools = ctx.toolNames();
|
|
2237
|
+
const grouped = [
|
|
2238
|
+
`**${tools.length} tools loaded:**`,
|
|
2239
|
+
"",
|
|
2240
|
+
...tools.sort().map((t) => `\u2022 \`${t}\``)
|
|
2241
|
+
];
|
|
2242
|
+
await channel.send(grouped.join("\n"), channelId);
|
|
2243
|
+
return true;
|
|
2244
|
+
}
|
|
2245
|
+
if (cmd === "/skills") {
|
|
2246
|
+
const names = ctx.skillNames();
|
|
2247
|
+
if (names.length === 0) {
|
|
2248
|
+
await channel.send('No skills installed. Ask me to "install skill from <url>" to add one.', channelId);
|
|
2249
|
+
} else {
|
|
2250
|
+
const lines = [
|
|
2251
|
+
`**${names.length} skill${names.length > 1 ? "s" : ""} installed:**`,
|
|
2252
|
+
"",
|
|
2253
|
+
...names.map((n) => `\u2022 ${n}`)
|
|
2254
|
+
];
|
|
2255
|
+
await channel.send(lines.join("\n"), channelId);
|
|
2256
|
+
}
|
|
2257
|
+
return true;
|
|
1415
2258
|
}
|
|
1416
2259
|
if (cmd === "/stream on") {
|
|
1417
2260
|
this.telegramStreaming = true;
|
|
@@ -1438,6 +2281,205 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
|
|
|
1438
2281
|
}
|
|
1439
2282
|
return false;
|
|
1440
2283
|
}
|
|
2284
|
+
async openCliCommandMenu(channel, channelId) {
|
|
2285
|
+
const ctx = this.capabilities.getChatCommandContext();
|
|
2286
|
+
if (!ctx) return;
|
|
2287
|
+
await channel.withMenu(async (select) => {
|
|
2288
|
+
while (true) {
|
|
2289
|
+
const streamLabel = this.telegramStreaming ? "Disable Telegram Streaming" : "Enable Telegram Streaming";
|
|
2290
|
+
const action = await select("Mercury Commands", [
|
|
2291
|
+
{ value: "status", label: "Status" },
|
|
2292
|
+
{ value: "telegram", label: "Telegram" },
|
|
2293
|
+
{ value: "tools", label: "Tools" },
|
|
2294
|
+
{ value: "skills", label: "Skills" },
|
|
2295
|
+
{ value: "stream", label: streamLabel },
|
|
2296
|
+
{ value: "help", label: "Help" },
|
|
2297
|
+
{ value: "exit", label: "Exit" }
|
|
2298
|
+
]);
|
|
2299
|
+
if (action === "exit") {
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
if (action === "status") {
|
|
2303
|
+
await this.handleChatCommand("/status", "cli", channelId);
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
if (action === "telegram") {
|
|
2307
|
+
await this.openCliTelegramMenu(channel, channelId, select);
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
if (action === "tools") {
|
|
2311
|
+
await this.handleChatCommand("/tools", "cli", channelId);
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
if (action === "skills") {
|
|
2315
|
+
await this.handleChatCommand("/skills", "cli", channelId);
|
|
2316
|
+
continue;
|
|
2317
|
+
}
|
|
2318
|
+
if (action === "stream") {
|
|
2319
|
+
await this.handleChatCommand("/stream", "cli", channelId);
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
if (action === "help") {
|
|
2323
|
+
await channel.send(ctx.manual(), channelId);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
async openCliTelegramMenu(channel, channelId, select) {
|
|
2329
|
+
const ctx = this.capabilities.getChatCommandContext();
|
|
2330
|
+
if (!ctx) return;
|
|
2331
|
+
const formatTelegramUser2 = (user) => {
|
|
2332
|
+
const username = user.username ? ` (@${user.username})` : "";
|
|
2333
|
+
const firstName = user.firstName ? ` ${user.firstName}` : "";
|
|
2334
|
+
const pairingCode = user.pairingCode ? ` [code: ${user.pairingCode}]` : "";
|
|
2335
|
+
return `${user.userId}${username}${firstName}${pairingCode}`;
|
|
2336
|
+
};
|
|
2337
|
+
const selectFromUsers = async (title, users, emptyMessage, backValue = "back") => {
|
|
2338
|
+
if (users.length === 0) {
|
|
2339
|
+
await channel.send(emptyMessage, channelId);
|
|
2340
|
+
return backValue;
|
|
2341
|
+
}
|
|
2342
|
+
return select(title, [
|
|
2343
|
+
...users.map((user) => ({
|
|
2344
|
+
value: user.pairingCode || user.userId.toString(),
|
|
2345
|
+
label: formatTelegramUser2(user)
|
|
2346
|
+
})),
|
|
2347
|
+
{ value: backValue, label: "Back" }
|
|
2348
|
+
]);
|
|
2349
|
+
};
|
|
2350
|
+
while (true) {
|
|
2351
|
+
const config = ctx.config();
|
|
2352
|
+
const action = await select("Telegram Commands", [
|
|
2353
|
+
{ value: "overview", label: "Overview" },
|
|
2354
|
+
{ value: "pending", label: `Pending Requests (${config.channels.telegram.pending.length})` },
|
|
2355
|
+
{ value: "users", label: `Approved Users (${getTelegramApprovedUsers(config).length})` },
|
|
2356
|
+
{ value: "approve", label: "Approve Request" },
|
|
2357
|
+
{ value: "reject", label: "Reject Request" },
|
|
2358
|
+
{ value: "remove", label: "Remove User" },
|
|
2359
|
+
{ value: "promote", label: "Promote to Admin" },
|
|
2360
|
+
{ value: "demote", label: "Demote Admin" },
|
|
2361
|
+
{ value: "reset", label: "Reset Telegram Access" },
|
|
2362
|
+
{ value: "back", label: "Back" },
|
|
2363
|
+
{ value: "exit", label: "Exit" }
|
|
2364
|
+
]);
|
|
2365
|
+
if (action === "exit") {
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (action === "back") {
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
if (action === "overview") {
|
|
2372
|
+
await this.handleChatCommand("/telegram status", "cli", channelId);
|
|
2373
|
+
continue;
|
|
2374
|
+
}
|
|
2375
|
+
if (action === "pending") {
|
|
2376
|
+
await this.handleChatCommand("/telegram pending", "cli", channelId);
|
|
2377
|
+
continue;
|
|
2378
|
+
}
|
|
2379
|
+
if (action === "users") {
|
|
2380
|
+
await this.handleChatCommand("/telegram users", "cli", channelId);
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
if (action === "approve") {
|
|
2384
|
+
const pending = getTelegramPendingRequests(config);
|
|
2385
|
+
const selected = await selectFromUsers(
|
|
2386
|
+
"Approve Telegram Request",
|
|
2387
|
+
pending,
|
|
2388
|
+
"There are no pending Telegram requests to approve."
|
|
2389
|
+
);
|
|
2390
|
+
if (selected === "back") {
|
|
2391
|
+
continue;
|
|
2392
|
+
}
|
|
2393
|
+
await this.handleChatCommand(`/telegram approve ${selected}`, "cli", channelId);
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
if (action === "reject") {
|
|
2397
|
+
const pending = getTelegramPendingRequests(config);
|
|
2398
|
+
const selected = await selectFromUsers(
|
|
2399
|
+
"Reject Telegram Request",
|
|
2400
|
+
pending,
|
|
2401
|
+
"There are no pending Telegram requests to reject."
|
|
2402
|
+
);
|
|
2403
|
+
if (selected === "back") {
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
const request = pending.find((entry) => (entry.pairingCode || entry.userId.toString()) === selected);
|
|
2407
|
+
if (!request) {
|
|
2408
|
+
await channel.send("That Telegram request is no longer pending.", channelId);
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
await this.handleChatCommand(`/telegram reject ${request.userId}`, "cli", channelId);
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
if (action === "remove") {
|
|
2415
|
+
const approved = getTelegramApprovedUsers(config);
|
|
2416
|
+
const selected = await selectFromUsers(
|
|
2417
|
+
"Remove Telegram User",
|
|
2418
|
+
approved,
|
|
2419
|
+
"There are no approved Telegram users to remove."
|
|
2420
|
+
);
|
|
2421
|
+
if (selected === "back") {
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
const user = approved.find((entry) => entry.userId.toString() === selected);
|
|
2425
|
+
if (!user) {
|
|
2426
|
+
await channel.send("That Telegram user is no longer approved.", channelId);
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
await this.handleChatCommand(`/telegram remove ${user.userId}`, "cli", channelId);
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
if (action === "promote") {
|
|
2433
|
+
const members = config.channels.telegram.members;
|
|
2434
|
+
const selected = await selectFromUsers(
|
|
2435
|
+
"Promote Telegram Member",
|
|
2436
|
+
members,
|
|
2437
|
+
"There are no Telegram members available to promote."
|
|
2438
|
+
);
|
|
2439
|
+
if (selected === "back") {
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
const member = members.find((entry) => entry.userId.toString() === selected);
|
|
2443
|
+
if (!member) {
|
|
2444
|
+
await channel.send("That Telegram member is no longer available.", channelId);
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
await this.handleChatCommand(`/telegram promote ${member.userId}`, "cli", channelId);
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
if (action === "demote") {
|
|
2451
|
+
const admins = config.channels.telegram.admins;
|
|
2452
|
+
const selected = await selectFromUsers(
|
|
2453
|
+
"Demote Telegram Admin",
|
|
2454
|
+
admins,
|
|
2455
|
+
"There are no Telegram admins available to demote."
|
|
2456
|
+
);
|
|
2457
|
+
if (selected === "back") {
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
const admin = admins.find((entry) => entry.userId.toString() === selected);
|
|
2461
|
+
if (!admin) {
|
|
2462
|
+
await channel.send("That Telegram admin is no longer available.", channelId);
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2465
|
+
await this.handleChatCommand(`/telegram demote ${admin.userId}`, "cli", channelId);
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
if (action === "reset") {
|
|
2469
|
+
const confirmation = await select("Reset Telegram Access?", [
|
|
2470
|
+
{ value: "cancel", label: "Cancel" },
|
|
2471
|
+
{ value: "confirm", label: "Reset all Telegram access" },
|
|
2472
|
+
{ value: "back", label: "Back" }
|
|
2473
|
+
]);
|
|
2474
|
+
if (confirmation === "confirm") {
|
|
2475
|
+
clearTelegramAccess(config);
|
|
2476
|
+
saveConfig(config);
|
|
2477
|
+
await channel.send("Telegram access reset. New users can send /start to begin pairing again.", channelId);
|
|
2478
|
+
}
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
1441
2483
|
};
|
|
1442
2484
|
|
|
1443
2485
|
// src/core/scheduler.ts
|
|
@@ -1496,437 +2538,117 @@ var Scheduler = class {
|
|
|
1496
2538
|
await this.heartbeatHandler?.();
|
|
1497
2539
|
} catch (err) {
|
|
1498
2540
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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;
|
|
2541
|
+
}
|
|
2542
|
+
}, ms);
|
|
1836
2543
|
}
|
|
1837
|
-
|
|
1838
|
-
this.
|
|
2544
|
+
stopHeartbeat() {
|
|
2545
|
+
if (this.heartbeatTimer) {
|
|
2546
|
+
clearInterval(this.heartbeatTimer);
|
|
2547
|
+
this.heartbeatTimer = null;
|
|
2548
|
+
logger.info("Heartbeat stopped");
|
|
2549
|
+
}
|
|
1839
2550
|
}
|
|
1840
|
-
|
|
1841
|
-
this.
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
this.showPrompt();
|
|
1850
|
-
return;
|
|
2551
|
+
addTask(task) {
|
|
2552
|
+
if (this.tasks.has(task.id)) {
|
|
2553
|
+
this.removeTask(task.id);
|
|
2554
|
+
}
|
|
2555
|
+
const scheduled = cron.schedule(task.cron, async () => {
|
|
2556
|
+
try {
|
|
2557
|
+
await task.handler();
|
|
2558
|
+
} catch (err) {
|
|
2559
|
+
logger.error({ task: task.id, err }, "Scheduled task error");
|
|
1851
2560
|
}
|
|
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
2561
|
});
|
|
1862
|
-
this.
|
|
1863
|
-
|
|
1864
|
-
logger.info("CLI channel started");
|
|
2562
|
+
this.tasks.set(task.id, scheduled);
|
|
2563
|
+
logger.info({ id: task.id, cron: task.cron, desc: task.description }, "Task scheduled");
|
|
1865
2564
|
}
|
|
1866
|
-
|
|
1867
|
-
this.
|
|
1868
|
-
this.
|
|
1869
|
-
|
|
2565
|
+
addPersistedTask(manifest) {
|
|
2566
|
+
this.taskManifests.set(manifest.id, manifest);
|
|
2567
|
+
this.addTask({
|
|
2568
|
+
id: manifest.id,
|
|
2569
|
+
cron: manifest.cron,
|
|
2570
|
+
description: manifest.description,
|
|
2571
|
+
handler: async () => {
|
|
2572
|
+
logger.info({ task: manifest.id }, "Scheduled task firing");
|
|
2573
|
+
if (this.onScheduledTask) {
|
|
2574
|
+
await this.onScheduledTask(manifest);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
});
|
|
1870
2578
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2579
|
+
addDelayedTask(manifest) {
|
|
2580
|
+
this.taskManifests.set(manifest.id, manifest);
|
|
2581
|
+
const delayMs = (manifest.delaySeconds || 60) * 1e3;
|
|
2582
|
+
const timer = setTimeout(async () => {
|
|
2583
|
+
try {
|
|
2584
|
+
logger.info({ task: manifest.id }, "Delayed task firing");
|
|
2585
|
+
if (this.onScheduledTask) {
|
|
2586
|
+
await this.onScheduledTask(manifest);
|
|
2587
|
+
}
|
|
2588
|
+
} catch (err) {
|
|
2589
|
+
logger.error({ task: manifest.id, err }, "Delayed task error");
|
|
2590
|
+
} finally {
|
|
2591
|
+
this.delayedTasks.delete(manifest.id);
|
|
2592
|
+
this.taskManifests.delete(manifest.id);
|
|
2593
|
+
this.persistSchedules();
|
|
2594
|
+
}
|
|
2595
|
+
}, delayMs);
|
|
2596
|
+
this.delayedTasks.set(manifest.id, timer);
|
|
2597
|
+
logger.info({ id: manifest.id, delaySeconds: manifest.delaySeconds }, "Delayed task scheduled");
|
|
1880
2598
|
}
|
|
1881
|
-
|
|
1882
|
-
const
|
|
1883
|
-
if (
|
|
1884
|
-
|
|
1885
|
-
|
|
2599
|
+
removeTask(id) {
|
|
2600
|
+
const task = this.tasks.get(id);
|
|
2601
|
+
if (task) {
|
|
2602
|
+
task.stop();
|
|
2603
|
+
this.tasks.delete(id);
|
|
1886
2604
|
}
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
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;
|
|
2605
|
+
const timer = this.delayedTasks.get(id);
|
|
2606
|
+
if (timer) {
|
|
2607
|
+
clearTimeout(timer);
|
|
2608
|
+
this.delayedTasks.delete(id);
|
|
1903
2609
|
}
|
|
1904
|
-
|
|
1905
|
-
this.showPrompt();
|
|
1906
|
-
return full;
|
|
2610
|
+
this.taskManifests.delete(id);
|
|
1907
2611
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
2612
|
+
getManifests() {
|
|
2613
|
+
return [...this.taskManifests.values()];
|
|
1910
2614
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2615
|
+
restorePersistedTasks() {
|
|
2616
|
+
const persisted = loadSchedules();
|
|
2617
|
+
for (const manifest of persisted) {
|
|
2618
|
+
if (manifest.delaySeconds) {
|
|
2619
|
+
const executeAt = manifest.executeAt ? new Date(manifest.executeAt) : null;
|
|
2620
|
+
const now = Date.now();
|
|
2621
|
+
if (executeAt && executeAt.getTime() > now) {
|
|
2622
|
+
const remainingMs = executeAt.getTime() - now;
|
|
2623
|
+
manifest.delaySeconds = Math.ceil(remainingMs / 1e3);
|
|
2624
|
+
this.addDelayedTask(manifest);
|
|
2625
|
+
} else {
|
|
2626
|
+
logger.info({ id: manifest.id }, "Delayed task already expired, skipping");
|
|
2627
|
+
}
|
|
2628
|
+
} else if (manifest.cron && cron.validate(manifest.cron)) {
|
|
2629
|
+
this.addPersistedTask(manifest);
|
|
2630
|
+
} else {
|
|
2631
|
+
logger.warn({ id: manifest.id, cron: manifest.cron }, "Skipping invalid task");
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
if (persisted.length > 0) {
|
|
2635
|
+
logger.info({ count: persisted.length }, "Restored persisted scheduled tasks");
|
|
1915
2636
|
}
|
|
1916
2637
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
this.rl?.question(question, (answer) => resolve13(answer.trim()));
|
|
1920
|
-
});
|
|
2638
|
+
persistSchedules() {
|
|
2639
|
+
saveSchedules(this.getManifests());
|
|
1921
2640
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
}
|
|
2641
|
+
stopAll() {
|
|
2642
|
+
this.stopHeartbeat();
|
|
2643
|
+
for (const [, task] of this.tasks) {
|
|
2644
|
+
task.stop();
|
|
2645
|
+
}
|
|
2646
|
+
for (const [, timer] of this.delayedTasks) {
|
|
2647
|
+
clearTimeout(timer);
|
|
2648
|
+
}
|
|
2649
|
+
this.tasks.clear();
|
|
2650
|
+
this.delayedTasks.clear();
|
|
2651
|
+
this.taskManifests.clear();
|
|
1930
2652
|
}
|
|
1931
2653
|
};
|
|
1932
2654
|
|
|
@@ -1936,16 +2658,16 @@ import path2 from "path";
|
|
|
1936
2658
|
import { Bot, InputFile, InlineKeyboard } from "grammy";
|
|
1937
2659
|
import { autoRetry } from "@grammyjs/auto-retry";
|
|
1938
2660
|
var MAX_MESSAGE_LENGTH = 4096;
|
|
2661
|
+
var ACCESS_ACTION_PREFIX = "tg_access";
|
|
1939
2662
|
var TelegramChannel = class extends BaseChannel {
|
|
1940
2663
|
constructor(config) {
|
|
1941
2664
|
super();
|
|
1942
2665
|
this.config = config;
|
|
1943
|
-
this.ownerChatId = config.channels.telegram.pairedChatId ?? null;
|
|
1944
2666
|
}
|
|
1945
2667
|
config;
|
|
1946
2668
|
type = "telegram";
|
|
1947
2669
|
bot = null;
|
|
1948
|
-
|
|
2670
|
+
lastActiveChatId = null;
|
|
1949
2671
|
typingInterval = null;
|
|
1950
2672
|
chatCommandContext;
|
|
1951
2673
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
@@ -1963,30 +2685,41 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1963
2685
|
bot.on("message:text", async (ctx) => {
|
|
1964
2686
|
const chatId = ctx.chat.id;
|
|
1965
2687
|
const userId = ctx.from?.id;
|
|
2688
|
+
const username = ctx.from?.username;
|
|
2689
|
+
const firstName = ctx.from?.first_name;
|
|
1966
2690
|
const text = ctx.message.text?.trim() || "";
|
|
2691
|
+
const command = this.getCommandName(text);
|
|
1967
2692
|
if (!userId) return;
|
|
1968
2693
|
if (ctx.chat.type !== "private") {
|
|
1969
2694
|
await this.sendDirectMessage(chatId, "This bot is only available in private one-to-one chats.");
|
|
1970
2695
|
return;
|
|
1971
2696
|
}
|
|
1972
|
-
if (
|
|
1973
|
-
await this.
|
|
2697
|
+
if (command === "/start" || command === "/pair") {
|
|
2698
|
+
await this.handleAccessRequest(userId, chatId, username, firstName);
|
|
1974
2699
|
return;
|
|
1975
2700
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
2701
|
+
const approvedUser = findTelegramApprovedUser(this.config, userId);
|
|
2702
|
+
if (!approvedUser) {
|
|
2703
|
+
const pending = findTelegramPendingRequest(this.config, userId);
|
|
2704
|
+
if (pending) {
|
|
2705
|
+
await this.sendDirectMessage(chatId, this.getPendingStatusMessage());
|
|
2706
|
+
} else {
|
|
2707
|
+
await this.sendDirectMessage(chatId, "This bot is not available to you. Send /start to request access.");
|
|
2708
|
+
}
|
|
1978
2709
|
return;
|
|
1979
2710
|
}
|
|
1980
|
-
this.
|
|
2711
|
+
this.lastActiveChatId = chatId;
|
|
1981
2712
|
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
2713
|
if (command === "/unpair") {
|
|
1988
|
-
this.
|
|
1989
|
-
|
|
2714
|
+
if (!this.isAdminUser(userId)) {
|
|
2715
|
+
await this.sendDirectMessage(chatId, "Only Telegram admins can reset Telegram access.");
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
this.resetAccess();
|
|
2719
|
+
await this.sendDirectMessage(
|
|
2720
|
+
chatId,
|
|
2721
|
+
"Telegram access reset. New users can send /start to request access. The first request must be approved from the Mercury CLI."
|
|
2722
|
+
);
|
|
1990
2723
|
return;
|
|
1991
2724
|
}
|
|
1992
2725
|
const msg = {
|
|
@@ -2003,6 +2736,10 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2003
2736
|
});
|
|
2004
2737
|
bot.on("callback_query:data", async (ctx) => {
|
|
2005
2738
|
const data = ctx.callbackQuery.data;
|
|
2739
|
+
if (data.startsWith(`${ACCESS_ACTION_PREFIX}:`)) {
|
|
2740
|
+
await this.handleAccessCallback(ctx, data);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2006
2743
|
const resolver = this.pendingApprovals.get(data);
|
|
2007
2744
|
if (!resolver) {
|
|
2008
2745
|
await ctx.answerCallbackQuery({ text: "Expired" });
|
|
@@ -2017,19 +2754,33 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2017
2754
|
logger.error({ err: err.message }, "Telegram bot error");
|
|
2018
2755
|
});
|
|
2019
2756
|
this.bot = bot;
|
|
2020
|
-
await
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2757
|
+
await new Promise((resolve13, reject) => {
|
|
2758
|
+
let settled = false;
|
|
2759
|
+
void bot.start({
|
|
2760
|
+
onStart: async (info) => {
|
|
2761
|
+
logger.info({ bot: info.username }, "Telegram bot started \u2014 long polling active");
|
|
2762
|
+
this.ready = true;
|
|
2763
|
+
await this.registerCommands();
|
|
2764
|
+
if (!settled) {
|
|
2765
|
+
settled = true;
|
|
2766
|
+
resolve13();
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
}).catch((err) => {
|
|
2770
|
+
if (!settled) {
|
|
2771
|
+
settled = true;
|
|
2772
|
+
reject(err);
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
logger.error({ err: err.message }, "Telegram bot start loop failed after startup");
|
|
2776
|
+
});
|
|
2026
2777
|
});
|
|
2027
2778
|
}
|
|
2028
2779
|
async registerCommands() {
|
|
2029
2780
|
if (!this.bot) return;
|
|
2030
2781
|
const commands = [
|
|
2031
|
-
{ command: "start", description: "
|
|
2032
|
-
{ command: "pair", description: "
|
|
2782
|
+
{ command: "start", description: "Request Telegram access to this Mercury instance" },
|
|
2783
|
+
{ command: "pair", description: "Request Telegram access to this Mercury instance" },
|
|
2033
2784
|
{ command: "help", description: "Show capabilities and commands manual" },
|
|
2034
2785
|
{ command: "status", description: "Show agent config, budget, and uptime" },
|
|
2035
2786
|
{ command: "tools", description: "List all loaded tools" },
|
|
@@ -2039,7 +2790,7 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2039
2790
|
{ command: "budget_reset", description: "Reset token usage to zero" },
|
|
2040
2791
|
{ command: "budget_set", description: "Set new daily token budget" },
|
|
2041
2792
|
{ command: "stream", description: "Toggle text streaming on/off" },
|
|
2042
|
-
{ command: "unpair", description: "
|
|
2793
|
+
{ command: "unpair", description: "Reset all Telegram access for this Mercury instance" }
|
|
2043
2794
|
];
|
|
2044
2795
|
try {
|
|
2045
2796
|
await this.bot.api.setMyCommands(commands);
|
|
@@ -2054,9 +2805,9 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2054
2805
|
this.stopTypingLoop();
|
|
2055
2806
|
}
|
|
2056
2807
|
async send(content, targetId, elapsedMs) {
|
|
2057
|
-
const
|
|
2058
|
-
if (
|
|
2059
|
-
logger.warn({ targetId,
|
|
2808
|
+
const chatIds = this.resolveTargetChatIds(targetId);
|
|
2809
|
+
if (chatIds.length === 0 || !this.bot) {
|
|
2810
|
+
logger.warn({ targetId, chatIds }, "Telegram send: no valid chat IDs");
|
|
2060
2811
|
return;
|
|
2061
2812
|
}
|
|
2062
2813
|
const timeSuffix = elapsedMs != null ? `
|
|
@@ -2064,69 +2815,79 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2064
2815
|
const fullContent = content + timeSuffix;
|
|
2065
2816
|
const html = mdToTelegram(fullContent);
|
|
2066
2817
|
const chunks = this.splitMessage(html, MAX_MESSAGE_LENGTH);
|
|
2067
|
-
for (const
|
|
2068
|
-
|
|
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");
|
|
2818
|
+
for (const chatId of chatIds) {
|
|
2819
|
+
for (const chunk of chunks) {
|
|
2072
2820
|
try {
|
|
2073
|
-
await this.bot.api.sendMessage(chatId,
|
|
2074
|
-
} catch (
|
|
2075
|
-
logger.
|
|
2821
|
+
await this.bot.api.sendMessage(chatId, chunk, { parse_mode: "HTML" });
|
|
2822
|
+
} catch (err) {
|
|
2823
|
+
logger.warn({ err: err.message, chatId }, "HTML parse failed, sending as plain text");
|
|
2824
|
+
try {
|
|
2825
|
+
await this.bot.api.sendMessage(chatId, this.stripHtml(chunk));
|
|
2826
|
+
} catch (err2) {
|
|
2827
|
+
logger.error({ err: err2.message, chatId }, "Telegram send failed");
|
|
2828
|
+
}
|
|
2076
2829
|
}
|
|
2077
2830
|
}
|
|
2078
2831
|
}
|
|
2079
2832
|
}
|
|
2080
2833
|
async sendFile(filePath, targetId) {
|
|
2081
|
-
const
|
|
2082
|
-
if (
|
|
2083
|
-
logger.warn({ targetId,
|
|
2834
|
+
const chatIds = this.resolveTargetChatIds(targetId);
|
|
2835
|
+
if (chatIds.length === 0 || !this.bot) {
|
|
2836
|
+
logger.warn({ targetId, chatIds }, "Telegram sendFile: no valid chat IDs");
|
|
2084
2837
|
return;
|
|
2085
2838
|
}
|
|
2086
2839
|
const resolved = path2.resolve(filePath);
|
|
2087
2840
|
if (!fs2.existsSync(resolved)) {
|
|
2088
|
-
|
|
2841
|
+
for (const chatId of chatIds) {
|
|
2842
|
+
await this.bot.api.sendMessage(chatId, `File not found: ${filePath}`).catch(() => {
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2089
2845
|
return;
|
|
2090
2846
|
}
|
|
2091
|
-
const inputFile = new InputFile(resolved);
|
|
2092
2847
|
const filename = path2.basename(resolved);
|
|
2093
2848
|
const ext = path2.extname(resolved).toLowerCase();
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2849
|
+
for (const chatId of chatIds) {
|
|
2850
|
+
const inputFile = new InputFile(resolved);
|
|
2851
|
+
try {
|
|
2852
|
+
if (this.isImageFile(ext)) {
|
|
2853
|
+
await this.bot.api.sendPhoto(chatId, inputFile, { caption: filename });
|
|
2854
|
+
} else if (this.isAudioFile(ext)) {
|
|
2855
|
+
await this.bot.api.sendAudio(chatId, inputFile, { title: filename });
|
|
2856
|
+
} else if (this.isVideoFile(ext)) {
|
|
2857
|
+
await this.bot.api.sendVideo(chatId, inputFile, { caption: filename });
|
|
2858
|
+
} else {
|
|
2859
|
+
await this.bot.api.sendDocument(chatId, inputFile, { caption: filename });
|
|
2860
|
+
}
|
|
2861
|
+
logger.info({ file: resolved, chatId }, "File sent via Telegram");
|
|
2862
|
+
} catch (err) {
|
|
2863
|
+
logger.error({ err: err.message, file: resolved, chatId }, "Telegram sendFile failed");
|
|
2864
|
+
await this.bot.api.sendMessage(chatId, `Failed to send file: ${err.message}`).catch(() => {
|
|
2865
|
+
});
|
|
2103
2866
|
}
|
|
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
2867
|
}
|
|
2110
2868
|
}
|
|
2111
2869
|
async stream(content, targetId) {
|
|
2112
|
-
const
|
|
2113
|
-
if (
|
|
2870
|
+
const chatIds = this.resolveTargetChatIds(targetId);
|
|
2871
|
+
if (chatIds.length === 0 || !this.bot) return "";
|
|
2114
2872
|
let full = "";
|
|
2115
2873
|
for await (const chunk of content) {
|
|
2116
2874
|
full += chunk;
|
|
2117
2875
|
}
|
|
2118
2876
|
const html = mdToTelegram(full);
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2877
|
+
for (const chatId of chatIds) {
|
|
2878
|
+
try {
|
|
2879
|
+
await this.bot.api.sendMessage(chatId, html, { parse_mode: "HTML" });
|
|
2880
|
+
} catch (err) {
|
|
2881
|
+
await this.bot.api.sendMessage(chatId, this.stripHtml(html)).catch(() => {
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2123
2884
|
}
|
|
2124
2885
|
return full;
|
|
2125
2886
|
}
|
|
2126
2887
|
async typing(targetId) {
|
|
2127
|
-
const
|
|
2128
|
-
if (
|
|
2129
|
-
await this.bot.api.sendChatAction(
|
|
2888
|
+
const chatIds = this.resolveTargetChatIds(targetId);
|
|
2889
|
+
if (chatIds.length === 0 || !this.bot) return;
|
|
2890
|
+
await this.bot.api.sendChatAction(chatIds[0], "typing");
|
|
2130
2891
|
}
|
|
2131
2892
|
startTypingLoop(chatId) {
|
|
2132
2893
|
this.stopTypingLoop();
|
|
@@ -2200,7 +2961,8 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2200
2961
|
}
|
|
2201
2962
|
}
|
|
2202
2963
|
async askPermission(prompt, targetId) {
|
|
2203
|
-
const
|
|
2964
|
+
const chatIds = this.resolveTargetChatIds(targetId);
|
|
2965
|
+
const chatId = chatIds[0];
|
|
2204
2966
|
if (!chatId || !this.bot) return "no";
|
|
2205
2967
|
const id = `perm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2206
2968
|
const keyboard = new InlineKeyboard().text("Allow", `${id}:yes`).text("Always", `${id}:always`).text("Deny", `${id}:no`);
|
|
@@ -2215,17 +2977,189 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2215
2977
|
reply_markup: keyboard
|
|
2216
2978
|
});
|
|
2217
2979
|
}
|
|
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
|
-
});
|
|
2980
|
+
return new Promise((resolve13) => {
|
|
2981
|
+
this.pendingApprovals.set(`${id}:yes`, () => resolve13("yes"));
|
|
2982
|
+
this.pendingApprovals.set(`${id}:always`, () => resolve13("always"));
|
|
2983
|
+
this.pendingApprovals.set(`${id}:no`, () => resolve13("no"));
|
|
2984
|
+
setTimeout(() => {
|
|
2985
|
+
this.pendingApprovals.delete(`${id}:yes`);
|
|
2986
|
+
this.pendingApprovals.delete(`${id}:always`);
|
|
2987
|
+
this.pendingApprovals.delete(`${id}:no`);
|
|
2988
|
+
resolve13("no");
|
|
2989
|
+
}, 12e4);
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
async handleAccessRequest(userId, chatId, username, firstName) {
|
|
2993
|
+
const approvedUser = findTelegramApprovedUser(this.config, userId);
|
|
2994
|
+
if (approvedUser) {
|
|
2995
|
+
await this.sendDirectMessage(chatId, this.getApprovedStatusMessage(approvedUser));
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
const existingRequest = findTelegramPendingRequest(this.config, userId);
|
|
2999
|
+
if (existingRequest) {
|
|
3000
|
+
await this.sendDirectMessage(chatId, this.getPendingStatusMessage(existingRequest));
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
if (!hasTelegramAdmins(this.config) && this.config.channels.telegram.pending.length > 0) {
|
|
3004
|
+
await this.sendDirectMessage(
|
|
3005
|
+
chatId,
|
|
3006
|
+
"Initial Telegram pairing is already in progress for another user. Ask the Mercury operator to finish setup or reset Telegram access first."
|
|
3007
|
+
);
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
const request = addTelegramPendingRequest(this.config, {
|
|
3011
|
+
userId,
|
|
3012
|
+
chatId,
|
|
3013
|
+
username,
|
|
3014
|
+
firstName,
|
|
3015
|
+
pairingCode: hasTelegramAdmins(this.config) ? void 0 : this.generatePairingCode()
|
|
3016
|
+
});
|
|
3017
|
+
saveConfig(this.config);
|
|
3018
|
+
logger.info({ chatId, userId, username }, "Telegram access request recorded");
|
|
3019
|
+
await this.sendDirectMessage(chatId, this.getPendingStatusMessage(request));
|
|
3020
|
+
if (!hasTelegramAdmins(this.config)) {
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
await this.notifyAdminsOfPendingRequest(request);
|
|
3024
|
+
}
|
|
3025
|
+
async notifyAdminsOfPendingRequest(request) {
|
|
3026
|
+
if (!this.bot) return;
|
|
3027
|
+
const keyboard = new InlineKeyboard().text("Approve", `${ACCESS_ACTION_PREFIX}:approve:${request.userId}`).text("Reject", `${ACCESS_ACTION_PREFIX}:reject:${request.userId}`);
|
|
3028
|
+
const username = request.username ? ` (@${request.username})` : "";
|
|
3029
|
+
const firstName = request.firstName ? ` (${request.firstName})` : "";
|
|
3030
|
+
const message = [
|
|
3031
|
+
"Telegram access request pending approval.",
|
|
3032
|
+
"",
|
|
3033
|
+
`User ID: ${request.userId}${username}${firstName}`,
|
|
3034
|
+
`Requested: ${new Date(request.requestedAt).toLocaleString()}`,
|
|
3035
|
+
"",
|
|
3036
|
+
"Use the buttons below to approve or reject this user."
|
|
3037
|
+
].join("\n");
|
|
3038
|
+
for (const admin of getTelegramAdmins(this.config)) {
|
|
3039
|
+
try {
|
|
3040
|
+
await this.bot.api.sendMessage(admin.chatId, mdToTelegram(message), {
|
|
3041
|
+
parse_mode: "HTML",
|
|
3042
|
+
reply_markup: keyboard
|
|
3043
|
+
});
|
|
3044
|
+
} catch {
|
|
3045
|
+
await this.bot.api.sendMessage(admin.chatId, message, {
|
|
3046
|
+
reply_markup: keyboard
|
|
3047
|
+
}).catch(() => {
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
async handleAccessCallback(ctx, data) {
|
|
3053
|
+
const actorUserId = ctx.from?.id;
|
|
3054
|
+
const actorChatId = ctx.chat?.id;
|
|
3055
|
+
if (!actorUserId || !actorChatId) {
|
|
3056
|
+
await ctx.answerCallbackQuery({ text: "Unavailable" });
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
if (!this.isAdminUser(actorUserId)) {
|
|
3060
|
+
await ctx.answerCallbackQuery({ text: "Admins only" });
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
const [, action, rawUserId] = data.split(":");
|
|
3064
|
+
const requestUserId = Number(rawUserId);
|
|
3065
|
+
if (!requestUserId) {
|
|
3066
|
+
await ctx.answerCallbackQuery({ text: "Invalid request" });
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
const request = findTelegramPendingRequest(this.config, requestUserId);
|
|
3070
|
+
if (!request) {
|
|
3071
|
+
await ctx.answerCallbackQuery({ text: "Already handled" });
|
|
3072
|
+
await ctx.editMessageReplyMarkup({ reply_markup: void 0 }).catch(() => {
|
|
3073
|
+
});
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
if (action === "approve") {
|
|
3077
|
+
const approved = approveTelegramPendingRequest(this.config, requestUserId, "member");
|
|
3078
|
+
if (!approved) {
|
|
3079
|
+
await ctx.answerCallbackQuery({ text: "Already handled" });
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
saveConfig(this.config);
|
|
3083
|
+
await ctx.answerCallbackQuery({ text: "Approved" });
|
|
3084
|
+
await ctx.editMessageReplyMarkup({ reply_markup: void 0 }).catch(() => {
|
|
3085
|
+
});
|
|
3086
|
+
await this.sendDirectMessage(
|
|
3087
|
+
request.chatId,
|
|
3088
|
+
`Telegram access approved. You can now chat with Mercury.
|
|
3089
|
+
|
|
3090
|
+
Telegram access: ${getTelegramAccessSummary(this.config)}`
|
|
3091
|
+
);
|
|
3092
|
+
await this.sendDirectMessage(actorChatId, `Approved Telegram access for ${this.formatRequestLabel(request)}.`);
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
if (action === "reject") {
|
|
3096
|
+
const rejected = rejectTelegramPendingRequest(this.config, requestUserId);
|
|
3097
|
+
if (!rejected) {
|
|
3098
|
+
await ctx.answerCallbackQuery({ text: "Already handled" });
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
saveConfig(this.config);
|
|
3102
|
+
await ctx.answerCallbackQuery({ text: "Rejected" });
|
|
3103
|
+
await ctx.editMessageReplyMarkup({ reply_markup: void 0 }).catch(() => {
|
|
3104
|
+
});
|
|
3105
|
+
await this.sendDirectMessage(
|
|
3106
|
+
request.chatId,
|
|
3107
|
+
"Your Telegram access request was rejected. This bot is not available to you."
|
|
3108
|
+
);
|
|
3109
|
+
await this.sendDirectMessage(actorChatId, `Rejected Telegram access for ${this.formatRequestLabel(request)}.`);
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
await ctx.answerCallbackQuery({ text: "Unknown action" });
|
|
3113
|
+
}
|
|
3114
|
+
resolveTargetChatIds(targetId) {
|
|
3115
|
+
if (!targetId || targetId === "notification") {
|
|
3116
|
+
return getTelegramApprovedChatIds(this.config);
|
|
3117
|
+
}
|
|
3118
|
+
if (targetId.startsWith("telegram:")) {
|
|
3119
|
+
const raw = Number(targetId.split(":")[1]);
|
|
3120
|
+
return isNaN(raw) ? [] : [raw];
|
|
3121
|
+
}
|
|
3122
|
+
const num = Number(targetId);
|
|
3123
|
+
return isNaN(num) ? [] : [num];
|
|
3124
|
+
}
|
|
3125
|
+
isAdminUser(userId) {
|
|
3126
|
+
return !!findTelegramAdmin(this.config, userId);
|
|
3127
|
+
}
|
|
3128
|
+
getCommandName(text) {
|
|
3129
|
+
return text.trim().split(/\s+/)[0]?.toLowerCase() || "";
|
|
3130
|
+
}
|
|
3131
|
+
getPendingStatusMessage(request) {
|
|
3132
|
+
if (!hasTelegramAdmins(this.config)) {
|
|
3133
|
+
const pairingCode = request?.pairingCode ?? "unknown";
|
|
3134
|
+
return [
|
|
3135
|
+
"Your Telegram pairing request has been recorded.",
|
|
3136
|
+
"",
|
|
3137
|
+
`Pairing code: ${pairingCode}`,
|
|
3138
|
+
"",
|
|
3139
|
+
"Enter this code in the Mercury terminal to finish setup."
|
|
3140
|
+
].join("\n");
|
|
3141
|
+
}
|
|
3142
|
+
return "Your Telegram access request has been recorded and is waiting for approval from a Telegram admin.";
|
|
3143
|
+
}
|
|
3144
|
+
getApprovedStatusMessage(user) {
|
|
3145
|
+
const role = this.isAdminUser(user.userId) ? "admin" : "member";
|
|
3146
|
+
return `You are already approved as a Telegram ${role}.
|
|
3147
|
+
|
|
3148
|
+
Telegram access: ${getTelegramAccessSummary(this.config)}`;
|
|
3149
|
+
}
|
|
3150
|
+
formatRequestLabel(request) {
|
|
3151
|
+
const username = request.username ? ` (@${request.username})` : "";
|
|
3152
|
+
const firstName = request.firstName ? ` ${request.firstName}` : "";
|
|
3153
|
+
return `${request.userId}${username}${firstName}`;
|
|
3154
|
+
}
|
|
3155
|
+
resetAccess() {
|
|
3156
|
+
clearTelegramAccess(this.config);
|
|
3157
|
+
saveConfig(this.config);
|
|
3158
|
+
this.lastActiveChatId = null;
|
|
3159
|
+
logger.info("Telegram access reset");
|
|
3160
|
+
}
|
|
3161
|
+
generatePairingCode() {
|
|
3162
|
+
return Math.floor(1e5 + Math.random() * 9e5).toString();
|
|
2229
3163
|
}
|
|
2230
3164
|
escapeHtml(text) {
|
|
2231
3165
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -2259,50 +3193,6 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2259
3193
|
isVideoFile(ext) {
|
|
2260
3194
|
return [".mp4", ".mov", ".avi", ".mkv", ".webm"].includes(ext);
|
|
2261
3195
|
}
|
|
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
3196
|
async sendDirectMessage(chatId, content) {
|
|
2307
3197
|
if (!this.bot) return;
|
|
2308
3198
|
try {
|
|
@@ -3116,7 +4006,7 @@ import { existsSync as existsSync12, statSync as statSync2 } from "fs";
|
|
|
3116
4006
|
import { resolve as resolve9, basename, isAbsolute as isAbsolute7 } from "path";
|
|
3117
4007
|
function createSendFileTool(permissions, getCwd, sendFile) {
|
|
3118
4008
|
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.",
|
|
4009
|
+
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
4010
|
parameters: z7.object({
|
|
3121
4011
|
path: z7.string().describe("Absolute or relative path to the file to send")
|
|
3122
4012
|
}),
|
|
@@ -3154,9 +4044,9 @@ import { tool as tool8 } from "ai";
|
|
|
3154
4044
|
import { z as z8 } from "zod";
|
|
3155
4045
|
function createSendMessageTool(sendMessage) {
|
|
3156
4046
|
return tool8({
|
|
3157
|
-
description: "Send a message
|
|
4047
|
+
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
4048
|
parameters: z8.object({
|
|
3159
|
-
content: z8.string().describe("The message content to send to the
|
|
4049
|
+
content: z8.string().describe("The message content to send to the approved Telegram recipients")
|
|
3160
4050
|
}),
|
|
3161
4051
|
execute: async ({ content }) => {
|
|
3162
4052
|
const trimmed = content.trim();
|
|
@@ -3165,7 +4055,7 @@ function createSendMessageTool(sendMessage) {
|
|
|
3165
4055
|
}
|
|
3166
4056
|
try {
|
|
3167
4057
|
await sendMessage(trimmed);
|
|
3168
|
-
return "Message sent to the
|
|
4058
|
+
return "Message sent to the approved Telegram recipients.";
|
|
3169
4059
|
} catch (err) {
|
|
3170
4060
|
return `Error sending message: ${err.message}`;
|
|
3171
4061
|
}
|
|
@@ -4332,15 +5222,15 @@ Describe what this skill enables Mercury to do. When invoked via the use_skill t
|
|
|
4332
5222
|
};
|
|
4333
5223
|
|
|
4334
5224
|
// src/utils/manual.ts
|
|
4335
|
-
import
|
|
5225
|
+
import chalk4 from "chalk";
|
|
4336
5226
|
function getManual() {
|
|
4337
5227
|
const sections = [];
|
|
4338
5228
|
sections.push("");
|
|
4339
|
-
sections.push(
|
|
4340
|
-
sections.push(
|
|
5229
|
+
sections.push(chalk4.bold.cyan(" MERCURY \u2014 Capabilities & Commands"));
|
|
5230
|
+
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
5231
|
sections.push("");
|
|
4342
|
-
sections.push(
|
|
4343
|
-
sections.push(
|
|
5232
|
+
sections.push(chalk4.bold.white(" Built-in Tools"));
|
|
5233
|
+
sections.push(chalk4.dim(" Tools Mercury can use during conversations."));
|
|
4344
5234
|
sections.push("");
|
|
4345
5235
|
const tools = [
|
|
4346
5236
|
["read_file", "Read file contents", "path (required)"],
|
|
@@ -4349,7 +5239,7 @@ function getManual() {
|
|
|
4349
5239
|
["edit_file", "Replace specific text in a file", "path, old_string, new_string"],
|
|
4350
5240
|
["list_dir", "List directory contents", "path"],
|
|
4351
5241
|
["delete_file", "Delete a file", "path"],
|
|
4352
|
-
["send_message", "Send a message to
|
|
5242
|
+
["send_message", "Send a message to approved Telegram users", "content"],
|
|
4353
5243
|
["run_command", "Execute a shell command", "command"],
|
|
4354
5244
|
["approve_command", "Permanently approve a command type", 'command (e.g. "curl")'],
|
|
4355
5245
|
["fetch_url", "Fetch a URL and return content", "url, format? (text/markdown)"],
|
|
@@ -4368,12 +5258,12 @@ function getManual() {
|
|
|
4368
5258
|
["budget_status", "Check token budget", "\u2014"]
|
|
4369
5259
|
];
|
|
4370
5260
|
for (const [name, desc, params] of tools) {
|
|
4371
|
-
sections.push(` ${
|
|
4372
|
-
sections.push(` ${" ".repeat(24)} ${
|
|
5261
|
+
sections.push(` ${chalk4.cyan(name.padEnd(24))} ${desc}`);
|
|
5262
|
+
sections.push(` ${" ".repeat(24)} ${chalk4.dim(params)}`);
|
|
4373
5263
|
}
|
|
4374
5264
|
sections.push("");
|
|
4375
|
-
sections.push(
|
|
4376
|
-
sections.push(
|
|
5265
|
+
sections.push(chalk4.bold.white(" CLI Commands"));
|
|
5266
|
+
sections.push(chalk4.dim(" Run these from your terminal (no API calls consumed)."));
|
|
4377
5267
|
sections.push("");
|
|
4378
5268
|
const commands = [
|
|
4379
5269
|
["mercury up", "Start persistently (install service + daemon)"],
|
|
@@ -4386,7 +5276,13 @@ function getManual() {
|
|
|
4386
5276
|
["mercury doctor", "Reconfigure settings (Enter keeps current)"],
|
|
4387
5277
|
["mercury setup", "Re-run the setup wizard"],
|
|
4388
5278
|
["mercury status", "Show config and daemon status"],
|
|
4389
|
-
["mercury telegram
|
|
5279
|
+
["mercury telegram list", "Show Telegram admins, members, and pending requests"],
|
|
5280
|
+
["mercury telegram approve <code|id>", "Approve the first Telegram pairing code or a later Telegram request"],
|
|
5281
|
+
["mercury telegram reject <id>", "Reject a pending Telegram request"],
|
|
5282
|
+
["mercury telegram remove <id>", "Remove an approved Telegram user"],
|
|
5283
|
+
["mercury telegram promote <id>", "Promote a Telegram member to admin"],
|
|
5284
|
+
["mercury telegram demote <id>", "Demote a Telegram admin to member"],
|
|
5285
|
+
["mercury telegram unpair", "Reset all Telegram access"],
|
|
4390
5286
|
["mercury help", "Show this manual"],
|
|
4391
5287
|
["mercury service install", "Install as system service (auto-start)"],
|
|
4392
5288
|
["mercury service uninstall", "Uninstall system service"],
|
|
@@ -4394,29 +5290,40 @@ function getManual() {
|
|
|
4394
5290
|
["mercury --verbose", "Start with debug logging on stderr"]
|
|
4395
5291
|
];
|
|
4396
5292
|
for (const [cmd, desc] of commands) {
|
|
4397
|
-
sections.push(` ${
|
|
5293
|
+
sections.push(` ${chalk4.white(cmd.padEnd(26))} ${desc}`);
|
|
4398
5294
|
}
|
|
4399
5295
|
sections.push("");
|
|
4400
|
-
sections.push(
|
|
4401
|
-
sections.push(
|
|
5296
|
+
sections.push(chalk4.bold.white(" In-Chat Commands"));
|
|
5297
|
+
sections.push(chalk4.dim(" Type these during a conversation (no API calls)."));
|
|
4402
5298
|
sections.push("");
|
|
4403
5299
|
const chat = [
|
|
4404
|
-
["/start", "
|
|
4405
|
-
["/pair", "
|
|
5300
|
+
["/start", "Start Telegram pairing or request Telegram access"],
|
|
5301
|
+
["/pair", "Start Telegram pairing or request Telegram access"],
|
|
5302
|
+
["/", "Open the CLI command picker with arrow-key navigation"],
|
|
5303
|
+
["/menu", "Open the CLI command picker with arrow-key navigation"],
|
|
4406
5304
|
["/help", "Show this manual"],
|
|
4407
5305
|
["/status", "Show config and budget info"],
|
|
5306
|
+
["/telegram", "CLI chat only: open the Telegram management menu"],
|
|
5307
|
+
["/telegram pending", "CLI chat only: list pending Telegram requests"],
|
|
5308
|
+
["/telegram users", "CLI chat only: list approved Telegram users"],
|
|
5309
|
+
["/telegram approve <code|id>", "CLI chat only: approve the first pairing code or a later request"],
|
|
5310
|
+
["/telegram reject <id>", "CLI chat only: reject a pending Telegram request"],
|
|
5311
|
+
["/telegram remove <id>", "CLI chat only: remove an approved Telegram user"],
|
|
5312
|
+
["/telegram promote <id>", "CLI chat only: promote a Telegram member to admin"],
|
|
5313
|
+
["/telegram demote <id>", "CLI chat only: demote a Telegram admin to member"],
|
|
5314
|
+
["/telegram reset", "CLI chat only: reset all Telegram access"],
|
|
4408
5315
|
["/tools", "List currently loaded tools"],
|
|
4409
5316
|
["/skills", "List installed skills"],
|
|
4410
5317
|
["/stream", "Toggle text streaming on/off (Telegram)"],
|
|
4411
5318
|
["/stream on", "Enable streaming (live text updates)"],
|
|
4412
5319
|
["/stream off", "Disable streaming (single message)"],
|
|
4413
|
-
["/unpair", "
|
|
5320
|
+
["/unpair", "Reset all Telegram access for this Mercury instance (admins only)"]
|
|
4414
5321
|
];
|
|
4415
5322
|
for (const [cmd, desc] of chat) {
|
|
4416
|
-
sections.push(` ${
|
|
5323
|
+
sections.push(` ${chalk4.white(cmd.padEnd(16))} ${desc}`);
|
|
4417
5324
|
}
|
|
4418
5325
|
sections.push("");
|
|
4419
|
-
sections.push(
|
|
5326
|
+
sections.push(chalk4.bold.white(" Permissions"));
|
|
4420
5327
|
sections.push("");
|
|
4421
5328
|
const perms = [
|
|
4422
5329
|
"Commands are blocked (never run), auto-approved, or need approval.",
|
|
@@ -4425,10 +5332,10 @@ function getManual() {
|
|
|
4425
5332
|
"File access is scoped \u2014 new paths need approval (y/n/always)."
|
|
4426
5333
|
];
|
|
4427
5334
|
for (const p of perms) {
|
|
4428
|
-
sections.push(` ${
|
|
5335
|
+
sections.push(` ${chalk4.dim("\u2022")} ${p}`);
|
|
4429
5336
|
}
|
|
4430
5337
|
sections.push("");
|
|
4431
|
-
sections.push(
|
|
5338
|
+
sections.push(chalk4.bold.white(" Skills"));
|
|
4432
5339
|
sections.push("");
|
|
4433
5340
|
const skillInfo = [
|
|
4434
5341
|
"Skills live in ~/.mercury/skills/<name>/SKILL.md",
|
|
@@ -4437,10 +5344,10 @@ function getManual() {
|
|
|
4437
5344
|
'Schedule: "remind me daily at 9am to run daily-digest skill"'
|
|
4438
5345
|
];
|
|
4439
5346
|
for (const s of skillInfo) {
|
|
4440
|
-
sections.push(` ${
|
|
5347
|
+
sections.push(` ${chalk4.dim("\u2022")} ${s}`);
|
|
4441
5348
|
}
|
|
4442
5349
|
sections.push("");
|
|
4443
|
-
sections.push(
|
|
5350
|
+
sections.push(chalk4.bold.white(" Scheduling"));
|
|
4444
5351
|
sections.push("");
|
|
4445
5352
|
const schedInfo = [
|
|
4446
5353
|
'Recurring: "every day at 9am remind me to\u2026"',
|
|
@@ -4448,10 +5355,10 @@ function getManual() {
|
|
|
4448
5355
|
"Tasks persist to ~/.mercury/schedules.yaml"
|
|
4449
5356
|
];
|
|
4450
5357
|
for (const s of schedInfo) {
|
|
4451
|
-
sections.push(` ${
|
|
5358
|
+
sections.push(` ${chalk4.dim("\u2022")} ${s}`);
|
|
4452
5359
|
}
|
|
4453
5360
|
sections.push("");
|
|
4454
|
-
sections.push(
|
|
5361
|
+
sections.push(chalk4.bold.white(" Configuration"));
|
|
4455
5362
|
sections.push("");
|
|
4456
5363
|
const configInfo = [
|
|
4457
5364
|
["~/.mercury/mercury.yaml", "Main config (providers, channels, budget)"],
|
|
@@ -4463,10 +5370,10 @@ function getManual() {
|
|
|
4463
5370
|
["~/.mercury/memory/", "Short-term, long-term, episodic memory"]
|
|
4464
5371
|
];
|
|
4465
5372
|
for (const [path3, desc] of configInfo) {
|
|
4466
|
-
sections.push(` ${
|
|
5373
|
+
sections.push(` ${chalk4.dim(path3.padEnd(36))} ${desc}`);
|
|
4467
5374
|
}
|
|
4468
5375
|
sections.push("");
|
|
4469
|
-
sections.push(
|
|
5376
|
+
sections.push(chalk4.dim(" mercury.cosmicstack.org"));
|
|
4470
5377
|
sections.push("");
|
|
4471
5378
|
return sections.join("\n");
|
|
4472
5379
|
}
|
|
@@ -4476,7 +5383,7 @@ import { spawn } from "child_process";
|
|
|
4476
5383
|
import { existsSync as existsSync16, readFileSync as readFileSync11, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, mkdirSync as mkdirSync9, openSync } from "fs";
|
|
4477
5384
|
import { join as join9 } from "path";
|
|
4478
5385
|
import process2 from "process";
|
|
4479
|
-
import
|
|
5386
|
+
import chalk5 from "chalk";
|
|
4480
5387
|
var PID_FILE = "daemon.pid";
|
|
4481
5388
|
var LOG_FILE = "daemon.log";
|
|
4482
5389
|
function pidPath() {
|
|
@@ -4512,8 +5419,8 @@ function getDaemonStatus() {
|
|
|
4512
5419
|
function startBackground() {
|
|
4513
5420
|
const status = getDaemonStatus();
|
|
4514
5421
|
if (status.running && status.pid) {
|
|
4515
|
-
console.log(
|
|
4516
|
-
console.log(
|
|
5422
|
+
console.log(chalk5.yellow(` Mercury is already running (PID: ${status.pid})`));
|
|
5423
|
+
console.log(chalk5.dim(` Use \`mercury stop\` to stop it first.`));
|
|
4517
5424
|
console.log("");
|
|
4518
5425
|
process2.exit(1);
|
|
4519
5426
|
}
|
|
@@ -4539,21 +5446,21 @@ function startBackground() {
|
|
|
4539
5446
|
child.unref();
|
|
4540
5447
|
writeFileSync11(pidPath(), String(child.pid));
|
|
4541
5448
|
console.log("");
|
|
4542
|
-
console.log(
|
|
4543
|
-
console.log(
|
|
4544
|
-
console.log(
|
|
4545
|
-
console.log(
|
|
5449
|
+
console.log(chalk5.green(` Mercury started in background (PID: ${child.pid})`));
|
|
5450
|
+
console.log(chalk5.dim(` Logs: ${logFile}`));
|
|
5451
|
+
console.log(chalk5.dim(` Use \`mercury stop\` to stop.`));
|
|
5452
|
+
console.log(chalk5.dim(` Use \`mercury logs\` to view logs.`));
|
|
4546
5453
|
console.log("");
|
|
4547
5454
|
}
|
|
4548
5455
|
function stopDaemon() {
|
|
4549
5456
|
const status = getDaemonStatus();
|
|
4550
5457
|
if (!status.pid) {
|
|
4551
|
-
console.log(
|
|
5458
|
+
console.log(chalk5.yellow(" Mercury is not running as a daemon."));
|
|
4552
5459
|
console.log("");
|
|
4553
5460
|
process2.exit(0);
|
|
4554
5461
|
}
|
|
4555
5462
|
if (!status.running) {
|
|
4556
|
-
console.log(
|
|
5463
|
+
console.log(chalk5.yellow(` Stale PID file found (PID: ${status.pid} is not running). Cleaning up.`));
|
|
4557
5464
|
try {
|
|
4558
5465
|
unlinkSync3(pidPath());
|
|
4559
5466
|
} catch {
|
|
@@ -4567,9 +5474,9 @@ function stopDaemon() {
|
|
|
4567
5474
|
} else {
|
|
4568
5475
|
process2.kill(status.pid, "SIGTERM");
|
|
4569
5476
|
}
|
|
4570
|
-
console.log(
|
|
5477
|
+
console.log(chalk5.green(` Mercury stopped (PID: ${status.pid})`));
|
|
4571
5478
|
} catch {
|
|
4572
|
-
console.log(
|
|
5479
|
+
console.log(chalk5.red(` Failed to stop PID ${status.pid}. You may need to kill it manually.`));
|
|
4573
5480
|
}
|
|
4574
5481
|
try {
|
|
4575
5482
|
unlinkSync3(pidPath());
|
|
@@ -4580,7 +5487,7 @@ function stopDaemon() {
|
|
|
4580
5487
|
function restartDaemon() {
|
|
4581
5488
|
const status = getDaemonStatus();
|
|
4582
5489
|
if (status.running && status.pid) {
|
|
4583
|
-
console.log(
|
|
5490
|
+
console.log(chalk5.yellow(` Stopping Mercury (PID: ${status.pid})...`));
|
|
4584
5491
|
try {
|
|
4585
5492
|
if (process2.platform === "win32") {
|
|
4586
5493
|
process2.kill(status.pid);
|
|
@@ -4593,20 +5500,20 @@ function restartDaemon() {
|
|
|
4593
5500
|
unlinkSync3(pidPath());
|
|
4594
5501
|
} catch {
|
|
4595
5502
|
}
|
|
4596
|
-
console.log(
|
|
5503
|
+
console.log(chalk5.green(" Mercury stopped."));
|
|
4597
5504
|
} else if (status.pid) {
|
|
4598
5505
|
try {
|
|
4599
5506
|
unlinkSync3(pidPath());
|
|
4600
5507
|
} catch {
|
|
4601
5508
|
}
|
|
4602
5509
|
}
|
|
4603
|
-
console.log(
|
|
5510
|
+
console.log(chalk5.yellow(" Starting Mercury..."));
|
|
4604
5511
|
startBackground();
|
|
4605
5512
|
}
|
|
4606
5513
|
function showLogs() {
|
|
4607
5514
|
const logFile = logPath();
|
|
4608
5515
|
if (!existsSync16(logFile)) {
|
|
4609
|
-
console.log(
|
|
5516
|
+
console.log(chalk5.dim(" No daemon log file found."));
|
|
4610
5517
|
console.log("");
|
|
4611
5518
|
return;
|
|
4612
5519
|
}
|
|
@@ -4654,7 +5561,7 @@ function tryAutoDaemonize() {
|
|
|
4654
5561
|
import { existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4 } from "fs";
|
|
4655
5562
|
import { join as join10 } from "path";
|
|
4656
5563
|
import { homedir as homedir3 } from "os";
|
|
4657
|
-
import
|
|
5564
|
+
import chalk6 from "chalk";
|
|
4658
5565
|
import { execSync as execSync8 } from "child_process";
|
|
4659
5566
|
var SERVICE_DESC = "Mercury \u2014 Soul-Driven AI Agent";
|
|
4660
5567
|
var WIN_TASK_NAME = "MercuryAgent";
|
|
@@ -4689,7 +5596,7 @@ function installService() {
|
|
|
4689
5596
|
} else if (platform === "win32") {
|
|
4690
5597
|
installWindows();
|
|
4691
5598
|
} else {
|
|
4692
|
-
console.log(
|
|
5599
|
+
console.log(chalk6.red(` Unsupported platform: ${platform}`));
|
|
4693
5600
|
process.exit(1);
|
|
4694
5601
|
}
|
|
4695
5602
|
}
|
|
@@ -4702,7 +5609,7 @@ function uninstallService() {
|
|
|
4702
5609
|
} else if (platform === "win32") {
|
|
4703
5610
|
uninstallWindows();
|
|
4704
5611
|
} else {
|
|
4705
|
-
console.log(
|
|
5612
|
+
console.log(chalk6.red(` Unsupported platform: ${platform}`));
|
|
4706
5613
|
process.exit(1);
|
|
4707
5614
|
}
|
|
4708
5615
|
}
|
|
@@ -4766,22 +5673,22 @@ function installMac() {
|
|
|
4766
5673
|
try {
|
|
4767
5674
|
execSync8(`launchctl load ${plistPath}`, { stdio: "inherit" });
|
|
4768
5675
|
} catch {
|
|
4769
|
-
console.log(
|
|
4770
|
-
console.log(
|
|
5676
|
+
console.log(chalk6.yellow(" launchctl load failed. Try running:"));
|
|
5677
|
+
console.log(chalk6.dim(` launchctl load ${plistPath}`));
|
|
4771
5678
|
}
|
|
4772
5679
|
console.log("");
|
|
4773
|
-
console.log(
|
|
4774
|
-
console.log(
|
|
4775
|
-
console.log(
|
|
4776
|
-
console.log(
|
|
5680
|
+
console.log(chalk6.green(" Mercury service installed (macOS LaunchAgent)"));
|
|
5681
|
+
console.log(chalk6.dim(` Plist: ${plistPath}`));
|
|
5682
|
+
console.log(chalk6.dim(` Logs: ${logPath2}`));
|
|
5683
|
+
console.log(chalk6.dim(" Auto-starts on login. Auto-restarts on crash."));
|
|
4777
5684
|
console.log("");
|
|
4778
|
-
console.log(
|
|
5685
|
+
console.log(chalk6.dim(" Uninstall: mercury service uninstall"));
|
|
4779
5686
|
console.log("");
|
|
4780
5687
|
}
|
|
4781
5688
|
function uninstallMac() {
|
|
4782
5689
|
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
4783
5690
|
if (!existsSync17(plistPath)) {
|
|
4784
|
-
console.log(
|
|
5691
|
+
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
4785
5692
|
console.log("");
|
|
4786
5693
|
process.exit(0);
|
|
4787
5694
|
}
|
|
@@ -4792,28 +5699,28 @@ function uninstallMac() {
|
|
|
4792
5699
|
try {
|
|
4793
5700
|
unlinkSync4(plistPath);
|
|
4794
5701
|
} catch {
|
|
4795
|
-
console.log(
|
|
4796
|
-
console.log(
|
|
5702
|
+
console.log(chalk6.yellow(" Failed to remove plist file. Remove manually:"));
|
|
5703
|
+
console.log(chalk6.dim(` rm ${plistPath}`));
|
|
4797
5704
|
}
|
|
4798
5705
|
console.log("");
|
|
4799
|
-
console.log(
|
|
5706
|
+
console.log(chalk6.green(" Mercury service uninstalled"));
|
|
4800
5707
|
console.log("");
|
|
4801
5708
|
}
|
|
4802
5709
|
function showMacStatus() {
|
|
4803
5710
|
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
4804
5711
|
if (!existsSync17(plistPath)) {
|
|
4805
|
-
console.log(
|
|
4806
|
-
console.log(
|
|
5712
|
+
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
5713
|
+
console.log(chalk6.dim(" Run `mercury service install` to set it up."));
|
|
4807
5714
|
console.log("");
|
|
4808
5715
|
return;
|
|
4809
5716
|
}
|
|
4810
5717
|
try {
|
|
4811
5718
|
const output = execSync8("launchctl list | grep com.cosmicstack.mercury", { encoding: "utf-8" }).trim();
|
|
4812
|
-
console.log(` ${
|
|
4813
|
-
console.log(
|
|
5719
|
+
console.log(` ${chalk6.green("Service installed and loaded")}`);
|
|
5720
|
+
console.log(chalk6.dim(` ${output}`));
|
|
4814
5721
|
} catch {
|
|
4815
|
-
console.log(` ${
|
|
4816
|
-
console.log(
|
|
5722
|
+
console.log(` ${chalk6.yellow("Service installed but not loaded")}`);
|
|
5723
|
+
console.log(chalk6.dim(` Plist: ${plistPath}`));
|
|
4817
5724
|
}
|
|
4818
5725
|
console.log("");
|
|
4819
5726
|
}
|
|
@@ -4849,30 +5756,30 @@ WantedBy=default.target`;
|
|
|
4849
5756
|
execSync8("systemctl --user enable mercury.service", { stdio: "inherit" });
|
|
4850
5757
|
execSync8("systemctl --user start mercury.service", { stdio: "inherit" });
|
|
4851
5758
|
} catch (err) {
|
|
4852
|
-
console.log(
|
|
4853
|
-
console.log(
|
|
4854
|
-
console.log(
|
|
4855
|
-
console.log(
|
|
5759
|
+
console.log(chalk6.yellow(" systemd commands failed. Try running manually:"));
|
|
5760
|
+
console.log(chalk6.dim(" systemctl --user daemon-reload"));
|
|
5761
|
+
console.log(chalk6.dim(" systemctl --user enable mercury.service"));
|
|
5762
|
+
console.log(chalk6.dim(" systemctl --user start mercury.service"));
|
|
4856
5763
|
}
|
|
4857
5764
|
try {
|
|
4858
5765
|
execSync8(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
|
|
4859
5766
|
} catch {
|
|
4860
|
-
console.log(
|
|
4861
|
-
console.log(
|
|
5767
|
+
console.log(chalk6.yellow(" Enable linger failed (needed for boot-without-login). Try:"));
|
|
5768
|
+
console.log(chalk6.dim(` sudo loginctl enable-linger ${process.env.USER || "$USER"}`));
|
|
4862
5769
|
}
|
|
4863
5770
|
console.log("");
|
|
4864
|
-
console.log(
|
|
4865
|
-
console.log(
|
|
4866
|
-
console.log(
|
|
4867
|
-
console.log(
|
|
5771
|
+
console.log(chalk6.green(" Mercury service installed (systemd --user)"));
|
|
5772
|
+
console.log(chalk6.dim(` Service: ${servicePath}`));
|
|
5773
|
+
console.log(chalk6.dim(` Logs: ${join10(home, "daemon.log")}`));
|
|
5774
|
+
console.log(chalk6.dim(" Auto-starts on login. Auto-restarts on crash (5s delay)."));
|
|
4868
5775
|
console.log("");
|
|
4869
|
-
console.log(
|
|
5776
|
+
console.log(chalk6.dim(" Uninstall: mercury service uninstall"));
|
|
4870
5777
|
console.log("");
|
|
4871
5778
|
}
|
|
4872
5779
|
function uninstallLinux() {
|
|
4873
5780
|
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4874
5781
|
if (!existsSync17(servicePath)) {
|
|
4875
|
-
console.log(
|
|
5782
|
+
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
4876
5783
|
console.log("");
|
|
4877
5784
|
process.exit(0);
|
|
4878
5785
|
}
|
|
@@ -4884,22 +5791,22 @@ function uninstallLinux() {
|
|
|
4884
5791
|
try {
|
|
4885
5792
|
unlinkSync4(servicePath);
|
|
4886
5793
|
} catch {
|
|
4887
|
-
console.log(
|
|
4888
|
-
console.log(
|
|
5794
|
+
console.log(chalk6.yellow(" Failed to remove service file. Remove manually:"));
|
|
5795
|
+
console.log(chalk6.dim(` rm ${servicePath}`));
|
|
4889
5796
|
}
|
|
4890
5797
|
try {
|
|
4891
5798
|
execSync8("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
4892
5799
|
} catch {
|
|
4893
5800
|
}
|
|
4894
5801
|
console.log("");
|
|
4895
|
-
console.log(
|
|
5802
|
+
console.log(chalk6.green(" Mercury service uninstalled"));
|
|
4896
5803
|
console.log("");
|
|
4897
5804
|
}
|
|
4898
5805
|
function showLinuxStatus() {
|
|
4899
5806
|
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4900
5807
|
if (!existsSync17(servicePath)) {
|
|
4901
|
-
console.log(
|
|
4902
|
-
console.log(
|
|
5808
|
+
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
5809
|
+
console.log(chalk6.dim(" Run `mercury service install` to set it up."));
|
|
4903
5810
|
console.log("");
|
|
4904
5811
|
return;
|
|
4905
5812
|
}
|
|
@@ -4907,8 +5814,8 @@ function showLinuxStatus() {
|
|
|
4907
5814
|
const output = execSync8("systemctl --user status mercury.service", { encoding: "utf-8" }).trim();
|
|
4908
5815
|
console.log(output);
|
|
4909
5816
|
} catch (err) {
|
|
4910
|
-
console.log(
|
|
4911
|
-
console.log(
|
|
5817
|
+
console.log(chalk6.yellow(" Could not get service status:"));
|
|
5818
|
+
console.log(chalk6.dim(` ${err.message || err}`));
|
|
4912
5819
|
}
|
|
4913
5820
|
console.log("");
|
|
4914
5821
|
}
|
|
@@ -4924,33 +5831,33 @@ function installWindows() {
|
|
|
4924
5831
|
{ stdio: "inherit", shell: "cmd.exe" }
|
|
4925
5832
|
);
|
|
4926
5833
|
} catch {
|
|
4927
|
-
console.log(
|
|
4928
|
-
console.log(
|
|
5834
|
+
console.log(chalk6.yellow(" schtasks create failed. Try running from an Administrator cmd:"));
|
|
5835
|
+
console.log(chalk6.dim(` schtasks /create /tn "${WIN_TASK_NAME}" /tr "${cmd}" /sc onlogon /rl limited /f`));
|
|
4929
5836
|
}
|
|
4930
5837
|
try {
|
|
4931
5838
|
execSync8(`schtasks /run /tn "${WIN_TASK_NAME}"`, { stdio: "inherit", shell: "cmd.exe" });
|
|
4932
5839
|
} catch {
|
|
4933
|
-
console.log(
|
|
5840
|
+
console.log(chalk6.yellow(" Task created but failed to start immediately. It will start on next login."));
|
|
4934
5841
|
}
|
|
4935
5842
|
console.log("");
|
|
4936
|
-
console.log(
|
|
4937
|
-
console.log(
|
|
4938
|
-
console.log(
|
|
4939
|
-
console.log(
|
|
4940
|
-
console.log(
|
|
5843
|
+
console.log(chalk6.green(" Mercury service installed (Windows Task Scheduler)"));
|
|
5844
|
+
console.log(chalk6.dim(` Task: ${WIN_TASK_NAME}`));
|
|
5845
|
+
console.log(chalk6.dim(` Trigger: on logon`));
|
|
5846
|
+
console.log(chalk6.dim(` Logs: ${logPath2}`));
|
|
5847
|
+
console.log(chalk6.dim(" Auto-starts on login. Use --daemon flag for crash recovery."));
|
|
4941
5848
|
console.log("");
|
|
4942
|
-
console.log(
|
|
5849
|
+
console.log(chalk6.dim(" Uninstall: mercury service uninstall"));
|
|
4943
5850
|
console.log("");
|
|
4944
5851
|
}
|
|
4945
5852
|
function uninstallWindows() {
|
|
4946
5853
|
try {
|
|
4947
5854
|
execSync8(`schtasks /delete /tn "${WIN_TASK_NAME}" /f`, { stdio: "inherit", shell: "cmd.exe" });
|
|
4948
5855
|
console.log("");
|
|
4949
|
-
console.log(
|
|
5856
|
+
console.log(chalk6.green(" Mercury service uninstalled"));
|
|
4950
5857
|
console.log("");
|
|
4951
5858
|
} catch {
|
|
4952
|
-
console.log(
|
|
4953
|
-
console.log(
|
|
5859
|
+
console.log(chalk6.yellow(" Task not found or failed to delete. Remove manually:"));
|
|
5860
|
+
console.log(chalk6.dim(` schtasks /delete /tn "${WIN_TASK_NAME}" /f`));
|
|
4954
5861
|
console.log("");
|
|
4955
5862
|
}
|
|
4956
5863
|
}
|
|
@@ -4963,8 +5870,8 @@ function showWindowsStatus() {
|
|
|
4963
5870
|
console.log(output);
|
|
4964
5871
|
console.log("");
|
|
4965
5872
|
} catch {
|
|
4966
|
-
console.log(
|
|
4967
|
-
console.log(
|
|
5873
|
+
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
5874
|
+
console.log(chalk6.dim(" Run `mercury service install` to set it up."));
|
|
4968
5875
|
console.log("");
|
|
4969
5876
|
}
|
|
4970
5877
|
}
|
|
@@ -4999,11 +5906,208 @@ function sleep(ms) {
|
|
|
4999
5906
|
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
5000
5907
|
}
|
|
5001
5908
|
|
|
5909
|
+
// src/utils/provider-models.ts
|
|
5910
|
+
var MAX_MODEL_OPTIONS = 7;
|
|
5911
|
+
var OPENAI_PREFERRED_MODELS = [
|
|
5912
|
+
"gpt-5.2",
|
|
5913
|
+
"gpt-5.2-chat-latest",
|
|
5914
|
+
"gpt-5.2-pro",
|
|
5915
|
+
"gpt-5-mini",
|
|
5916
|
+
"gpt-5-nano",
|
|
5917
|
+
"gpt-4.1",
|
|
5918
|
+
"gpt-4.1-mini",
|
|
5919
|
+
"gpt-oss-120b",
|
|
5920
|
+
"gpt-oss-20b"
|
|
5921
|
+
];
|
|
5922
|
+
var ANTHROPIC_PREFERRED_MODELS = [
|
|
5923
|
+
"claude-sonnet-4-20250514",
|
|
5924
|
+
"claude-opus-4-20250514",
|
|
5925
|
+
"claude-3-7-sonnet-latest",
|
|
5926
|
+
"claude-3-5-sonnet-latest",
|
|
5927
|
+
"claude-3-5-haiku-latest"
|
|
5928
|
+
];
|
|
5929
|
+
var DEEPSEEK_PREFERRED_MODELS = [
|
|
5930
|
+
"deepseek-chat",
|
|
5931
|
+
"deepseek-reasoner"
|
|
5932
|
+
];
|
|
5933
|
+
var GROK_PREFERRED_MODELS = [
|
|
5934
|
+
"grok-4",
|
|
5935
|
+
"grok-4-latest",
|
|
5936
|
+
"grok-4.20",
|
|
5937
|
+
"grok-3",
|
|
5938
|
+
"grok-3-latest"
|
|
5939
|
+
];
|
|
5940
|
+
var OLLAMA_CLOUD_PREFERRED_MODELS = [
|
|
5941
|
+
"gpt-oss:120b",
|
|
5942
|
+
"gpt-oss:120b-cloud",
|
|
5943
|
+
"gpt-oss:20b"
|
|
5944
|
+
];
|
|
5945
|
+
var OLLAMA_LOCAL_PREFERRED_MODELS = [
|
|
5946
|
+
"gpt-oss:20b",
|
|
5947
|
+
"gpt-oss:120b"
|
|
5948
|
+
];
|
|
5949
|
+
var ProviderModelFetchError = class extends Error {
|
|
5950
|
+
constructor(message) {
|
|
5951
|
+
super(message);
|
|
5952
|
+
this.name = "ProviderModelFetchError";
|
|
5953
|
+
}
|
|
5954
|
+
};
|
|
5955
|
+
function trimTrailingSlash(value) {
|
|
5956
|
+
return value.replace(/\/+$/, "");
|
|
5957
|
+
}
|
|
5958
|
+
async function fetchJson(url, init, invalidMessage) {
|
|
5959
|
+
let response;
|
|
5960
|
+
try {
|
|
5961
|
+
response = await fetch(url, {
|
|
5962
|
+
...init,
|
|
5963
|
+
signal: AbortSignal.timeout(1e4)
|
|
5964
|
+
});
|
|
5965
|
+
} catch {
|
|
5966
|
+
throw new ProviderModelFetchError(invalidMessage);
|
|
5967
|
+
}
|
|
5968
|
+
if (!response.ok) {
|
|
5969
|
+
throw new ProviderModelFetchError(invalidMessage);
|
|
5970
|
+
}
|
|
5971
|
+
try {
|
|
5972
|
+
return await response.json();
|
|
5973
|
+
} catch {
|
|
5974
|
+
throw new ProviderModelFetchError("Mercury could not read the model list returned by this provider.");
|
|
5975
|
+
}
|
|
5976
|
+
}
|
|
5977
|
+
function uniq(values) {
|
|
5978
|
+
return [...new Set(values.filter(Boolean))];
|
|
5979
|
+
}
|
|
5980
|
+
function prioritizeModels(models, preferred) {
|
|
5981
|
+
const preferredSet = new Set(preferred);
|
|
5982
|
+
const preferredMatches = preferred.filter((model) => models.includes(model));
|
|
5983
|
+
const others = models.filter((model) => !preferredSet.has(model)).sort((a, b) => a.localeCompare(b));
|
|
5984
|
+
return [...preferredMatches, ...others];
|
|
5985
|
+
}
|
|
5986
|
+
function limitModels(models) {
|
|
5987
|
+
return models.slice(0, MAX_MODEL_OPTIONS);
|
|
5988
|
+
}
|
|
5989
|
+
function isOpenAIChatModel(id) {
|
|
5990
|
+
const lower = id.toLowerCase();
|
|
5991
|
+
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")) {
|
|
5992
|
+
return false;
|
|
5993
|
+
}
|
|
5994
|
+
return lower.startsWith("gpt-") || /^o\d/.test(lower);
|
|
5995
|
+
}
|
|
5996
|
+
function chooseRecommendedModel(provider, models, currentModel) {
|
|
5997
|
+
const preferredByProvider = {
|
|
5998
|
+
deepseek: DEEPSEEK_PREFERRED_MODELS,
|
|
5999
|
+
openai: OPENAI_PREFERRED_MODELS,
|
|
6000
|
+
anthropic: ANTHROPIC_PREFERRED_MODELS,
|
|
6001
|
+
grok: GROK_PREFERRED_MODELS,
|
|
6002
|
+
ollamaCloud: OLLAMA_CLOUD_PREFERRED_MODELS,
|
|
6003
|
+
ollamaLocal: OLLAMA_LOCAL_PREFERRED_MODELS
|
|
6004
|
+
};
|
|
6005
|
+
for (const candidate of preferredByProvider[provider]) {
|
|
6006
|
+
if (models.includes(candidate)) {
|
|
6007
|
+
return candidate;
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
if (currentModel && models.includes(currentModel)) {
|
|
6011
|
+
return currentModel;
|
|
6012
|
+
}
|
|
6013
|
+
return models[0];
|
|
6014
|
+
}
|
|
6015
|
+
function buildModelCatalog(provider, models, currentModel) {
|
|
6016
|
+
const filtered = uniq(models);
|
|
6017
|
+
if (filtered.length === 0) {
|
|
6018
|
+
throw new ProviderModelFetchError("Mercury could not find any supported chat models for this provider.");
|
|
6019
|
+
}
|
|
6020
|
+
const recommendedModel = chooseRecommendedModel(provider, filtered, currentModel);
|
|
6021
|
+
const preferredByProvider = {
|
|
6022
|
+
deepseek: DEEPSEEK_PREFERRED_MODELS,
|
|
6023
|
+
openai: OPENAI_PREFERRED_MODELS,
|
|
6024
|
+
anthropic: ANTHROPIC_PREFERRED_MODELS,
|
|
6025
|
+
grok: GROK_PREFERRED_MODELS,
|
|
6026
|
+
ollamaCloud: OLLAMA_CLOUD_PREFERRED_MODELS,
|
|
6027
|
+
ollamaLocal: OLLAMA_LOCAL_PREFERRED_MODELS
|
|
6028
|
+
};
|
|
6029
|
+
const withoutRecommended = filtered.filter((model) => model !== recommendedModel);
|
|
6030
|
+
const prioritized = prioritizeModels(withoutRecommended, preferredByProvider[provider]);
|
|
6031
|
+
return {
|
|
6032
|
+
recommendedModel,
|
|
6033
|
+
models: limitModels(prioritized)
|
|
6034
|
+
};
|
|
6035
|
+
}
|
|
6036
|
+
async function fetchOpenAICompatModels(provider, config) {
|
|
6037
|
+
const data = await fetchJson(
|
|
6038
|
+
`${trimTrailingSlash(config.baseUrl)}/models`,
|
|
6039
|
+
{
|
|
6040
|
+
headers: {
|
|
6041
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
6042
|
+
}
|
|
6043
|
+
},
|
|
6044
|
+
`Mercury could not fetch models for this ${provider === "grok" ? "Grok" : provider === "deepseek" ? "DeepSeek" : "OpenAI"} key. Please re-enter it.`
|
|
6045
|
+
);
|
|
6046
|
+
const ids = (data.data ?? []).map((model) => model.id?.trim() ?? "").filter((id) => {
|
|
6047
|
+
if (provider === "deepseek") {
|
|
6048
|
+
return id.startsWith("deepseek-");
|
|
6049
|
+
}
|
|
6050
|
+
return isOpenAIChatModel(id);
|
|
6051
|
+
});
|
|
6052
|
+
return buildModelCatalog(provider, ids, config.model);
|
|
6053
|
+
}
|
|
6054
|
+
async function fetchAnthropicModels(config) {
|
|
6055
|
+
const data = await fetchJson(
|
|
6056
|
+
"https://api.anthropic.com/v1/models",
|
|
6057
|
+
{
|
|
6058
|
+
headers: {
|
|
6059
|
+
"x-api-key": config.apiKey,
|
|
6060
|
+
"anthropic-version": "2023-06-01"
|
|
6061
|
+
}
|
|
6062
|
+
},
|
|
6063
|
+
"Mercury could not fetch models for this Anthropic key. Please re-enter it."
|
|
6064
|
+
);
|
|
6065
|
+
const ids = (data.data ?? []).map((model) => model.id?.trim() ?? "").filter((id) => id.startsWith("claude-"));
|
|
6066
|
+
return buildModelCatalog("anthropic", ids, config.model);
|
|
6067
|
+
}
|
|
6068
|
+
async function fetchGrokModels(config) {
|
|
6069
|
+
const data = await fetchJson(
|
|
6070
|
+
`${trimTrailingSlash(config.baseUrl)}/language-models`,
|
|
6071
|
+
{
|
|
6072
|
+
headers: {
|
|
6073
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
6074
|
+
}
|
|
6075
|
+
},
|
|
6076
|
+
"Mercury could not fetch models for this Grok key. Please re-enter it."
|
|
6077
|
+
);
|
|
6078
|
+
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-"));
|
|
6079
|
+
return buildModelCatalog("grok", ids, config.model);
|
|
6080
|
+
}
|
|
6081
|
+
async function fetchOllamaModels(provider, config) {
|
|
6082
|
+
const headers = config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : void 0;
|
|
6083
|
+
const data = await fetchJson(
|
|
6084
|
+
`${trimTrailingSlash(config.baseUrl)}/tags`,
|
|
6085
|
+
{
|
|
6086
|
+
headers
|
|
6087
|
+
},
|
|
6088
|
+
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."
|
|
6089
|
+
);
|
|
6090
|
+
const ids = (data.models ?? []).map((model) => model.model?.trim() || model.name?.trim() || "").filter(Boolean);
|
|
6091
|
+
return buildModelCatalog(provider, ids, config.model);
|
|
6092
|
+
}
|
|
6093
|
+
async function fetchProviderModelCatalog(provider, config) {
|
|
6094
|
+
if (provider === "anthropic") {
|
|
6095
|
+
return fetchAnthropicModels(config);
|
|
6096
|
+
}
|
|
6097
|
+
if (provider === "grok") {
|
|
6098
|
+
return fetchGrokModels(config);
|
|
6099
|
+
}
|
|
6100
|
+
if (provider === "ollamaCloud" || provider === "ollamaLocal") {
|
|
6101
|
+
return fetchOllamaModels(provider, config);
|
|
6102
|
+
}
|
|
6103
|
+
return fetchOpenAICompatModels(provider, config);
|
|
6104
|
+
}
|
|
6105
|
+
|
|
5002
6106
|
// src/index.ts
|
|
5003
6107
|
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
5004
6108
|
var pkgVersion = JSON.parse(readFileSync12(join11(__dirname, "..", "package.json"), "utf8")).version;
|
|
5005
6109
|
function hr() {
|
|
5006
|
-
console.log(
|
|
6110
|
+
console.log(chalk7.dim("\u2500".repeat(50)));
|
|
5007
6111
|
}
|
|
5008
6112
|
var MERCURY_ASCII = [
|
|
5009
6113
|
" __ _____________ ________ ________ __",
|
|
@@ -5015,26 +6119,26 @@ var MERCURY_ASCII = [
|
|
|
5015
6119
|
function banner() {
|
|
5016
6120
|
console.log("");
|
|
5017
6121
|
for (const line of MERCURY_ASCII) {
|
|
5018
|
-
console.log(
|
|
6122
|
+
console.log(chalk7.bold.cyan(` ${line}`));
|
|
5019
6123
|
}
|
|
5020
6124
|
console.log("");
|
|
5021
|
-
console.log(
|
|
5022
|
-
console.log(
|
|
6125
|
+
console.log(chalk7.white(" an AI agent for personal tasks"));
|
|
6126
|
+
console.log(chalk7.dim(` v${pkgVersion} \xB7 by Cosmic Stack \xB7 mercury.cosmicstack.org`));
|
|
5023
6127
|
console.log("");
|
|
5024
6128
|
}
|
|
5025
6129
|
function splashScreen() {
|
|
5026
6130
|
console.log("");
|
|
5027
6131
|
for (const line of MERCURY_ASCII) {
|
|
5028
|
-
console.log(
|
|
6132
|
+
console.log(chalk7.bold.cyan(` ${line}`));
|
|
5029
6133
|
}
|
|
5030
6134
|
console.log("");
|
|
5031
|
-
console.log(
|
|
5032
|
-
console.log(
|
|
5033
|
-
console.log(
|
|
6135
|
+
console.log(chalk7.dim(" an AI agent for personal tasks"));
|
|
6136
|
+
console.log(chalk7.cyan(" by Cosmic Stack"));
|
|
6137
|
+
console.log(chalk7.dim(" mercury.cosmicstack.org"));
|
|
5034
6138
|
console.log("");
|
|
5035
6139
|
}
|
|
5036
6140
|
async function ask(prompt) {
|
|
5037
|
-
const rl =
|
|
6141
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
5038
6142
|
return new Promise((resolve13) => {
|
|
5039
6143
|
rl.question(prompt, (answer) => {
|
|
5040
6144
|
rl.close();
|
|
@@ -5083,14 +6187,14 @@ async function chooseProvidersToConfigure(config, isReconfig) {
|
|
|
5083
6187
|
for (let i = 0; i < PROVIDER_OPTIONS.length; i++) {
|
|
5084
6188
|
const option = PROVIDER_OPTIONS[i];
|
|
5085
6189
|
const status = configured.includes(option.key) ? " (configured)" : "";
|
|
5086
|
-
console.log(
|
|
6190
|
+
console.log(chalk7.white(` ${i + 1}. ${option.label}${status}`));
|
|
5087
6191
|
}
|
|
5088
6192
|
console.log("");
|
|
5089
|
-
const prompt = isReconfig ?
|
|
6193
|
+
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
6194
|
const input = await ask(prompt);
|
|
5091
6195
|
const parsed = parseProviderSelection(input);
|
|
5092
6196
|
if (parsed === null) {
|
|
5093
|
-
console.log(
|
|
6197
|
+
console.log(chalk7.red(" Please choose valid provider numbers, like `1` or `1,3,5`."));
|
|
5094
6198
|
console.log("");
|
|
5095
6199
|
continue;
|
|
5096
6200
|
}
|
|
@@ -5106,23 +6210,23 @@ async function chooseDefaultProvider(config) {
|
|
|
5106
6210
|
}
|
|
5107
6211
|
if (configured.length === 1) {
|
|
5108
6212
|
config.providers.default = configured[0];
|
|
5109
|
-
console.log(
|
|
6213
|
+
console.log(chalk7.dim(` Default provider set to ${getProviderLabel(configured[0])}`));
|
|
5110
6214
|
return;
|
|
5111
6215
|
}
|
|
5112
6216
|
const suggested = configured.includes("deepseek") ? "deepseek" : configured[0];
|
|
5113
6217
|
console.log("");
|
|
5114
|
-
console.log(
|
|
5115
|
-
console.log(
|
|
6218
|
+
console.log(chalk7.bold.white(" Default Provider"));
|
|
6219
|
+
console.log(chalk7.dim(" Select the LLM provider Mercury should use first."));
|
|
5116
6220
|
console.log("");
|
|
5117
6221
|
for (let i = 0; i < configured.length; i++) {
|
|
5118
6222
|
const provider = configured[i];
|
|
5119
6223
|
const recommended = provider === suggested ? " (recommended)" : "";
|
|
5120
6224
|
const current = provider === config.providers.default ? " (current)" : "";
|
|
5121
|
-
console.log(
|
|
6225
|
+
console.log(chalk7.white(` ${i + 1}. ${getProviderLabel(provider)}${recommended}${current}`));
|
|
5122
6226
|
}
|
|
5123
6227
|
console.log("");
|
|
5124
6228
|
while (true) {
|
|
5125
|
-
const choice = await ask(
|
|
6229
|
+
const choice = await ask(chalk7.white(` Choose [1-${configured.length}] [Enter for ${getProviderLabel(suggested)}]: `));
|
|
5126
6230
|
if (!choice) {
|
|
5127
6231
|
config.providers.default = suggested;
|
|
5128
6232
|
return;
|
|
@@ -5132,7 +6236,7 @@ async function chooseDefaultProvider(config) {
|
|
|
5132
6236
|
config.providers.default = configured[num - 1];
|
|
5133
6237
|
return;
|
|
5134
6238
|
}
|
|
5135
|
-
console.log(
|
|
6239
|
+
console.log(chalk7.red(" Please choose a valid number from the list above."));
|
|
5136
6240
|
}
|
|
5137
6241
|
}
|
|
5138
6242
|
function looksLikeToken(value, minLength = 20) {
|
|
@@ -5172,18 +6276,117 @@ function validateModelName(value) {
|
|
|
5172
6276
|
if (/\s/.test(value)) return "Model name cannot contain spaces.";
|
|
5173
6277
|
return null;
|
|
5174
6278
|
}
|
|
6279
|
+
async function chooseProviderModel(providerLabel, recommendedModel, models) {
|
|
6280
|
+
const selection = await selectWithArrowKeys(
|
|
6281
|
+
`${providerLabel} Models`,
|
|
6282
|
+
[
|
|
6283
|
+
{
|
|
6284
|
+
value: "__default__",
|
|
6285
|
+
label: `Use provider default (${recommendedModel})`
|
|
6286
|
+
},
|
|
6287
|
+
...models.map((model) => ({
|
|
6288
|
+
value: model,
|
|
6289
|
+
label: model
|
|
6290
|
+
})),
|
|
6291
|
+
{
|
|
6292
|
+
value: "__custom__",
|
|
6293
|
+
label: "Enter a custom model name"
|
|
6294
|
+
}
|
|
6295
|
+
]
|
|
6296
|
+
);
|
|
6297
|
+
if (!selection || selection === "__default__") {
|
|
6298
|
+
return recommendedModel;
|
|
6299
|
+
}
|
|
6300
|
+
if (selection !== "__custom__") {
|
|
6301
|
+
return selection;
|
|
6302
|
+
}
|
|
6303
|
+
while (true) {
|
|
6304
|
+
const customModel = await ask(chalk7.white(` ${providerLabel} model [Enter or "none" for ${recommendedModel}]: `));
|
|
6305
|
+
if (!customModel || customModel.toLowerCase() === "none") {
|
|
6306
|
+
return recommendedModel;
|
|
6307
|
+
}
|
|
6308
|
+
const error = validateModelName(customModel);
|
|
6309
|
+
if (!error) {
|
|
6310
|
+
return customModel;
|
|
6311
|
+
}
|
|
6312
|
+
console.log(chalk7.red(` ${error}`));
|
|
6313
|
+
}
|
|
6314
|
+
}
|
|
6315
|
+
async function promptApiKeyWithModelSelection(config, provider, providerLabel, prompt, isReconfig) {
|
|
6316
|
+
const existingConfig = config.providers[provider];
|
|
6317
|
+
while (true) {
|
|
6318
|
+
const value = await ask(prompt);
|
|
6319
|
+
if (!value) {
|
|
6320
|
+
if (isReconfig && existingConfig.apiKey) {
|
|
6321
|
+
return {
|
|
6322
|
+
apiKey: existingConfig.apiKey,
|
|
6323
|
+
model: existingConfig.model,
|
|
6324
|
+
skipped: true
|
|
6325
|
+
};
|
|
6326
|
+
}
|
|
6327
|
+
return { skipped: true };
|
|
6328
|
+
}
|
|
6329
|
+
const formatError = validateApiKey(provider, value);
|
|
6330
|
+
if (formatError) {
|
|
6331
|
+
console.log(chalk7.red(` ${formatError}`));
|
|
6332
|
+
continue;
|
|
6333
|
+
}
|
|
6334
|
+
console.log(chalk7.dim(` Validating ${providerLabel} and fetching models...`));
|
|
6335
|
+
try {
|
|
6336
|
+
const catalog = await fetchProviderModelCatalog(provider, {
|
|
6337
|
+
...existingConfig,
|
|
6338
|
+
apiKey: value
|
|
6339
|
+
});
|
|
6340
|
+
const model = await chooseProviderModel(
|
|
6341
|
+
providerLabel,
|
|
6342
|
+
catalog.recommendedModel,
|
|
6343
|
+
catalog.models
|
|
6344
|
+
);
|
|
6345
|
+
return { apiKey: value, model, skipped: false };
|
|
6346
|
+
} catch (error) {
|
|
6347
|
+
const message = error instanceof ProviderModelFetchError ? error.message : `Mercury could not fetch models for ${providerLabel}. Please re-enter the key.`;
|
|
6348
|
+
console.log(chalk7.red(` ${message}`));
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
}
|
|
6352
|
+
async function promptOllamaLocalModelSelection(config) {
|
|
6353
|
+
const existingConfig = config.providers.ollamaLocal;
|
|
6354
|
+
while (true) {
|
|
6355
|
+
const baseUrl = await promptValidatedValue(
|
|
6356
|
+
chalk7.white(` Ollama Local base URL [${existingConfig.baseUrl}]: `),
|
|
6357
|
+
validateBaseUrl,
|
|
6358
|
+
existingConfig.baseUrl
|
|
6359
|
+
);
|
|
6360
|
+
console.log(chalk7.dim(" Fetching Ollama Local models..."));
|
|
6361
|
+
try {
|
|
6362
|
+
const catalog = await fetchProviderModelCatalog("ollamaLocal", {
|
|
6363
|
+
...existingConfig,
|
|
6364
|
+
baseUrl
|
|
6365
|
+
});
|
|
6366
|
+
const model = await chooseProviderModel(
|
|
6367
|
+
"Ollama Local",
|
|
6368
|
+
catalog.recommendedModel,
|
|
6369
|
+
catalog.models
|
|
6370
|
+
);
|
|
6371
|
+
return { baseUrl, model, skipped: false };
|
|
6372
|
+
} catch (error) {
|
|
6373
|
+
const message = error instanceof ProviderModelFetchError ? error.message : "Mercury could not fetch Ollama Local models. Please check the base URL and try again.";
|
|
6374
|
+
console.log(chalk7.red(` ${message}`));
|
|
6375
|
+
}
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
5175
6378
|
async function promptValidatedValue(prompt, validator, existingValue, options) {
|
|
5176
6379
|
while (true) {
|
|
5177
6380
|
const value = await ask(prompt);
|
|
5178
6381
|
if (!value) {
|
|
5179
6382
|
if (existingValue) return existingValue;
|
|
5180
6383
|
if (options?.allowSkip) return void 0;
|
|
5181
|
-
console.log(
|
|
6384
|
+
console.log(chalk7.red(" A value is required here."));
|
|
5182
6385
|
continue;
|
|
5183
6386
|
}
|
|
5184
6387
|
const error = validator(value);
|
|
5185
6388
|
if (!error) return value;
|
|
5186
|
-
console.log(
|
|
6389
|
+
console.log(chalk7.red(` ${error}`));
|
|
5187
6390
|
}
|
|
5188
6391
|
}
|
|
5189
6392
|
function appendToEnv(key, value) {
|
|
@@ -5205,43 +6408,103 @@ function parseGithubRepo(input) {
|
|
|
5205
6408
|
if (shortMatch) return { owner: shortMatch[1], repo: shortMatch[2] };
|
|
5206
6409
|
return null;
|
|
5207
6410
|
}
|
|
6411
|
+
function formatTelegramUser(user) {
|
|
6412
|
+
const username = user.username ? ` (@${user.username})` : "";
|
|
6413
|
+
const firstName = user.firstName ? ` ${user.firstName}` : "";
|
|
6414
|
+
return `${user.userId}${username}${firstName}`;
|
|
6415
|
+
}
|
|
6416
|
+
function printTelegramAccessState(config) {
|
|
6417
|
+
const admins = config.channels.telegram.admins;
|
|
6418
|
+
const members = config.channels.telegram.members;
|
|
6419
|
+
const pending = config.channels.telegram.pending;
|
|
6420
|
+
const pendingSummary = pending.length > 0 ? pending.map((entry) => {
|
|
6421
|
+
const code = entry.pairingCode ? ` [code: ${entry.pairingCode}]` : "";
|
|
6422
|
+
return `${formatTelegramUser(entry)}${code}`;
|
|
6423
|
+
}).join(", ") : "";
|
|
6424
|
+
console.log("");
|
|
6425
|
+
console.log(` Telegram Access: ${chalk7.white(getTelegramAccessSummary(config))}`);
|
|
6426
|
+
console.log(` Admins: ${admins.length > 0 ? chalk7.green(admins.map(formatTelegramUser).join(", ")) : chalk7.dim("none")}`);
|
|
6427
|
+
console.log(` Members: ${members.length > 0 ? chalk7.green(members.map(formatTelegramUser).join(", ")) : chalk7.dim("none")}`);
|
|
6428
|
+
console.log(` Pending: ${pending.length > 0 ? chalk7.yellow(pendingSummary) : chalk7.dim("none")}`);
|
|
6429
|
+
}
|
|
6430
|
+
function restartDaemonIfRunning(message) {
|
|
6431
|
+
const daemon = getDaemonStatus();
|
|
6432
|
+
if (!daemon.running) return;
|
|
6433
|
+
if (message) {
|
|
6434
|
+
console.log(chalk7.dim(` ${message}`));
|
|
6435
|
+
}
|
|
6436
|
+
restartDaemon();
|
|
6437
|
+
}
|
|
6438
|
+
async function completeInitialTelegramPairing(config) {
|
|
6439
|
+
if (!config.channels.telegram.enabled || !config.channels.telegram.botToken || hasTelegramAdmins(config)) {
|
|
6440
|
+
return;
|
|
6441
|
+
}
|
|
6442
|
+
console.log("");
|
|
6443
|
+
console.log(chalk7.bold.white(" Telegram Pairing"));
|
|
6444
|
+
console.log(chalk7.dim(" 1. Open Telegram and message your bot."));
|
|
6445
|
+
console.log(chalk7.dim(" 2. Send /start to receive your pairing code in Telegram."));
|
|
6446
|
+
console.log(chalk7.dim(" 3. Paste that pairing code below to finish setup."));
|
|
6447
|
+
console.log("");
|
|
6448
|
+
const telegram = new TelegramChannel(config);
|
|
6449
|
+
try {
|
|
6450
|
+
await telegram.start();
|
|
6451
|
+
while (true) {
|
|
6452
|
+
const pairingCode = await ask(chalk7.white(" Telegram Pairing Code: "));
|
|
6453
|
+
if (!pairingCode) {
|
|
6454
|
+
console.log(chalk7.red(" Telegram pairing code is required to continue."));
|
|
6455
|
+
continue;
|
|
6456
|
+
}
|
|
6457
|
+
const approved = approveTelegramPendingRequestByPairingCode(config, pairingCode);
|
|
6458
|
+
if (!approved) {
|
|
6459
|
+
console.log(chalk7.red(" That pairing code is not valid yet. Send /start in Telegram, then paste the exact code here."));
|
|
6460
|
+
continue;
|
|
6461
|
+
}
|
|
6462
|
+
saveConfig(config);
|
|
6463
|
+
console.log(chalk7.green(` \u2713 Telegram paired. First admin: ${formatTelegramUser(approved)}.`));
|
|
6464
|
+
console.log("");
|
|
6465
|
+
break;
|
|
6466
|
+
}
|
|
6467
|
+
} finally {
|
|
6468
|
+
await telegram.stop();
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
5208
6471
|
async function configure(existingConfig) {
|
|
5209
6472
|
const isReconfig = !!existingConfig;
|
|
5210
6473
|
const config = existingConfig ?? loadConfig();
|
|
5211
6474
|
if (isReconfig) {
|
|
5212
6475
|
banner();
|
|
5213
|
-
console.log(
|
|
6476
|
+
console.log(chalk7.yellow(" Reconfiguring Mercury \u2014 press Enter to keep current value."));
|
|
5214
6477
|
} else {
|
|
5215
6478
|
splashScreen();
|
|
5216
|
-
console.log(
|
|
6479
|
+
console.log(chalk7.yellow(" First run detected \u2014 let's set you up."));
|
|
5217
6480
|
}
|
|
5218
6481
|
hr();
|
|
5219
6482
|
console.log("");
|
|
5220
|
-
console.log(
|
|
6483
|
+
console.log(chalk7.bold.white(" Identity"));
|
|
5221
6484
|
console.log("");
|
|
5222
6485
|
if (isReconfig) {
|
|
5223
|
-
const ownerName = await ask(
|
|
6486
|
+
const ownerName = await ask(chalk7.white(` Your name [${config.identity.owner}]: `));
|
|
5224
6487
|
if (ownerName) config.identity.owner = ownerName;
|
|
5225
|
-
const agentName = await ask(
|
|
6488
|
+
const agentName = await ask(chalk7.white(` Agent name [${config.identity.name}]: `));
|
|
5226
6489
|
if (agentName) config.identity.name = agentName;
|
|
5227
6490
|
} else {
|
|
5228
|
-
const ownerName = await ask(
|
|
6491
|
+
const ownerName = await ask(chalk7.white(" Your name: "));
|
|
5229
6492
|
if (!ownerName) {
|
|
5230
|
-
console.log(
|
|
6493
|
+
console.log(chalk7.red(" Name is required."));
|
|
5231
6494
|
process.exit(1);
|
|
5232
6495
|
}
|
|
5233
6496
|
config.identity.owner = ownerName;
|
|
5234
|
-
const agentName = await ask(
|
|
6497
|
+
const agentName = await ask(chalk7.white(` Agent name [${config.identity.name}]: `));
|
|
5235
6498
|
if (agentName) config.identity.name = agentName;
|
|
5236
6499
|
}
|
|
5237
6500
|
config.identity.creator = config.identity.creator || "Cosmic Stack";
|
|
5238
6501
|
hr();
|
|
5239
6502
|
console.log("");
|
|
5240
|
-
console.log(
|
|
6503
|
+
console.log(chalk7.bold.white(" LLM Providers"));
|
|
5241
6504
|
if (isReconfig) {
|
|
5242
|
-
console.log(
|
|
6505
|
+
console.log(chalk7.dim(" Choose which providers to configure now. Existing values are shown where available."));
|
|
5243
6506
|
} else {
|
|
5244
|
-
console.log(
|
|
6507
|
+
console.log(chalk7.dim(" Choose one or more providers. Press Enter to configure DeepSeek by default."));
|
|
5245
6508
|
}
|
|
5246
6509
|
console.log("");
|
|
5247
6510
|
while (true) {
|
|
@@ -5250,92 +6513,97 @@ async function configure(existingConfig) {
|
|
|
5250
6513
|
for (const provider of selectedProviders) {
|
|
5251
6514
|
if (provider === "deepseek") {
|
|
5252
6515
|
const mask = isReconfig && config.providers.deepseek.apiKey ? ` [${maskKey(config.providers.deepseek.apiKey)}]` : "";
|
|
5253
|
-
const
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
{
|
|
6516
|
+
const result = await promptApiKeyWithModelSelection(
|
|
6517
|
+
config,
|
|
6518
|
+
"deepseek",
|
|
6519
|
+
"DeepSeek",
|
|
6520
|
+
chalk7.white(` DeepSeek API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
6521
|
+
isReconfig
|
|
5258
6522
|
);
|
|
5259
|
-
if (
|
|
5260
|
-
config.providers.deepseek.apiKey =
|
|
6523
|
+
if (!result.skipped && result.apiKey && result.model) {
|
|
6524
|
+
config.providers.deepseek.apiKey = result.apiKey;
|
|
6525
|
+
config.providers.deepseek.model = result.model;
|
|
5261
6526
|
config.providers.deepseek.enabled = true;
|
|
5262
6527
|
}
|
|
5263
6528
|
continue;
|
|
5264
6529
|
}
|
|
5265
6530
|
if (provider === "openai") {
|
|
5266
6531
|
const mask = isReconfig && config.providers.openai.apiKey ? ` [${maskKey(config.providers.openai.apiKey)}]` : "";
|
|
5267
|
-
const
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
{
|
|
6532
|
+
const result = await promptApiKeyWithModelSelection(
|
|
6533
|
+
config,
|
|
6534
|
+
"openai",
|
|
6535
|
+
"OpenAI",
|
|
6536
|
+
chalk7.white(` OpenAI API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
6537
|
+
isReconfig
|
|
5272
6538
|
);
|
|
5273
|
-
if (
|
|
5274
|
-
config.providers.openai.apiKey =
|
|
6539
|
+
if (!result.skipped && result.apiKey && result.model) {
|
|
6540
|
+
config.providers.openai.apiKey = result.apiKey;
|
|
6541
|
+
config.providers.openai.model = result.model;
|
|
5275
6542
|
config.providers.openai.enabled = true;
|
|
5276
6543
|
}
|
|
5277
6544
|
continue;
|
|
5278
6545
|
}
|
|
5279
6546
|
if (provider === "anthropic") {
|
|
5280
6547
|
const mask = isReconfig && config.providers.anthropic.apiKey ? ` [${maskKey(config.providers.anthropic.apiKey)}]` : "";
|
|
5281
|
-
const
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
{
|
|
6548
|
+
const result = await promptApiKeyWithModelSelection(
|
|
6549
|
+
config,
|
|
6550
|
+
"anthropic",
|
|
6551
|
+
"Anthropic",
|
|
6552
|
+
chalk7.white(` Anthropic API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
6553
|
+
isReconfig
|
|
5286
6554
|
);
|
|
5287
|
-
if (
|
|
5288
|
-
config.providers.anthropic.apiKey =
|
|
6555
|
+
if (!result.skipped && result.apiKey && result.model) {
|
|
6556
|
+
config.providers.anthropic.apiKey = result.apiKey;
|
|
6557
|
+
config.providers.anthropic.model = result.model;
|
|
5289
6558
|
config.providers.anthropic.enabled = true;
|
|
5290
6559
|
}
|
|
5291
6560
|
continue;
|
|
5292
6561
|
}
|
|
5293
6562
|
if (provider === "grok") {
|
|
5294
6563
|
const mask = isReconfig && config.providers.grok.apiKey ? ` [${maskKey(config.providers.grok.apiKey)}]` : "";
|
|
5295
|
-
const
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
{
|
|
6564
|
+
const result = await promptApiKeyWithModelSelection(
|
|
6565
|
+
config,
|
|
6566
|
+
"grok",
|
|
6567
|
+
"Grok",
|
|
6568
|
+
chalk7.white(` Grok API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
6569
|
+
isReconfig
|
|
5300
6570
|
);
|
|
5301
|
-
if (
|
|
5302
|
-
config.providers.grok.apiKey =
|
|
6571
|
+
if (!result.skipped && result.apiKey && result.model) {
|
|
6572
|
+
config.providers.grok.apiKey = result.apiKey;
|
|
6573
|
+
config.providers.grok.model = result.model;
|
|
5303
6574
|
config.providers.grok.enabled = true;
|
|
5304
6575
|
}
|
|
5305
6576
|
continue;
|
|
5306
6577
|
}
|
|
5307
6578
|
if (provider === "ollamaCloud") {
|
|
5308
6579
|
const mask = isReconfig && config.providers.ollamaCloud.apiKey ? ` [${maskKey(config.providers.ollamaCloud.apiKey)}]` : "";
|
|
5309
|
-
const
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
{
|
|
6580
|
+
const result = await promptApiKeyWithModelSelection(
|
|
6581
|
+
config,
|
|
6582
|
+
"ollamaCloud",
|
|
6583
|
+
"Ollama Cloud",
|
|
6584
|
+
chalk7.white(` Ollama Cloud API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
6585
|
+
isReconfig
|
|
5314
6586
|
);
|
|
5315
|
-
if (
|
|
5316
|
-
config.providers.ollamaCloud.apiKey =
|
|
6587
|
+
if (!result.skipped && result.apiKey && result.model) {
|
|
6588
|
+
config.providers.ollamaCloud.apiKey = result.apiKey;
|
|
6589
|
+
config.providers.ollamaCloud.model = result.model;
|
|
5317
6590
|
config.providers.ollamaCloud.enabled = true;
|
|
5318
6591
|
}
|
|
5319
6592
|
continue;
|
|
5320
6593
|
}
|
|
5321
6594
|
if (provider === "ollamaLocal") {
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
config.providers.ollamaLocal.
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
chalk6.white(` Ollama Local model [${config.providers.ollamaLocal.model}]: `),
|
|
5329
|
-
validateModelName,
|
|
5330
|
-
config.providers.ollamaLocal.model
|
|
5331
|
-
);
|
|
5332
|
-
config.providers.ollamaLocal.enabled = true;
|
|
6595
|
+
const result = await promptOllamaLocalModelSelection(config);
|
|
6596
|
+
if (!result.skipped && result.baseUrl && result.model) {
|
|
6597
|
+
config.providers.ollamaLocal.baseUrl = result.baseUrl;
|
|
6598
|
+
config.providers.ollamaLocal.model = result.model;
|
|
6599
|
+
config.providers.ollamaLocal.enabled = true;
|
|
6600
|
+
}
|
|
5333
6601
|
}
|
|
5334
6602
|
}
|
|
5335
6603
|
const configuredProviders = getConfiguredProviderNames(config);
|
|
5336
6604
|
if (configuredProviders.length === 0) {
|
|
5337
|
-
console.log(
|
|
5338
|
-
console.log(
|
|
6605
|
+
console.log(chalk7.red(" You need to configure at least one LLM provider to continue."));
|
|
6606
|
+
console.log(chalk7.dim(" Let\u2019s try that step again."));
|
|
5339
6607
|
console.log("");
|
|
5340
6608
|
continue;
|
|
5341
6609
|
}
|
|
@@ -5344,78 +6612,81 @@ async function configure(existingConfig) {
|
|
|
5344
6612
|
}
|
|
5345
6613
|
hr();
|
|
5346
6614
|
console.log("");
|
|
5347
|
-
console.log(
|
|
6615
|
+
console.log(chalk7.bold.white(" Telegram (optional)"));
|
|
5348
6616
|
if (isReconfig) {
|
|
5349
|
-
console.log(
|
|
6617
|
+
console.log(chalk7.dim(' Leave empty to keep current value. Enter "none" to disable.'));
|
|
5350
6618
|
} else {
|
|
5351
|
-
console.log(
|
|
5352
|
-
console.log(
|
|
5353
|
-
console.log(
|
|
5354
|
-
console.log(
|
|
5355
|
-
console.log(
|
|
5356
|
-
console.log(
|
|
6619
|
+
console.log(chalk7.dim(" Leave empty to skip. You can add it later."));
|
|
6620
|
+
console.log(chalk7.dim(" To create a bot token:"));
|
|
6621
|
+
console.log(chalk7.dim(" 1. Open Telegram and message @BotFather"));
|
|
6622
|
+
console.log(chalk7.dim(" 2. Run /newbot and follow the prompts"));
|
|
6623
|
+
console.log(chalk7.dim(" 3. Copy the bot token BotFather gives you"));
|
|
6624
|
+
console.log(chalk7.dim(" 4. Paste that token here"));
|
|
6625
|
+
console.log(chalk7.dim(" After setup, users send /start to request access."));
|
|
6626
|
+
console.log(chalk7.dim(" The first Telegram user gets a pairing code, and you approve that code from the CLI."));
|
|
5357
6627
|
}
|
|
5358
6628
|
console.log("");
|
|
5359
6629
|
const tgMask = isReconfig && config.channels.telegram.botToken ? ` [${maskKey(config.channels.telegram.botToken)}]` : "";
|
|
5360
|
-
const telegramToken = await ask(
|
|
6630
|
+
const telegramToken = await ask(chalk7.white(` Telegram Bot Token${tgMask}: `));
|
|
5361
6631
|
if (isReconfig && telegramToken.toLowerCase() === "none") {
|
|
5362
6632
|
config.channels.telegram.enabled = false;
|
|
5363
6633
|
config.channels.telegram.botToken = "";
|
|
5364
|
-
|
|
6634
|
+
clearTelegramAccess(config);
|
|
5365
6635
|
} else if (telegramToken) {
|
|
5366
6636
|
if (telegramToken !== config.channels.telegram.botToken) {
|
|
5367
|
-
|
|
6637
|
+
clearTelegramAccess(config);
|
|
5368
6638
|
}
|
|
5369
6639
|
config.channels.telegram.botToken = telegramToken;
|
|
5370
6640
|
config.channels.telegram.enabled = true;
|
|
5371
6641
|
}
|
|
6642
|
+
await completeInitialTelegramPairing(config);
|
|
5372
6643
|
hr();
|
|
5373
6644
|
console.log("");
|
|
5374
|
-
console.log(
|
|
5375
|
-
console.log(
|
|
5376
|
-
console.log(
|
|
5377
|
-
console.log(
|
|
6645
|
+
console.log(chalk7.bold.white(" GitHub Integration (optional)"));
|
|
6646
|
+
console.log(chalk7.dim(" Connect Mercury to GitHub so it can create PRs, manage issues,"));
|
|
6647
|
+
console.log(chalk7.dim(" review code, and co-author commits on your behalf."));
|
|
6648
|
+
console.log(chalk7.dim(" Leave empty to skip. You can add it later with mercury doctor."));
|
|
5378
6649
|
console.log("");
|
|
5379
6650
|
const ghUserCurrent = isReconfig && config.github.username ? ` [${config.github.username}]` : "";
|
|
5380
|
-
const ghUsername = await ask(
|
|
6651
|
+
const ghUsername = await ask(chalk7.white(` 1. Your GitHub username${ghUserCurrent}: `));
|
|
5381
6652
|
if (ghUsername) config.github.username = ghUsername;
|
|
5382
6653
|
if (!config.github.email) {
|
|
5383
6654
|
config.github.email = "mercury@cosmicstack.org";
|
|
5384
6655
|
}
|
|
5385
6656
|
console.log("");
|
|
5386
|
-
console.log(
|
|
5387
|
-
console.log(
|
|
5388
|
-
console.log(
|
|
5389
|
-
console.log(
|
|
5390
|
-
console.log(
|
|
6657
|
+
console.log(chalk7.dim(" You need a Personal Access Token (PAT) with repo access."));
|
|
6658
|
+
console.log(chalk7.dim(" Fine-grained (recommended): github.com/settings/personal-access-tokens/new"));
|
|
6659
|
+
console.log(chalk7.dim(" \u2192 Permissions: Contents (R/W), Pull requests (R/W), Issues (R/W)"));
|
|
6660
|
+
console.log(chalk7.dim(" Classic: github.com/settings/tokens/new"));
|
|
6661
|
+
console.log(chalk7.dim(" \u2192 Scope: repo (full control)"));
|
|
5391
6662
|
const ghTokenCurrent = process.env.GITHUB_TOKEN ? ` [${maskKey(process.env.GITHUB_TOKEN)}]` : "";
|
|
5392
|
-
const ghToken = await ask(
|
|
6663
|
+
const ghToken = await ask(chalk7.white(` 2. GitHub PAT${ghTokenCurrent}: `));
|
|
5393
6664
|
if (ghToken) {
|
|
5394
6665
|
appendToEnv("GITHUB_TOKEN", ghToken);
|
|
5395
6666
|
}
|
|
5396
6667
|
if (config.github.username || process.env.GITHUB_TOKEN) {
|
|
5397
6668
|
console.log("");
|
|
5398
|
-
console.log(
|
|
5399
|
-
console.log(
|
|
5400
|
-
console.log(
|
|
5401
|
-
console.log(
|
|
6669
|
+
console.log(chalk7.dim(' Set a default repo so you can say "create an issue" without'));
|
|
6670
|
+
console.log(chalk7.dim(" specifying the repo every time. Enter owner/name or a full URL."));
|
|
6671
|
+
console.log(chalk7.dim(" Example: hotheadhacker/mercury-agent"));
|
|
6672
|
+
console.log(chalk7.dim(" Example: https://github.com/hotheadhacker/mercury-agent"));
|
|
5402
6673
|
const ghOwnerCurrent = isReconfig && config.github.defaultOwner ? ` [${config.github.defaultOwner}/${config.github.defaultRepo}]` : "";
|
|
5403
|
-
const ghRepoInput = await ask(
|
|
6674
|
+
const ghRepoInput = await ask(chalk7.white(` 3. Default repo${ghOwnerCurrent}: `));
|
|
5404
6675
|
if (ghRepoInput) {
|
|
5405
6676
|
const parsed = parseGithubRepo(ghRepoInput);
|
|
5406
6677
|
if (parsed) {
|
|
5407
6678
|
config.github.defaultOwner = parsed.owner;
|
|
5408
6679
|
config.github.defaultRepo = parsed.repo;
|
|
5409
6680
|
} else {
|
|
5410
|
-
console.log(
|
|
6681
|
+
console.log(chalk7.yellow(" Could not parse repo. Use format: owner/repo or a GitHub URL."));
|
|
5411
6682
|
}
|
|
5412
6683
|
}
|
|
5413
6684
|
}
|
|
5414
6685
|
hr();
|
|
5415
6686
|
console.log("");
|
|
5416
|
-
console.log(
|
|
6687
|
+
console.log(chalk7.bold.white(" Token Budget"));
|
|
5417
6688
|
console.log("");
|
|
5418
|
-
const budgetPrompt = isReconfig ?
|
|
6689
|
+
const budgetPrompt = isReconfig ? chalk7.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `) : chalk7.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `);
|
|
5419
6690
|
const budgetStr = await ask(budgetPrompt);
|
|
5420
6691
|
if (budgetStr) {
|
|
5421
6692
|
const budget = parseInt(budgetStr.replace(/,/g, ""), 10);
|
|
@@ -5427,14 +6698,14 @@ async function configure(existingConfig) {
|
|
|
5427
6698
|
saveConfig(config);
|
|
5428
6699
|
const home = getMercuryHome();
|
|
5429
6700
|
console.log("");
|
|
5430
|
-
console.log(
|
|
5431
|
-
console.log(
|
|
5432
|
-
console.log(
|
|
5433
|
-
console.log(
|
|
5434
|
-
console.log(
|
|
6701
|
+
console.log(chalk7.green(` \u2713 Config saved to ${home}/mercury.yaml`));
|
|
6702
|
+
console.log(chalk7.green(` \u2713 Soul files seeded in ${home}/soul/`));
|
|
6703
|
+
console.log(chalk7.green(` \u2713 Memory stored in ${home}/memory/`));
|
|
6704
|
+
console.log(chalk7.green(` \u2713 Permissions seeded in ${home}/permissions.yaml`));
|
|
6705
|
+
console.log(chalk7.green(` \u2713 Skills directory ready in ${home}/skills/`));
|
|
5435
6706
|
console.log("");
|
|
5436
|
-
console.log(
|
|
5437
|
-
console.log(
|
|
6707
|
+
console.log(chalk7.cyan(` ${config.identity.name} is ready. Run \`mercury start\` to chat.`));
|
|
6708
|
+
console.log(chalk7.dim(" mercury.cosmicstack.org"));
|
|
5438
6709
|
console.log("");
|
|
5439
6710
|
}
|
|
5440
6711
|
function autoDaemonize() {
|
|
@@ -5442,22 +6713,22 @@ function autoDaemonize() {
|
|
|
5442
6713
|
if (daemon.running) {
|
|
5443
6714
|
return;
|
|
5444
6715
|
}
|
|
5445
|
-
console.log(
|
|
6716
|
+
console.log(chalk7.dim(" Setting up background mode..."));
|
|
5446
6717
|
try {
|
|
5447
6718
|
if (!isServiceInstalled()) {
|
|
5448
6719
|
installService();
|
|
5449
6720
|
}
|
|
5450
6721
|
} catch {
|
|
5451
|
-
console.log(
|
|
6722
|
+
console.log(chalk7.dim(" Service install skipped (can run `mercury service install` later)."));
|
|
5452
6723
|
}
|
|
5453
6724
|
const ok = tryAutoDaemonize();
|
|
5454
6725
|
if (ok) {
|
|
5455
6726
|
const status = getDaemonStatus();
|
|
5456
|
-
console.log(
|
|
5457
|
-
console.log(
|
|
5458
|
-
console.log(
|
|
6727
|
+
console.log(chalk7.green(` \u2713 Mercury is running in background (PID: ${status.pid})`));
|
|
6728
|
+
console.log(chalk7.green(" \u2713 Auto-starts on login. Auto-restarts on crash."));
|
|
6729
|
+
console.log(chalk7.dim(" Use `mercury stop` to stop. `mercury restart` to restart."));
|
|
5459
6730
|
} else {
|
|
5460
|
-
console.log(
|
|
6731
|
+
console.log(chalk7.dim(" Background mode not available. Run `mercury up` to set it up."));
|
|
5461
6732
|
}
|
|
5462
6733
|
console.log("");
|
|
5463
6734
|
}
|
|
@@ -5467,7 +6738,7 @@ async function runAgent(isDaemon = false) {
|
|
|
5467
6738
|
const name = config.identity.name;
|
|
5468
6739
|
if (!isDaemon) {
|
|
5469
6740
|
banner();
|
|
5470
|
-
console.log(
|
|
6741
|
+
console.log(chalk7.white(` ${name} is waking up...`));
|
|
5471
6742
|
console.log("");
|
|
5472
6743
|
} else {
|
|
5473
6744
|
logger.info(`${name} is waking up (daemon mode)...`);
|
|
@@ -5479,19 +6750,25 @@ async function runAgent(isDaemon = false) {
|
|
|
5479
6750
|
logger.error("No LLM providers available. Run `mercury doctor` to configure providers.");
|
|
5480
6751
|
return;
|
|
5481
6752
|
}
|
|
5482
|
-
console.log(
|
|
6753
|
+
console.log(chalk7.red(" No LLM providers available. Run `mercury doctor` to configure providers."));
|
|
5483
6754
|
process.exit(1);
|
|
5484
6755
|
}
|
|
5485
6756
|
const available = providers.listAvailable();
|
|
6757
|
+
const providerLabels = available.map((provider) => getProviderLabel(provider));
|
|
6758
|
+
const providerModels = available.map((provider) => {
|
|
6759
|
+
const key = provider;
|
|
6760
|
+
return `${getProviderLabel(key)}: ${config.providers[key].model}`;
|
|
6761
|
+
});
|
|
5486
6762
|
if (!isDaemon) {
|
|
5487
|
-
console.log(
|
|
6763
|
+
console.log(chalk7.dim(` Providers: ${providerLabels.join(", ")}`));
|
|
6764
|
+
console.log(chalk7.dim(` Models: ${providerModels.join(" | ")}`));
|
|
5488
6765
|
} else {
|
|
5489
6766
|
logger.info({ providers: available }, "Providers loaded");
|
|
5490
6767
|
}
|
|
5491
6768
|
const skillLoader = new SkillLoader();
|
|
5492
6769
|
const skills = skillLoader.discover();
|
|
5493
6770
|
if (!isDaemon) {
|
|
5494
|
-
console.log(
|
|
6771
|
+
console.log(chalk7.dim(` Skills: ${skills.length > 0 ? skills.map((s) => s.name).join(", ") : "none installed"}`));
|
|
5495
6772
|
}
|
|
5496
6773
|
const scheduler = new Scheduler(config);
|
|
5497
6774
|
const identity = new Identity();
|
|
@@ -5508,22 +6785,30 @@ async function runAgent(isDaemon = false) {
|
|
|
5508
6785
|
manual: () => getManual()
|
|
5509
6786
|
});
|
|
5510
6787
|
capabilities.setSendFileHandler(async (filePath) => {
|
|
5511
|
-
const
|
|
5512
|
-
|
|
5513
|
-
|
|
6788
|
+
const { channelId, channelType } = capabilities.getChannelContext();
|
|
6789
|
+
const telegram = channels.get("telegram");
|
|
6790
|
+
if (channelType === "telegram" && telegram) {
|
|
6791
|
+
await telegram.sendFile(filePath, channelId);
|
|
6792
|
+
return;
|
|
6793
|
+
}
|
|
6794
|
+
if (config.channels.telegram.enabled && telegram && getTelegramApprovedUsers(config).length > 0) {
|
|
6795
|
+
await telegram.sendFile(filePath);
|
|
6796
|
+
return;
|
|
6797
|
+
}
|
|
6798
|
+
const cli = channels.get("cli");
|
|
6799
|
+
if (cli) {
|
|
6800
|
+
await cli.sendFile(filePath);
|
|
5514
6801
|
}
|
|
5515
6802
|
});
|
|
5516
6803
|
capabilities.setSendMessageHandler(async (content) => {
|
|
5517
6804
|
const telegram = channels.get("telegram");
|
|
5518
|
-
const pairedChatId = config.channels.telegram.pairedChatId;
|
|
5519
|
-
const pairedUserId = config.channels.telegram.pairedUserId;
|
|
5520
6805
|
if (!config.channels.telegram.enabled || !telegram) {
|
|
5521
6806
|
throw new Error("Telegram is not configured. Add a bot token in setup or run `mercury doctor`.");
|
|
5522
6807
|
}
|
|
5523
|
-
if (
|
|
5524
|
-
throw new Error("Telegram
|
|
6808
|
+
if (getTelegramApprovedUsers(config).length === 0) {
|
|
6809
|
+
throw new Error("Telegram has no approved users. Ask someone to send /start, then approve the request from Mercury.");
|
|
5525
6810
|
}
|
|
5526
|
-
await telegram.send(content
|
|
6811
|
+
await telegram.send(content);
|
|
5527
6812
|
});
|
|
5528
6813
|
if (process.env.GITHUB_TOKEN) {
|
|
5529
6814
|
setGitHubToken(process.env.GITHUB_TOKEN);
|
|
@@ -5558,17 +6843,13 @@ async function runAgent(isDaemon = false) {
|
|
|
5558
6843
|
const activeCh = channels.getActiveChannels();
|
|
5559
6844
|
const toolNames = capabilities.getToolNames();
|
|
5560
6845
|
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
6846
|
if (config.identity.creator) {
|
|
5566
|
-
console.log(
|
|
6847
|
+
console.log(chalk7.dim(` Creator: ${config.identity.creator}`));
|
|
5567
6848
|
}
|
|
5568
6849
|
hr();
|
|
5569
6850
|
console.log("");
|
|
5570
|
-
console.log(
|
|
5571
|
-
console.log(
|
|
6851
|
+
console.log(chalk7.green(` ${name} is live. Type a message and press Enter.`));
|
|
6852
|
+
console.log(chalk7.dim(" Ctrl+C to exit \xB7 /help for commands"));
|
|
5572
6853
|
console.log("");
|
|
5573
6854
|
} else {
|
|
5574
6855
|
logger.info({ channels: activeCh, tools: toolNames }, "Mercury is live (daemon mode)");
|
|
@@ -5576,7 +6857,7 @@ async function runAgent(isDaemon = false) {
|
|
|
5576
6857
|
const shutdown = async () => {
|
|
5577
6858
|
if (!isDaemon) {
|
|
5578
6859
|
console.log("");
|
|
5579
|
-
console.log(
|
|
6860
|
+
console.log(chalk7.dim(` ${name} is shutting down...`));
|
|
5580
6861
|
} else {
|
|
5581
6862
|
logger.info("Mercury is shutting down (daemon mode)");
|
|
5582
6863
|
}
|
|
@@ -5623,17 +6904,17 @@ program.command("up").description("Ensure Mercury is running persistently \u2014
|
|
|
5623
6904
|
const daemon = getDaemonStatus();
|
|
5624
6905
|
if (daemon.running && daemon.pid) {
|
|
5625
6906
|
console.log("");
|
|
5626
|
-
console.log(
|
|
5627
|
-
console.log(
|
|
6907
|
+
console.log(chalk7.green(` Mercury is already running (PID: ${daemon.pid})`));
|
|
6908
|
+
console.log(chalk7.dim(` Logs: ${daemon.logPath}`));
|
|
5628
6909
|
console.log("");
|
|
5629
6910
|
return;
|
|
5630
6911
|
}
|
|
5631
6912
|
if (!isServiceInstalled()) {
|
|
5632
6913
|
console.log("");
|
|
5633
|
-
console.log(
|
|
6914
|
+
console.log(chalk7.cyan(" Installing Mercury as a system service..."));
|
|
5634
6915
|
installService();
|
|
5635
6916
|
}
|
|
5636
|
-
console.log(
|
|
6917
|
+
console.log(chalk7.cyan(" Starting Mercury in background..."));
|
|
5637
6918
|
startBackground();
|
|
5638
6919
|
});
|
|
5639
6920
|
program.command("logs").description("Show recent daemon logs").action(() => {
|
|
@@ -5660,43 +6941,175 @@ program.command("status").description("Show current configuration and daemon sta
|
|
|
5660
6941
|
const skills = skillLoader.discover();
|
|
5661
6942
|
const daemon = getDaemonStatus();
|
|
5662
6943
|
banner();
|
|
5663
|
-
console.log(` Name: ${
|
|
5664
|
-
console.log(` Owner: ${
|
|
6944
|
+
console.log(` Name: ${chalk7.cyan(config.identity.name)}`);
|
|
6945
|
+
console.log(` Owner: ${chalk7.white(config.identity.owner || "(not set)")}`);
|
|
5665
6946
|
if (config.identity.creator) {
|
|
5666
|
-
console.log(` Creator: ${
|
|
5667
|
-
}
|
|
5668
|
-
console.log(` Provider: ${
|
|
5669
|
-
console.log(` Telegram: ${config.channels.telegram.enabled ?
|
|
5670
|
-
console.log(` Telegram
|
|
5671
|
-
console.log(` Skills: ${skills.length > 0 ?
|
|
5672
|
-
console.log(` Budget: ${
|
|
5673
|
-
console.log(` Setup: ${isSetupComplete() ?
|
|
5674
|
-
console.log(` Daemon: ${daemon.running ?
|
|
5675
|
-
console.log(` Home: ${
|
|
6947
|
+
console.log(` Creator: ${chalk7.white(config.identity.creator)}`);
|
|
6948
|
+
}
|
|
6949
|
+
console.log(` Provider: ${chalk7.white(getProviderLabel(config.providers.default))}`);
|
|
6950
|
+
console.log(` Telegram: ${config.channels.telegram.enabled ? chalk7.green("enabled") : chalk7.dim("disabled")}`);
|
|
6951
|
+
console.log(` Telegram Access: ${chalk7.white(getTelegramAccessSummary(config))}`);
|
|
6952
|
+
console.log(` Skills: ${skills.length > 0 ? chalk7.green(skills.map((s) => s.name).join(", ")) : chalk7.dim("none")}`);
|
|
6953
|
+
console.log(` Budget: ${chalk7.white(config.tokens.dailyBudget.toLocaleString())} tokens/day`);
|
|
6954
|
+
console.log(` Setup: ${isSetupComplete() ? chalk7.green("complete") : chalk7.red("not done")}`);
|
|
6955
|
+
console.log(` Daemon: ${daemon.running ? chalk7.green(`running (PID: ${daemon.pid})`) : chalk7.dim("not running")}`);
|
|
6956
|
+
console.log(` Home: ${chalk7.dim(home)}`);
|
|
6957
|
+
printTelegramAccessState(config);
|
|
5676
6958
|
console.log("");
|
|
5677
6959
|
});
|
|
5678
6960
|
program.command("help").description("Show capabilities and commands manual").action(() => {
|
|
5679
6961
|
console.log(getManual());
|
|
5680
6962
|
});
|
|
5681
|
-
var telegramCmd = program.command("telegram").description("Manage Telegram
|
|
5682
|
-
telegramCmd.command("
|
|
6963
|
+
var telegramCmd = program.command("telegram").description("Manage Telegram access approvals and admins");
|
|
6964
|
+
telegramCmd.command("list").description("Show approved Telegram users and pending access requests").action(() => {
|
|
5683
6965
|
const config = loadConfig();
|
|
5684
|
-
|
|
5685
|
-
|
|
6966
|
+
console.log("");
|
|
6967
|
+
printTelegramAccessState(config);
|
|
6968
|
+
console.log("");
|
|
6969
|
+
});
|
|
6970
|
+
telegramCmd.command("approve <codeOrUserId>").description("Approve a pending Telegram access request by pairing code or user ID").action((codeOrUserId) => {
|
|
6971
|
+
const config = loadConfig();
|
|
6972
|
+
const hasAdmins = hasTelegramAdmins(config);
|
|
6973
|
+
if (!hasAdmins) {
|
|
6974
|
+
const approved2 = approveTelegramPendingRequestByPairingCode(config, codeOrUserId.trim());
|
|
6975
|
+
if (!approved2) {
|
|
6976
|
+
console.log("");
|
|
6977
|
+
console.log(chalk7.red(` No pending first-time Telegram pairing found for code ${codeOrUserId}.`));
|
|
6978
|
+
console.log("");
|
|
6979
|
+
return;
|
|
6980
|
+
}
|
|
6981
|
+
saveConfig(config);
|
|
6982
|
+
console.log("");
|
|
6983
|
+
console.log(chalk7.green(` \u2713 Approved first Telegram admin ${formatTelegramUser(approved2)}.`));
|
|
6984
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
6985
|
+
console.log("");
|
|
6986
|
+
return;
|
|
6987
|
+
}
|
|
6988
|
+
const targetUserId = Number(codeOrUserId);
|
|
6989
|
+
if (isNaN(targetUserId)) {
|
|
6990
|
+
console.log("");
|
|
6991
|
+
console.log(chalk7.red(" Please provide a numeric Telegram user ID once Telegram already has an admin."));
|
|
6992
|
+
console.log("");
|
|
6993
|
+
return;
|
|
6994
|
+
}
|
|
6995
|
+
const approved = approveTelegramPendingRequest(config, targetUserId, "member");
|
|
6996
|
+
if (!approved) {
|
|
5686
6997
|
console.log("");
|
|
5687
|
-
console.log(
|
|
6998
|
+
console.log(chalk7.red(` No pending Telegram request found for user ${codeOrUserId}.`));
|
|
5688
6999
|
console.log("");
|
|
5689
7000
|
return;
|
|
5690
7001
|
}
|
|
5691
|
-
clearTelegramPairing(config);
|
|
5692
7002
|
saveConfig(config);
|
|
5693
7003
|
console.log("");
|
|
5694
|
-
console.log(
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
7004
|
+
console.log(chalk7.green(` \u2713 Approved Telegram member ${formatTelegramUser(approved)}.`));
|
|
7005
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
7006
|
+
console.log("");
|
|
7007
|
+
});
|
|
7008
|
+
telegramCmd.command("reject <userId>").description("Reject a pending Telegram access request").action((userId) => {
|
|
7009
|
+
const config = loadConfig();
|
|
7010
|
+
const targetUserId = Number(userId);
|
|
7011
|
+
if (isNaN(targetUserId)) {
|
|
7012
|
+
console.log("");
|
|
7013
|
+
console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
|
|
7014
|
+
console.log("");
|
|
7015
|
+
return;
|
|
7016
|
+
}
|
|
7017
|
+
const rejected = rejectTelegramPendingRequest(config, targetUserId);
|
|
7018
|
+
if (!rejected) {
|
|
7019
|
+
console.log("");
|
|
7020
|
+
console.log(chalk7.red(` No pending Telegram request found for user ${userId}.`));
|
|
7021
|
+
console.log("");
|
|
7022
|
+
return;
|
|
7023
|
+
}
|
|
7024
|
+
saveConfig(config);
|
|
7025
|
+
console.log("");
|
|
7026
|
+
console.log(chalk7.green(` \u2713 Rejected Telegram request for ${formatTelegramUser(rejected)}.`));
|
|
7027
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
7028
|
+
console.log("");
|
|
7029
|
+
});
|
|
7030
|
+
telegramCmd.command("remove <userId>").description("Remove an approved Telegram admin or member").action((userId) => {
|
|
7031
|
+
const config = loadConfig();
|
|
7032
|
+
const targetUserId = Number(userId);
|
|
7033
|
+
if (isNaN(targetUserId)) {
|
|
7034
|
+
console.log("");
|
|
7035
|
+
console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
|
|
7036
|
+
console.log("");
|
|
7037
|
+
return;
|
|
7038
|
+
}
|
|
7039
|
+
const removed = removeTelegramUser(config, targetUserId);
|
|
7040
|
+
if (!removed) {
|
|
7041
|
+
console.log("");
|
|
7042
|
+
console.log(chalk7.red(` No approved Telegram user found for ${userId}.`));
|
|
7043
|
+
console.log("");
|
|
7044
|
+
return;
|
|
7045
|
+
}
|
|
7046
|
+
saveConfig(config);
|
|
7047
|
+
console.log("");
|
|
7048
|
+
console.log(chalk7.green(` \u2713 Removed Telegram access for ${formatTelegramUser(removed)}.`));
|
|
7049
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
7050
|
+
console.log("");
|
|
7051
|
+
});
|
|
7052
|
+
telegramCmd.command("promote <userId>").description("Promote an approved Telegram member to admin").action((userId) => {
|
|
7053
|
+
const config = loadConfig();
|
|
7054
|
+
const targetUserId = Number(userId);
|
|
7055
|
+
if (isNaN(targetUserId)) {
|
|
7056
|
+
console.log("");
|
|
7057
|
+
console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
|
|
7058
|
+
console.log("");
|
|
7059
|
+
return;
|
|
7060
|
+
}
|
|
7061
|
+
const promoted = promoteTelegramUserToAdmin(config, targetUserId);
|
|
7062
|
+
if (!promoted) {
|
|
7063
|
+
console.log("");
|
|
7064
|
+
console.log(chalk7.red(` No Telegram member found for ${userId}.`));
|
|
7065
|
+
console.log("");
|
|
7066
|
+
return;
|
|
7067
|
+
}
|
|
7068
|
+
saveConfig(config);
|
|
7069
|
+
console.log("");
|
|
7070
|
+
console.log(chalk7.green(` \u2713 Promoted ${formatTelegramUser(promoted)} to Telegram admin.`));
|
|
7071
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
7072
|
+
console.log("");
|
|
7073
|
+
});
|
|
7074
|
+
telegramCmd.command("demote <userId>").description("Demote a Telegram admin to member").action((userId) => {
|
|
7075
|
+
const config = loadConfig();
|
|
7076
|
+
const targetUserId = Number(userId);
|
|
7077
|
+
if (isNaN(targetUserId)) {
|
|
7078
|
+
console.log("");
|
|
7079
|
+
console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
|
|
7080
|
+
console.log("");
|
|
7081
|
+
return;
|
|
7082
|
+
}
|
|
7083
|
+
const demoted = demoteTelegramAdmin(config, targetUserId);
|
|
7084
|
+
if (!demoted) {
|
|
7085
|
+
console.log("");
|
|
7086
|
+
console.log(chalk7.red(" Could not demote that Telegram admin. Mercury must keep at least one admin."));
|
|
7087
|
+
console.log("");
|
|
7088
|
+
return;
|
|
7089
|
+
}
|
|
7090
|
+
saveConfig(config);
|
|
7091
|
+
console.log("");
|
|
7092
|
+
console.log(chalk7.green(` \u2713 Demoted ${formatTelegramUser(demoted)} to Telegram member.`));
|
|
7093
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
7094
|
+
console.log("");
|
|
7095
|
+
});
|
|
7096
|
+
telegramCmd.command("unpair").description("Reset all Telegram access for this Mercury instance").action(() => {
|
|
7097
|
+
const config = loadConfig();
|
|
7098
|
+
const hasAnyAccess = getTelegramApprovedUsers(config).length > 0 || getTelegramPendingRequests(config).length > 0;
|
|
7099
|
+
if (!hasAnyAccess) {
|
|
7100
|
+
console.log("");
|
|
7101
|
+
console.log(chalk7.dim(" Telegram access is already empty."));
|
|
7102
|
+
console.log("");
|
|
7103
|
+
return;
|
|
7104
|
+
}
|
|
7105
|
+
clearTelegramAccess(config);
|
|
7106
|
+
saveConfig(config);
|
|
7107
|
+
console.log("");
|
|
7108
|
+
console.log(chalk7.green(" \u2713 Telegram access reset."));
|
|
7109
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
7110
|
+
if (!getDaemonStatus().running) {
|
|
7111
|
+
console.log(chalk7.dim(" New private Telegram users can send /start to request access."));
|
|
7112
|
+
console.log(chalk7.dim(" The first request must be approved from the CLI with `mercury telegram approve <pairing-code>`."));
|
|
5700
7113
|
}
|
|
5701
7114
|
console.log("");
|
|
5702
7115
|
});
|