@cosmicstack/mercury-agent 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -11
- package/dist/index.js +2492 -986
- 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: {
|
|
@@ -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,110 +891,654 @@ var Lifecycle = class {
|
|
|
761
891
|
}
|
|
762
892
|
};
|
|
763
893
|
|
|
764
|
-
// src/
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
this.
|
|
776
|
-
this.lifecycle = new Lifecycle();
|
|
777
|
-
this.scheduler = scheduler;
|
|
778
|
-
this.capabilities = capabilities;
|
|
779
|
-
this.telegramStreaming = config.channels.telegram.streaming ?? true;
|
|
780
|
-
this.scheduler.setOnScheduledTask(async (manifest) => this.handleScheduledTask(manifest));
|
|
781
|
-
this.channels.onIncomingMessage((msg) => this.enqueueMessage(msg));
|
|
782
|
-
this.scheduler.onHeartbeat(async () => {
|
|
783
|
-
await this.heartbeat();
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
config;
|
|
787
|
-
providers;
|
|
788
|
-
identity;
|
|
789
|
-
shortTerm;
|
|
790
|
-
longTerm;
|
|
791
|
-
episodic;
|
|
792
|
-
channels;
|
|
793
|
-
tokenBudget;
|
|
794
|
-
lifecycle;
|
|
795
|
-
scheduler;
|
|
796
|
-
capabilities;
|
|
797
|
-
running = false;
|
|
798
|
-
messageQueue = [];
|
|
799
|
-
processing = false;
|
|
800
|
-
telegramStreaming;
|
|
801
|
-
enqueueMessage(msg) {
|
|
802
|
-
logger.info({ from: msg.channelType, content: msg.content.slice(0, 50) }, "Message enqueued");
|
|
803
|
-
this.messageQueue.push(msg);
|
|
804
|
-
this.processQueue();
|
|
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;
|
|
805
906
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
if (this.messageQueue.length === 0) return;
|
|
809
|
-
if (!this.lifecycle.is("idle")) return;
|
|
810
|
-
this.processing = true;
|
|
811
|
-
while (this.messageQueue.length > 0) {
|
|
812
|
-
const msg = this.messageQueue.shift();
|
|
813
|
-
try {
|
|
814
|
-
await this.handleMessage(msg);
|
|
815
|
-
} catch (err) {
|
|
816
|
-
logger.error({ err, msg: msg.content.slice(0, 50) }, "Failed to handle message");
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
this.processing = false;
|
|
907
|
+
onMessage(handler) {
|
|
908
|
+
this.messageHandler = handler;
|
|
820
909
|
}
|
|
821
|
-
|
|
822
|
-
this.
|
|
823
|
-
logger.info({ name: this.config.identity.name }, "Mercury is being born...");
|
|
824
|
-
this.lifecycle.transition("onboarding");
|
|
910
|
+
emit(message) {
|
|
911
|
+
this.messageHandler?.(message);
|
|
825
912
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
const
|
|
835
|
-
|
|
913
|
+
};
|
|
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;
|
|
836
926
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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 || "";
|
|
843
969
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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 || "";
|
|
851
998
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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)}`);
|
|
864
1026
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1027
|
+
}
|
|
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]);
|
|
1096
|
+
}
|
|
1097
|
+
for (let i = 0; i < codeBlocks.length; i++) {
|
|
1098
|
+
out = out.replace(`__CODEBLOCK_${i}__`, codeBlocks[i]);
|
|
1099
|
+
}
|
|
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
|
+
}
|
|
1150
|
+
}
|
|
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);
|
|
1185
|
+
}
|
|
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");
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (key.name === "up") {
|
|
1198
|
+
activeIndex = (activeIndex - 1 + options.length) % options.length;
|
|
1199
|
+
render();
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (key.name === "down") {
|
|
1203
|
+
activeIndex = (activeIndex + 1) % options.length;
|
|
1204
|
+
render();
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (key.name === "return") {
|
|
1208
|
+
const selected = options[activeIndex]?.value ?? "";
|
|
1209
|
+
cleanup();
|
|
1210
|
+
stdout.write("\n");
|
|
1211
|
+
resolve13(selected);
|
|
1212
|
+
}
|
|
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();
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
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();
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
closeActiveMenu() {
|
|
1357
|
+
if (!this.menuAbortController?.signal.aborted) {
|
|
1358
|
+
this.menuAbortController?.abort();
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
beginOutput() {
|
|
1362
|
+
this.outputInProgress += 1;
|
|
1363
|
+
}
|
|
1364
|
+
endOutput() {
|
|
1365
|
+
this.outputInProgress = Math.max(0, this.outputInProgress - 1);
|
|
1366
|
+
if (this.menuDepth === 0 && this.outputInProgress === 0) {
|
|
1367
|
+
this.showPrompt();
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
suspendPrompt() {
|
|
1371
|
+
if (!this.rl) return;
|
|
1372
|
+
process.stdout.write("\n");
|
|
1373
|
+
this.rl.close();
|
|
1374
|
+
this.rl = null;
|
|
1375
|
+
}
|
|
1376
|
+
resumePrompt() {
|
|
1377
|
+
if (!this.ready || this.rl) return;
|
|
1378
|
+
this.createInterface();
|
|
1379
|
+
}
|
|
1380
|
+
async prompt(question) {
|
|
1381
|
+
return new Promise((resolve13) => {
|
|
1382
|
+
this.rl?.question(question, (answer) => resolve13(answer.trim()));
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
async askPermission(prompt) {
|
|
1386
|
+
return new Promise((resolve13) => {
|
|
1387
|
+
console.log("");
|
|
1388
|
+
console.log(chalk3.yellow(` \u26A0 ${prompt}`));
|
|
1389
|
+
this.rl?.question(chalk3.yellow(" > "), (answer) => {
|
|
1390
|
+
resolve13(answer.trim());
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
|
|
1396
|
+
// src/core/agent.ts
|
|
1397
|
+
var ToolCallLoopDetector = class {
|
|
1398
|
+
recentCalls = [];
|
|
1399
|
+
maxEntries = 10;
|
|
1400
|
+
record(toolName, params) {
|
|
1401
|
+
const paramsKey = JSON.stringify(params).slice(0, 100);
|
|
1402
|
+
this.recentCalls.push({ tool: toolName, params: paramsKey });
|
|
1403
|
+
if (this.recentCalls.length > this.maxEntries) {
|
|
1404
|
+
this.recentCalls.shift();
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
detect() {
|
|
1408
|
+
if (this.recentCalls.length < 3) return null;
|
|
1409
|
+
const last = this.recentCalls[this.recentCalls.length - 1];
|
|
1410
|
+
let consecutiveCount = 0;
|
|
1411
|
+
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
1412
|
+
if (this.recentCalls[i].tool === last.tool && this.recentCalls[i].params === last.params) {
|
|
1413
|
+
consecutiveCount++;
|
|
1414
|
+
} else {
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (consecutiveCount >= 3) {
|
|
1419
|
+
return { tool: last.tool, count: consecutiveCount };
|
|
1420
|
+
}
|
|
1421
|
+
const lastTool = last.tool;
|
|
1422
|
+
let toolCount = 0;
|
|
1423
|
+
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
1424
|
+
if (this.recentCalls[i].tool === lastTool) {
|
|
1425
|
+
toolCount++;
|
|
1426
|
+
} else {
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (toolCount >= 4) {
|
|
1431
|
+
return { tool: lastTool, count: toolCount };
|
|
1432
|
+
}
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
reset() {
|
|
1436
|
+
this.recentCalls = [];
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
var MAX_STEPS = 10;
|
|
1440
|
+
var Agent = class {
|
|
1441
|
+
constructor(config, providers, identity, shortTerm, longTerm, episodic, channels, tokenBudget, capabilities, scheduler) {
|
|
1442
|
+
this.config = config;
|
|
1443
|
+
this.providers = providers;
|
|
1444
|
+
this.identity = identity;
|
|
1445
|
+
this.shortTerm = shortTerm;
|
|
1446
|
+
this.longTerm = longTerm;
|
|
1447
|
+
this.episodic = episodic;
|
|
1448
|
+
this.channels = channels;
|
|
1449
|
+
this.tokenBudget = tokenBudget;
|
|
1450
|
+
this.lifecycle = new Lifecycle();
|
|
1451
|
+
this.scheduler = scheduler;
|
|
1452
|
+
this.capabilities = capabilities;
|
|
1453
|
+
this.telegramStreaming = config.channels.telegram.streaming ?? true;
|
|
1454
|
+
this.scheduler.setOnScheduledTask(async (manifest) => this.handleScheduledTask(manifest));
|
|
1455
|
+
this.channels.onIncomingMessage((msg) => this.enqueueMessage(msg));
|
|
1456
|
+
this.scheduler.onHeartbeat(async () => {
|
|
1457
|
+
await this.heartbeat();
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
config;
|
|
1461
|
+
providers;
|
|
1462
|
+
identity;
|
|
1463
|
+
shortTerm;
|
|
1464
|
+
longTerm;
|
|
1465
|
+
episodic;
|
|
1466
|
+
channels;
|
|
1467
|
+
tokenBudget;
|
|
1468
|
+
lifecycle;
|
|
1469
|
+
scheduler;
|
|
1470
|
+
capabilities;
|
|
1471
|
+
running = false;
|
|
1472
|
+
messageQueue = [];
|
|
1473
|
+
processing = false;
|
|
1474
|
+
telegramStreaming;
|
|
1475
|
+
enqueueMessage(msg) {
|
|
1476
|
+
logger.info({ from: msg.channelType, content: msg.content.slice(0, 50) }, "Message enqueued");
|
|
1477
|
+
this.messageQueue.push(msg);
|
|
1478
|
+
this.processQueue();
|
|
1479
|
+
}
|
|
1480
|
+
async processQueue() {
|
|
1481
|
+
if (this.processing) return;
|
|
1482
|
+
if (this.messageQueue.length === 0) return;
|
|
1483
|
+
if (!this.lifecycle.is("idle")) return;
|
|
1484
|
+
this.processing = true;
|
|
1485
|
+
while (this.messageQueue.length > 0) {
|
|
1486
|
+
const msg = this.messageQueue.shift();
|
|
1487
|
+
try {
|
|
1488
|
+
await this.handleMessage(msg);
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
logger.error({ err, msg: msg.content.slice(0, 50) }, "Failed to handle message");
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
this.processing = false;
|
|
1494
|
+
}
|
|
1495
|
+
async birth() {
|
|
1496
|
+
this.lifecycle.transition("birthing");
|
|
1497
|
+
logger.info({ name: this.config.identity.name }, "Mercury is being born...");
|
|
1498
|
+
this.lifecycle.transition("onboarding");
|
|
1499
|
+
}
|
|
1500
|
+
async wake() {
|
|
1501
|
+
this.lifecycle.transition("onboarding");
|
|
1502
|
+
this.lifecycle.transition("idle");
|
|
1503
|
+
this.scheduler.restorePersistedTasks();
|
|
1504
|
+
this.scheduler.startHeartbeat();
|
|
1505
|
+
await this.channels.startAll();
|
|
1506
|
+
this.running = true;
|
|
1507
|
+
const activeChannels = this.channels.getActiveChannels();
|
|
1508
|
+
const toolNames = this.capabilities.getToolNames();
|
|
1509
|
+
logger.info({ channels: activeChannels, tools: toolNames }, "Mercury is awake");
|
|
1510
|
+
}
|
|
1511
|
+
async sleep() {
|
|
1512
|
+
this.running = false;
|
|
1513
|
+
this.scheduler.stopAll();
|
|
1514
|
+
await this.channels.stopAll();
|
|
1515
|
+
this.lifecycle.transition("sleeping");
|
|
1516
|
+
logger.info("Mercury is sleeping");
|
|
1517
|
+
}
|
|
1518
|
+
async handleMessage(msg) {
|
|
1519
|
+
this.lifecycle.transition("thinking");
|
|
1520
|
+
const startTime = Date.now();
|
|
1521
|
+
const isInternal = msg.channelType === "internal";
|
|
1522
|
+
const isScheduled = msg.senderId === "system" && msg.channelType !== "internal";
|
|
1523
|
+
if (isInternal || isScheduled) {
|
|
1524
|
+
this.capabilities.permissions.setAutoApproveAll(true);
|
|
1525
|
+
}
|
|
1526
|
+
try {
|
|
1527
|
+
const trimmed = msg.content.trim();
|
|
1528
|
+
if (trimmed.startsWith("/budget")) {
|
|
1529
|
+
const subcommand = trimmed.slice("/budget".length).trim();
|
|
1530
|
+
await this.handleBudgetCommand(subcommand || "status", msg.channelType, msg.channelId);
|
|
1531
|
+
this.lifecycle.transition("idle");
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
if (trimmed === "/budget_override") {
|
|
1535
|
+
await this.handleBudgetCommand("override", msg.channelType, msg.channelId);
|
|
1536
|
+
this.lifecycle.transition("idle");
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (trimmed === "/budget_reset") {
|
|
1540
|
+
await this.handleBudgetCommand("reset", msg.channelType, msg.channelId);
|
|
1541
|
+
this.lifecycle.transition("idle");
|
|
868
1542
|
return;
|
|
869
1543
|
}
|
|
870
1544
|
if (trimmed.startsWith("/budget_set")) {
|
|
@@ -923,6 +1597,30 @@ You can override this:
|
|
|
923
1597
|
const recentMemory = this.shortTerm.getRecent(msg.channelId, 10);
|
|
924
1598
|
const relevantFacts = this.longTerm.search(msg.content, 3);
|
|
925
1599
|
const messages = [];
|
|
1600
|
+
const recentSteps = this.shortTerm.getRecent(msg.channelId, 4);
|
|
1601
|
+
let loopWarning = null;
|
|
1602
|
+
if (recentSteps.length >= 3) {
|
|
1603
|
+
const toolCallPattern = /\[Using: (.+?)\]/g;
|
|
1604
|
+
const toolCalls = [];
|
|
1605
|
+
for (const m of recentSteps) {
|
|
1606
|
+
if (m.role === "assistant") {
|
|
1607
|
+
let match;
|
|
1608
|
+
while ((match = toolCallPattern.exec(m.content)) !== null) {
|
|
1609
|
+
toolCalls.push(match[1]);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (toolCalls.length >= 3) {
|
|
1614
|
+
const last3 = toolCalls.slice(-3);
|
|
1615
|
+
if (last3[0] === last3[1] && last3[1] === last3[2]) {
|
|
1616
|
+
loopWarning = `[SYSTEM WARNING] You have called ${last3[0]} 3+ times in a row with the same result. Stop repeating this call. Try a different approach \u2014 if you're failing on permissions, try a different path. If you're failing on git push auth, use github_api with PUT /repos/{owner}/{repo}/contents/{path} to push files directly through the API.`;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (loopWarning) {
|
|
1621
|
+
messages.push({ role: "user", content: loopWarning });
|
|
1622
|
+
messages.push({ role: "assistant", content: "Understood. I will try a different approach." });
|
|
1623
|
+
}
|
|
926
1624
|
if (relevantFacts.length > 0) {
|
|
927
1625
|
messages.push({
|
|
928
1626
|
role: "user",
|
|
@@ -952,6 +1650,7 @@ You can override this:
|
|
|
952
1650
|
let usedProvider = null;
|
|
953
1651
|
let lastError = null;
|
|
954
1652
|
let streamedText = "";
|
|
1653
|
+
const loopDetector = new ToolCallLoopDetector();
|
|
955
1654
|
const canStream = msg.channelType === "cli" || msg.channelType === "telegram" && this.telegramStreaming;
|
|
956
1655
|
for (const provider of fallbackIterator) {
|
|
957
1656
|
try {
|
|
@@ -967,6 +1666,13 @@ You can override this:
|
|
|
967
1666
|
if (toolCalls && toolCalls.length > 0) {
|
|
968
1667
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
969
1668
|
logger.info({ tools: names }, "Tool call step");
|
|
1669
|
+
for (const tc of toolCalls) {
|
|
1670
|
+
loopDetector.record(tc.toolName, tc.args);
|
|
1671
|
+
}
|
|
1672
|
+
const loop = loopDetector.detect();
|
|
1673
|
+
if (loop) {
|
|
1674
|
+
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected");
|
|
1675
|
+
}
|
|
970
1676
|
await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
|
|
971
1677
|
});
|
|
972
1678
|
}
|
|
@@ -1004,6 +1710,13 @@ You can override this:
|
|
|
1004
1710
|
if (toolCalls && toolCalls.length > 0) {
|
|
1005
1711
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
1006
1712
|
logger.info({ tools: names }, "Tool call step");
|
|
1713
|
+
for (const tc of toolCalls) {
|
|
1714
|
+
loopDetector.record(tc.toolName, tc.args);
|
|
1715
|
+
}
|
|
1716
|
+
const loop = loopDetector.detect();
|
|
1717
|
+
if (loop) {
|
|
1718
|
+
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected");
|
|
1719
|
+
}
|
|
1007
1720
|
if (channel && msg.channelType !== "internal") {
|
|
1008
1721
|
await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
|
|
1009
1722
|
});
|
|
@@ -1098,16 +1811,35 @@ You can override this:
|
|
|
1098
1811
|
if (this.tokenBudget.getUsagePercentage() > 70) {
|
|
1099
1812
|
prompt += "\nBe concise to conserve tokens.";
|
|
1100
1813
|
}
|
|
1814
|
+
prompt += `
|
|
1815
|
+
|
|
1816
|
+
Environment:
|
|
1817
|
+
- Platform: ${process.platform}
|
|
1818
|
+
- Working directory: ${this.capabilities.getCwd()}`;
|
|
1101
1819
|
const toolNames = this.capabilities.getToolNames();
|
|
1102
1820
|
const githubTools = ["create_pr", "review_pr", "list_issues", "create_issue", "github_api"];
|
|
1103
1821
|
const hasGitHub = githubTools.some((t) => toolNames.includes(t));
|
|
1104
1822
|
if (hasGitHub) {
|
|
1105
|
-
let githubHint = "\n\nGitHub companion is active.
|
|
1823
|
+
let githubHint = "\n\nGitHub companion is active.";
|
|
1106
1824
|
const { defaultOwner, defaultRepo } = this.config.github;
|
|
1107
1825
|
if (defaultOwner && defaultRepo) {
|
|
1108
1826
|
githubHint += ` Default repo: ${defaultOwner}/${defaultRepo}. Use this when the user doesn't specify a repo.`;
|
|
1109
1827
|
}
|
|
1110
|
-
githubHint +=
|
|
1828
|
+
githubHint += `
|
|
1829
|
+
|
|
1830
|
+
Available GitHub tools and when to use them:
|
|
1831
|
+
- git_add, git_commit, git_push: LOCAL git operations (stage, commit, push to a remote you have SSH/auth access to). All commits include "Co-authored-by: Mercury <mercury@cosmicstack.org>".
|
|
1832
|
+
- create_pr: Create a pull request on GitHub. The head branch must already exist on the remote.
|
|
1833
|
+
- review_pr: Get PR details and optionally post a review comment.
|
|
1834
|
+
- list_issues, create_issue: Browse and file issues.
|
|
1835
|
+
- github_api: Raw GitHub API access. IMPORTANT USE CASES:
|
|
1836
|
+
- Push files directly to GitHub via PUT /repos/{owner}/{repo}/contents/{path} when git push fails due to auth. The body must include "message" and "content" (base64-encoded file content). This creates a commit on GitHub with Mercury as co-author.
|
|
1837
|
+
- Delete files via DELETE /repos/{owner}/{repo}/contents/{path} with a "message" and "sha" in the body.
|
|
1838
|
+
- Any other GitHub API operation not covered by the other tools.
|
|
1839
|
+
|
|
1840
|
+
When the user asks to "push to GitHub" or "upload files" and git push fails, use github_api with PUT /repos/{owner}/{repo}/contents/{path} to push content directly through the API. This bypasses local git entirely.
|
|
1841
|
+
|
|
1842
|
+
Always specify owner and repo parameters on GitHub tools. The user's GitHub username is ${this.config.github.username || "not set"}.'`;
|
|
1111
1843
|
prompt += githubHint;
|
|
1112
1844
|
}
|
|
1113
1845
|
return prompt;
|
|
@@ -1253,40 +1985,210 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
|
|
|
1253
1985
|
await channel.send("Please specify the new budget. Usage: `/budget set 100000` or type e.g. `3 100000`", channelId);
|
|
1254
1986
|
return;
|
|
1255
1987
|
}
|
|
1256
|
-
this.tokenBudget.setBudget(newBudget);
|
|
1257
|
-
await channel.send(`Daily budget updated to ${newBudget.toLocaleString()} tokens. ${this.tokenBudget.getStatusText()}`, channelId);
|
|
1258
|
-
} else if (action === "cancel" || action === "4") {
|
|
1259
|
-
await channel.send(`Cancelled. ${this.tokenBudget.getStatusText()}`, channelId);
|
|
1260
|
-
} else if (!action || action === "status") {
|
|
1261
|
-
await channel.send(this.tokenBudget.getStatusText(), channelId);
|
|
1262
|
-
} else {
|
|
1263
|
-
await channel.send(`Unknown budget command "${action}". Available: /budget, /budget override, /budget reset, /budget set <number>, /budget status`, channelId);
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
async handleChatCommand(content, channelType, channelId) {
|
|
1267
|
-
const
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
1988
|
+
this.tokenBudget.setBudget(newBudget);
|
|
1989
|
+
await channel.send(`Daily budget updated to ${newBudget.toLocaleString()} tokens. ${this.tokenBudget.getStatusText()}`, channelId);
|
|
1990
|
+
} else if (action === "cancel" || action === "4") {
|
|
1991
|
+
await channel.send(`Cancelled. ${this.tokenBudget.getStatusText()}`, channelId);
|
|
1992
|
+
} else if (!action || action === "status") {
|
|
1993
|
+
await channel.send(this.tokenBudget.getStatusText(), channelId);
|
|
1994
|
+
} else {
|
|
1995
|
+
await channel.send(`Unknown budget command "${action}". Available: /budget, /budget override, /budget reset, /budget set <number>, /budget status`, channelId);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
async handleChatCommand(content, channelType, channelId) {
|
|
1999
|
+
const trimmed = content.trim();
|
|
2000
|
+
const cmd = trimmed.toLowerCase();
|
|
2001
|
+
const channel = this.channels.get(channelType);
|
|
2002
|
+
if (!channel) return false;
|
|
2003
|
+
const ctx = this.capabilities.getChatCommandContext();
|
|
2004
|
+
if (!ctx) return false;
|
|
2005
|
+
if (cmd === "/help") {
|
|
2006
|
+
await channel.send(ctx.manual(), channelId);
|
|
2007
|
+
return true;
|
|
2008
|
+
}
|
|
2009
|
+
if (cmd === "/status") {
|
|
2010
|
+
const config = ctx.config();
|
|
2011
|
+
const budget = ctx.tokenBudget();
|
|
2012
|
+
const lines = [
|
|
2013
|
+
`**${config.identity.name}** \u2014 Status`,
|
|
2014
|
+
`Owner: ${config.identity.owner || "(not set)"}`,
|
|
2015
|
+
`Provider: ${config.providers.default}`,
|
|
2016
|
+
`Telegram: ${config.channels.telegram.enabled ? "enabled" : "disabled"}`,
|
|
2017
|
+
`Telegram access: ${getTelegramAccessSummary(config)}`,
|
|
2018
|
+
`Budget: ${budget.getStatusText()}`,
|
|
2019
|
+
`Skills: ${ctx.skillNames().length > 0 ? ctx.skillNames().join(", ") : "none"}`
|
|
2020
|
+
];
|
|
2021
|
+
await channel.send(lines.join("\n"), channelId);
|
|
2022
|
+
return true;
|
|
2023
|
+
}
|
|
2024
|
+
if (cmd.startsWith("/telegram")) {
|
|
2025
|
+
if (channelType !== "cli") {
|
|
2026
|
+
await channel.send("`/telegram` is only available from the Mercury CLI chat.", channelId);
|
|
2027
|
+
return true;
|
|
2028
|
+
}
|
|
2029
|
+
const config = ctx.config();
|
|
2030
|
+
const rawSubcommand = trimmed.slice("/telegram".length).trim();
|
|
2031
|
+
if (!rawSubcommand && channel instanceof CLIChannel) {
|
|
2032
|
+
await channel.withMenu(async (select) => {
|
|
2033
|
+
await this.openCliTelegramMenu(channel, channelId, select);
|
|
2034
|
+
});
|
|
2035
|
+
return true;
|
|
2036
|
+
}
|
|
2037
|
+
const parts = rawSubcommand.split(/\s+/).filter(Boolean);
|
|
2038
|
+
const action = parts[0]?.toLowerCase() || "help";
|
|
2039
|
+
const formatTelegramUser2 = (user) => {
|
|
2040
|
+
const username = user.username ? ` (@${user.username})` : "";
|
|
2041
|
+
const firstName = user.firstName ? ` ${user.firstName}` : "";
|
|
2042
|
+
const pairingCode = user.pairingCode ? ` [code: ${user.pairingCode}]` : "";
|
|
2043
|
+
return `${user.userId}${username}${firstName}${pairingCode}`;
|
|
2044
|
+
};
|
|
2045
|
+
const sendTelegramOverview = async () => {
|
|
2046
|
+
const lines = [
|
|
2047
|
+
"**Telegram Management**",
|
|
2048
|
+
"",
|
|
2049
|
+
`Access: ${getTelegramAccessSummary(config)}`,
|
|
2050
|
+
`Admins: ${config.channels.telegram.admins.length > 0 ? config.channels.telegram.admins.map(formatTelegramUser2).join(", ") : "none"}`,
|
|
2051
|
+
`Members: ${config.channels.telegram.members.length > 0 ? config.channels.telegram.members.map(formatTelegramUser2).join(", ") : "none"}`,
|
|
2052
|
+
`Pending: ${config.channels.telegram.pending.length > 0 ? config.channels.telegram.pending.map(formatTelegramUser2).join(", ") : "none"}`,
|
|
2053
|
+
"",
|
|
2054
|
+
"Commands:",
|
|
2055
|
+
"\u2022 `/telegram pending`",
|
|
2056
|
+
"\u2022 `/telegram users`",
|
|
2057
|
+
"\u2022 `/telegram approve <pairing-code|user-id>`",
|
|
2058
|
+
"\u2022 `/telegram reject <user-id>`",
|
|
2059
|
+
"\u2022 `/telegram remove <user-id>`",
|
|
2060
|
+
"\u2022 `/telegram promote <user-id>`",
|
|
2061
|
+
"\u2022 `/telegram demote <user-id>`",
|
|
2062
|
+
"\u2022 `/telegram reset`"
|
|
2063
|
+
];
|
|
2064
|
+
await channel.send(lines.join("\n"), channelId);
|
|
2065
|
+
};
|
|
2066
|
+
if (action === "help" || action === "status") {
|
|
2067
|
+
await sendTelegramOverview();
|
|
2068
|
+
return true;
|
|
2069
|
+
}
|
|
2070
|
+
if (action === "pending") {
|
|
2071
|
+
const pending = getTelegramPendingRequests(config);
|
|
2072
|
+
const lines = [
|
|
2073
|
+
"**Telegram Pending Requests**",
|
|
2074
|
+
"",
|
|
2075
|
+
pending.length > 0 ? pending.map(formatTelegramUser2).join("\n") : "No pending Telegram requests."
|
|
2076
|
+
];
|
|
2077
|
+
await channel.send(lines.join("\n"), channelId);
|
|
2078
|
+
return true;
|
|
2079
|
+
}
|
|
2080
|
+
if (action === "users") {
|
|
2081
|
+
const approved = getTelegramApprovedUsers(config);
|
|
2082
|
+
const lines = [
|
|
2083
|
+
"**Telegram Approved Users**",
|
|
2084
|
+
"",
|
|
2085
|
+
`Admins: ${config.channels.telegram.admins.length > 0 ? config.channels.telegram.admins.map(formatTelegramUser2).join(", ") : "none"}`,
|
|
2086
|
+
`Members: ${config.channels.telegram.members.length > 0 ? config.channels.telegram.members.map(formatTelegramUser2).join(", ") : "none"}`,
|
|
2087
|
+
"",
|
|
2088
|
+
`Total approved: ${approved.length}`
|
|
2089
|
+
];
|
|
2090
|
+
await channel.send(lines.join("\n"), channelId);
|
|
2091
|
+
return true;
|
|
2092
|
+
}
|
|
2093
|
+
if (action === "approve") {
|
|
2094
|
+
const value = parts[1];
|
|
2095
|
+
if (!value) {
|
|
2096
|
+
await channel.send("Usage: `/telegram approve <pairing-code|user-id>`", channelId);
|
|
2097
|
+
return true;
|
|
2098
|
+
}
|
|
2099
|
+
let approved = approveTelegramPendingRequestByPairingCode(config, value);
|
|
2100
|
+
let resultLabel = value;
|
|
2101
|
+
if (!approved) {
|
|
2102
|
+
const userId = Number(value);
|
|
2103
|
+
if (!isNaN(userId)) {
|
|
2104
|
+
approved = approveTelegramPendingRequest(config, userId, "member");
|
|
2105
|
+
resultLabel = userId.toString();
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
if (!approved) {
|
|
2109
|
+
await channel.send(`No pending Telegram request found for \`${resultLabel}\`.`, channelId);
|
|
2110
|
+
return true;
|
|
2111
|
+
}
|
|
2112
|
+
saveConfig(config);
|
|
2113
|
+
await channel.send(`Approved Telegram user ${formatTelegramUser2(approved)}.`, channelId);
|
|
2114
|
+
return true;
|
|
2115
|
+
}
|
|
2116
|
+
if (action === "reject") {
|
|
2117
|
+
const value = Number(parts[1]);
|
|
2118
|
+
if (isNaN(value)) {
|
|
2119
|
+
await channel.send("Usage: `/telegram reject <user-id>`", channelId);
|
|
2120
|
+
return true;
|
|
2121
|
+
}
|
|
2122
|
+
const rejected = rejectTelegramPendingRequest(config, value);
|
|
2123
|
+
if (!rejected) {
|
|
2124
|
+
await channel.send(`No pending Telegram request found for \`${value}\`.`, channelId);
|
|
2125
|
+
return true;
|
|
2126
|
+
}
|
|
2127
|
+
saveConfig(config);
|
|
2128
|
+
await channel.send(`Rejected Telegram request for ${formatTelegramUser2(rejected)}.`, channelId);
|
|
2129
|
+
return true;
|
|
2130
|
+
}
|
|
2131
|
+
if (action === "remove") {
|
|
2132
|
+
const value = Number(parts[1]);
|
|
2133
|
+
if (isNaN(value)) {
|
|
2134
|
+
await channel.send("Usage: `/telegram remove <user-id>`", channelId);
|
|
2135
|
+
return true;
|
|
2136
|
+
}
|
|
2137
|
+
const removed = removeTelegramUser(config, value);
|
|
2138
|
+
if (!removed) {
|
|
2139
|
+
await channel.send(`No approved Telegram user found for \`${value}\`.`, channelId);
|
|
2140
|
+
return true;
|
|
2141
|
+
}
|
|
2142
|
+
saveConfig(config);
|
|
2143
|
+
await channel.send(`Removed Telegram access for ${formatTelegramUser2(removed)}.`, channelId);
|
|
2144
|
+
return true;
|
|
2145
|
+
}
|
|
2146
|
+
if (action === "promote") {
|
|
2147
|
+
const value = Number(parts[1]);
|
|
2148
|
+
if (isNaN(value)) {
|
|
2149
|
+
await channel.send("Usage: `/telegram promote <user-id>`", channelId);
|
|
2150
|
+
return true;
|
|
2151
|
+
}
|
|
2152
|
+
const promoted = promoteTelegramUserToAdmin(config, value);
|
|
2153
|
+
if (!promoted) {
|
|
2154
|
+
await channel.send(`No Telegram member found for \`${value}\`.`, channelId);
|
|
2155
|
+
return true;
|
|
2156
|
+
}
|
|
2157
|
+
saveConfig(config);
|
|
2158
|
+
await channel.send(`Promoted ${formatTelegramUser2(promoted)} to Telegram admin.`, channelId);
|
|
2159
|
+
return true;
|
|
2160
|
+
}
|
|
2161
|
+
if (action === "demote") {
|
|
2162
|
+
const value = Number(parts[1]);
|
|
2163
|
+
if (isNaN(value)) {
|
|
2164
|
+
await channel.send("Usage: `/telegram demote <user-id>`", channelId);
|
|
2165
|
+
return true;
|
|
2166
|
+
}
|
|
2167
|
+
const demoted = demoteTelegramAdmin(config, value);
|
|
2168
|
+
if (!demoted) {
|
|
2169
|
+
await channel.send("Could not demote that Telegram admin. Mercury must keep at least one admin.", channelId);
|
|
2170
|
+
return true;
|
|
2171
|
+
}
|
|
2172
|
+
saveConfig(config);
|
|
2173
|
+
await channel.send(`Demoted ${formatTelegramUser2(demoted)} to Telegram member.`, channelId);
|
|
2174
|
+
return true;
|
|
2175
|
+
}
|
|
2176
|
+
if (action === "reset" || action === "unpair") {
|
|
2177
|
+
config.channels.telegram.admins = [];
|
|
2178
|
+
config.channels.telegram.members = [];
|
|
2179
|
+
config.channels.telegram.pending = [];
|
|
2180
|
+
saveConfig(config);
|
|
2181
|
+
await channel.send("Telegram access reset. New users can send /start to begin pairing again.", channelId);
|
|
2182
|
+
return true;
|
|
2183
|
+
}
|
|
2184
|
+
await channel.send(
|
|
2185
|
+
`Unknown Telegram command "${action}". Try \`/telegram\`, \`/telegram pending\`, or \`/telegram users\`.`,
|
|
2186
|
+
channelId
|
|
2187
|
+
);
|
|
1274
2188
|
return true;
|
|
1275
2189
|
}
|
|
1276
|
-
if (cmd === "/
|
|
1277
|
-
|
|
1278
|
-
const budget = ctx.tokenBudget();
|
|
1279
|
-
const telegramPairing = config.channels.telegram.pairedUserId != null ? `paired to user ${config.channels.telegram.pairedUserId}${config.channels.telegram.pairedUsername ? ` (@${config.channels.telegram.pairedUsername})` : ""}` : "unpaired";
|
|
1280
|
-
const lines = [
|
|
1281
|
-
`**${config.identity.name}** \u2014 Status`,
|
|
1282
|
-
`Owner: ${config.identity.owner || "(not set)"}`,
|
|
1283
|
-
`Provider: ${config.providers.default}`,
|
|
1284
|
-
`Telegram: ${config.channels.telegram.enabled ? "enabled" : "disabled"}`,
|
|
1285
|
-
`Telegram pairing: ${telegramPairing}`,
|
|
1286
|
-
`Budget: ${budget.getStatusText()}`,
|
|
1287
|
-
`Skills: ${ctx.skillNames().length > 0 ? ctx.skillNames().join(", ") : "none"}`
|
|
1288
|
-
];
|
|
1289
|
-
await channel.send(lines.join("\n"), channelId);
|
|
2190
|
+
if ((cmd === "/" || cmd === "/menu") && channelType === "cli" && channel instanceof CLIChannel) {
|
|
2191
|
+
await this.openCliCommandMenu(channel, channelId);
|
|
1290
2192
|
return true;
|
|
1291
2193
|
}
|
|
1292
2194
|
if (cmd === "/tools") {
|
|
@@ -1338,495 +2240,374 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
|
|
|
1338
2240
|
}
|
|
1339
2241
|
return false;
|
|
1340
2242
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
heartbeatHandler;
|
|
1383
|
-
heartbeatTimer = null;
|
|
1384
|
-
setOnScheduledTask(handler) {
|
|
1385
|
-
this.onScheduledTask = handler;
|
|
1386
|
-
}
|
|
1387
|
-
onHeartbeat(handler) {
|
|
1388
|
-
this.heartbeatHandler = handler;
|
|
1389
|
-
}
|
|
1390
|
-
startHeartbeat() {
|
|
1391
|
-
if (this.heartbeatTimer) return;
|
|
1392
|
-
const ms = this.heartbeatIntervalMinutes * 60 * 1e3;
|
|
1393
|
-
logger.info({ intervalMin: this.heartbeatIntervalMinutes }, "Heartbeat started");
|
|
1394
|
-
this.heartbeatTimer = setInterval(async () => {
|
|
1395
|
-
try {
|
|
1396
|
-
await this.heartbeatHandler?.();
|
|
1397
|
-
} catch (err) {
|
|
1398
|
-
logger.error({ err }, "Heartbeat error");
|
|
1399
|
-
}
|
|
1400
|
-
}, ms);
|
|
1401
|
-
}
|
|
1402
|
-
stopHeartbeat() {
|
|
1403
|
-
if (this.heartbeatTimer) {
|
|
1404
|
-
clearInterval(this.heartbeatTimer);
|
|
1405
|
-
this.heartbeatTimer = null;
|
|
1406
|
-
logger.info("Heartbeat stopped");
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
addTask(task) {
|
|
1410
|
-
if (this.tasks.has(task.id)) {
|
|
1411
|
-
this.removeTask(task.id);
|
|
1412
|
-
}
|
|
1413
|
-
const scheduled = cron.schedule(task.cron, async () => {
|
|
1414
|
-
try {
|
|
1415
|
-
await task.handler();
|
|
1416
|
-
} catch (err) {
|
|
1417
|
-
logger.error({ task: task.id, err }, "Scheduled task error");
|
|
2243
|
+
async openCliCommandMenu(channel, channelId) {
|
|
2244
|
+
const ctx = this.capabilities.getChatCommandContext();
|
|
2245
|
+
if (!ctx) return;
|
|
2246
|
+
await channel.withMenu(async (select) => {
|
|
2247
|
+
while (true) {
|
|
2248
|
+
const streamLabel = this.telegramStreaming ? "Disable Telegram Streaming" : "Enable Telegram Streaming";
|
|
2249
|
+
const action = await select("Mercury Commands", [
|
|
2250
|
+
{ value: "status", label: "Status" },
|
|
2251
|
+
{ value: "telegram", label: "Telegram" },
|
|
2252
|
+
{ value: "tools", label: "Tools" },
|
|
2253
|
+
{ value: "skills", label: "Skills" },
|
|
2254
|
+
{ value: "stream", label: streamLabel },
|
|
2255
|
+
{ value: "help", label: "Help" },
|
|
2256
|
+
{ value: "exit", label: "Exit" }
|
|
2257
|
+
]);
|
|
2258
|
+
if (action === "exit") {
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
if (action === "status") {
|
|
2262
|
+
await this.handleChatCommand("/status", "cli", channelId);
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
if (action === "telegram") {
|
|
2266
|
+
await this.openCliTelegramMenu(channel, channelId, select);
|
|
2267
|
+
continue;
|
|
2268
|
+
}
|
|
2269
|
+
if (action === "tools") {
|
|
2270
|
+
await this.handleChatCommand("/tools", "cli", channelId);
|
|
2271
|
+
continue;
|
|
2272
|
+
}
|
|
2273
|
+
if (action === "skills") {
|
|
2274
|
+
await this.handleChatCommand("/skills", "cli", channelId);
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
2277
|
+
if (action === "stream") {
|
|
2278
|
+
await this.handleChatCommand("/stream", "cli", channelId);
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
if (action === "help") {
|
|
2282
|
+
await channel.send(ctx.manual(), channelId);
|
|
2283
|
+
}
|
|
1418
2284
|
}
|
|
1419
2285
|
});
|
|
1420
|
-
this.tasks.set(task.id, scheduled);
|
|
1421
|
-
logger.info({ id: task.id, cron: task.cron, desc: task.description }, "Task scheduled");
|
|
1422
2286
|
}
|
|
1423
|
-
|
|
1424
|
-
this.
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
2287
|
+
async openCliTelegramMenu(channel, channelId, select) {
|
|
2288
|
+
const ctx = this.capabilities.getChatCommandContext();
|
|
2289
|
+
if (!ctx) return;
|
|
2290
|
+
const formatTelegramUser2 = (user) => {
|
|
2291
|
+
const username = user.username ? ` (@${user.username})` : "";
|
|
2292
|
+
const firstName = user.firstName ? ` ${user.firstName}` : "";
|
|
2293
|
+
const pairingCode = user.pairingCode ? ` [code: ${user.pairingCode}]` : "";
|
|
2294
|
+
return `${user.userId}${username}${firstName}${pairingCode}`;
|
|
2295
|
+
};
|
|
2296
|
+
const selectFromUsers = async (title, users, emptyMessage, backValue = "back") => {
|
|
2297
|
+
if (users.length === 0) {
|
|
2298
|
+
await channel.send(emptyMessage, channelId);
|
|
2299
|
+
return backValue;
|
|
2300
|
+
}
|
|
2301
|
+
return select(title, [
|
|
2302
|
+
...users.map((user) => ({
|
|
2303
|
+
value: user.pairingCode || user.userId.toString(),
|
|
2304
|
+
label: formatTelegramUser2(user)
|
|
2305
|
+
})),
|
|
2306
|
+
{ value: backValue, label: "Back" }
|
|
2307
|
+
]);
|
|
2308
|
+
};
|
|
2309
|
+
while (true) {
|
|
2310
|
+
const config = ctx.config();
|
|
2311
|
+
const action = await select("Telegram Commands", [
|
|
2312
|
+
{ value: "overview", label: "Overview" },
|
|
2313
|
+
{ value: "pending", label: `Pending Requests (${config.channels.telegram.pending.length})` },
|
|
2314
|
+
{ value: "users", label: `Approved Users (${getTelegramApprovedUsers(config).length})` },
|
|
2315
|
+
{ value: "approve", label: "Approve Request" },
|
|
2316
|
+
{ value: "reject", label: "Reject Request" },
|
|
2317
|
+
{ value: "remove", label: "Remove User" },
|
|
2318
|
+
{ value: "promote", label: "Promote to Admin" },
|
|
2319
|
+
{ value: "demote", label: "Demote Admin" },
|
|
2320
|
+
{ value: "reset", label: "Reset Telegram Access" },
|
|
2321
|
+
{ value: "back", label: "Back" },
|
|
2322
|
+
{ value: "exit", label: "Exit" }
|
|
2323
|
+
]);
|
|
2324
|
+
if (action === "exit") {
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
if (action === "back") {
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
if (action === "overview") {
|
|
2331
|
+
await this.handleChatCommand("/telegram status", "cli", channelId);
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
if (action === "pending") {
|
|
2335
|
+
await this.handleChatCommand("/telegram pending", "cli", channelId);
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
if (action === "users") {
|
|
2339
|
+
await this.handleChatCommand("/telegram users", "cli", channelId);
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
if (action === "approve") {
|
|
2343
|
+
const pending = getTelegramPendingRequests(config);
|
|
2344
|
+
const selected = await selectFromUsers(
|
|
2345
|
+
"Approve Telegram Request",
|
|
2346
|
+
pending,
|
|
2347
|
+
"There are no pending Telegram requests to approve."
|
|
2348
|
+
);
|
|
2349
|
+
if (selected === "back") {
|
|
2350
|
+
continue;
|
|
1433
2351
|
}
|
|
2352
|
+
await this.handleChatCommand(`/telegram approve ${selected}`, "cli", channelId);
|
|
2353
|
+
continue;
|
|
1434
2354
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
await this.onScheduledTask(manifest);
|
|
2355
|
+
if (action === "reject") {
|
|
2356
|
+
const pending = getTelegramPendingRequests(config);
|
|
2357
|
+
const selected = await selectFromUsers(
|
|
2358
|
+
"Reject Telegram Request",
|
|
2359
|
+
pending,
|
|
2360
|
+
"There are no pending Telegram requests to reject."
|
|
2361
|
+
);
|
|
2362
|
+
if (selected === "back") {
|
|
2363
|
+
continue;
|
|
1445
2364
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
this.
|
|
2365
|
+
const request = pending.find((entry) => (entry.pairingCode || entry.userId.toString()) === selected);
|
|
2366
|
+
if (!request) {
|
|
2367
|
+
await channel.send("That Telegram request is no longer pending.", channelId);
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
await this.handleChatCommand(`/telegram reject ${request.userId}`, "cli", channelId);
|
|
2371
|
+
continue;
|
|
1452
2372
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
}
|
|
1463
|
-
const timer = this.delayedTasks.get(id);
|
|
1464
|
-
if (timer) {
|
|
1465
|
-
clearTimeout(timer);
|
|
1466
|
-
this.delayedTasks.delete(id);
|
|
1467
|
-
}
|
|
1468
|
-
this.taskManifests.delete(id);
|
|
1469
|
-
}
|
|
1470
|
-
getManifests() {
|
|
1471
|
-
return [...this.taskManifests.values()];
|
|
1472
|
-
}
|
|
1473
|
-
restorePersistedTasks() {
|
|
1474
|
-
const persisted = loadSchedules();
|
|
1475
|
-
for (const manifest of persisted) {
|
|
1476
|
-
if (manifest.delaySeconds) {
|
|
1477
|
-
const executeAt = manifest.executeAt ? new Date(manifest.executeAt) : null;
|
|
1478
|
-
const now = Date.now();
|
|
1479
|
-
if (executeAt && executeAt.getTime() > now) {
|
|
1480
|
-
const remainingMs = executeAt.getTime() - now;
|
|
1481
|
-
manifest.delaySeconds = Math.ceil(remainingMs / 1e3);
|
|
1482
|
-
this.addDelayedTask(manifest);
|
|
1483
|
-
} else {
|
|
1484
|
-
logger.info({ id: manifest.id }, "Delayed task already expired, skipping");
|
|
2373
|
+
if (action === "remove") {
|
|
2374
|
+
const approved = getTelegramApprovedUsers(config);
|
|
2375
|
+
const selected = await selectFromUsers(
|
|
2376
|
+
"Remove Telegram User",
|
|
2377
|
+
approved,
|
|
2378
|
+
"There are no approved Telegram users to remove."
|
|
2379
|
+
);
|
|
2380
|
+
if (selected === "back") {
|
|
2381
|
+
continue;
|
|
1485
2382
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
2383
|
+
const user = approved.find((entry) => entry.userId.toString() === selected);
|
|
2384
|
+
if (!user) {
|
|
2385
|
+
await channel.send("That Telegram user is no longer approved.", channelId);
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
await this.handleChatCommand(`/telegram remove ${user.userId}`, "cli", channelId);
|
|
2389
|
+
continue;
|
|
1490
2390
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
2391
|
+
if (action === "promote") {
|
|
2392
|
+
const members = config.channels.telegram.members;
|
|
2393
|
+
const selected = await selectFromUsers(
|
|
2394
|
+
"Promote Telegram Member",
|
|
2395
|
+
members,
|
|
2396
|
+
"There are no Telegram members available to promote."
|
|
2397
|
+
);
|
|
2398
|
+
if (selected === "back") {
|
|
2399
|
+
continue;
|
|
2400
|
+
}
|
|
2401
|
+
const member = members.find((entry) => entry.userId.toString() === selected);
|
|
2402
|
+
if (!member) {
|
|
2403
|
+
await channel.send("That Telegram member is no longer available.", channelId);
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
await this.handleChatCommand(`/telegram promote ${member.userId}`, "cli", channelId);
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
if (action === "demote") {
|
|
2410
|
+
const admins = config.channels.telegram.admins;
|
|
2411
|
+
const selected = await selectFromUsers(
|
|
2412
|
+
"Demote Telegram Admin",
|
|
2413
|
+
admins,
|
|
2414
|
+
"There are no Telegram admins available to demote."
|
|
2415
|
+
);
|
|
2416
|
+
if (selected === "back") {
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
const admin = admins.find((entry) => entry.userId.toString() === selected);
|
|
2420
|
+
if (!admin) {
|
|
2421
|
+
await channel.send("That Telegram admin is no longer available.", channelId);
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
await this.handleChatCommand(`/telegram demote ${admin.userId}`, "cli", channelId);
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
if (action === "reset") {
|
|
2428
|
+
const confirmation = await select("Reset Telegram Access?", [
|
|
2429
|
+
{ value: "cancel", label: "Cancel" },
|
|
2430
|
+
{ value: "confirm", label: "Reset all Telegram access" },
|
|
2431
|
+
{ value: "back", label: "Back" }
|
|
2432
|
+
]);
|
|
2433
|
+
if (confirmation === "confirm") {
|
|
2434
|
+
clearTelegramAccess(config);
|
|
2435
|
+
saveConfig(config);
|
|
2436
|
+
await channel.send("Telegram access reset. New users can send /start to begin pairing again.", channelId);
|
|
2437
|
+
}
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
1531
2441
|
}
|
|
1532
2442
|
};
|
|
1533
2443
|
|
|
1534
|
-
// src/
|
|
1535
|
-
import
|
|
1536
|
-
import
|
|
1537
|
-
|
|
1538
|
-
|
|
2444
|
+
// src/core/scheduler.ts
|
|
2445
|
+
import cron from "node-cron";
|
|
2446
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
2447
|
+
import { join as join4 } from "path";
|
|
2448
|
+
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
2449
|
+
var SCHEDULES_FILE = "schedules.yaml";
|
|
2450
|
+
function getSchedulesPath() {
|
|
2451
|
+
return join4(getMercuryHome(), SCHEDULES_FILE);
|
|
2452
|
+
}
|
|
2453
|
+
function loadSchedules() {
|
|
2454
|
+
const path3 = getSchedulesPath();
|
|
2455
|
+
if (!existsSync4(path3)) return [];
|
|
1539
2456
|
try {
|
|
1540
|
-
const
|
|
1541
|
-
const
|
|
1542
|
-
return
|
|
1543
|
-
} catch {
|
|
1544
|
-
|
|
2457
|
+
const raw = readFileSync4(path3, "utf-8");
|
|
2458
|
+
const data = parseYaml2(raw);
|
|
2459
|
+
return data.tasks || [];
|
|
2460
|
+
} catch (err) {
|
|
2461
|
+
logger.warn({ err }, "Failed to load schedules.yaml");
|
|
2462
|
+
return [];
|
|
1545
2463
|
}
|
|
1546
2464
|
}
|
|
1547
|
-
function
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
switch (t.type) {
|
|
1553
|
-
case "heading":
|
|
1554
|
-
return renderHeading(t);
|
|
1555
|
-
case "paragraph":
|
|
1556
|
-
return renderInline(t.tokens) + "\n\n";
|
|
1557
|
-
case "strong":
|
|
1558
|
-
return chalk.bold(renderInline(t.tokens));
|
|
1559
|
-
case "em":
|
|
1560
|
-
return chalk.italic(renderInline(t.tokens));
|
|
1561
|
-
case "del":
|
|
1562
|
-
return chalk.dim.strikethrough(renderInline(t.tokens));
|
|
1563
|
-
case "codespan":
|
|
1564
|
-
return chalk.yellow(t.text);
|
|
1565
|
-
case "code":
|
|
1566
|
-
return renderCodeBlock(t);
|
|
1567
|
-
case "list":
|
|
1568
|
-
return renderList(t);
|
|
1569
|
-
case "blockquote":
|
|
1570
|
-
return renderBlockquote(t);
|
|
1571
|
-
case "hr":
|
|
1572
|
-
return chalk.dim("\u2500".repeat(50)) + "\n\n";
|
|
1573
|
-
case "link":
|
|
1574
|
-
return `${chalk.blue.underline(renderInline(t.tokens))} ${chalk.dim(`(${t.href})`)}`;
|
|
1575
|
-
case "image":
|
|
1576
|
-
return chalk.blue(`\u{1F5BC} ${t.title || t.href}`);
|
|
1577
|
-
case "table":
|
|
1578
|
-
return renderTable(t);
|
|
1579
|
-
case "text":
|
|
1580
|
-
if (t.tokens) return renderInline(t.tokens);
|
|
1581
|
-
return t.text || "";
|
|
1582
|
-
case "html":
|
|
1583
|
-
return t.text || "";
|
|
1584
|
-
case "space":
|
|
1585
|
-
return "";
|
|
1586
|
-
default:
|
|
1587
|
-
return t.text || "";
|
|
2465
|
+
function saveSchedules(tasks) {
|
|
2466
|
+
const path3 = getSchedulesPath();
|
|
2467
|
+
const dir = getMercuryHome();
|
|
2468
|
+
if (!existsSync4(dir)) {
|
|
2469
|
+
mkdirSync4(dir, { recursive: true });
|
|
1588
2470
|
}
|
|
2471
|
+
writeFileSync4(path3, stringifyYaml2({ tasks }), "utf-8");
|
|
1589
2472
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
`;
|
|
1596
|
-
if (t.depth === 2) return `
|
|
1597
|
-
${chalk.bold.cyan(` \u25A0 ${text}`)}
|
|
1598
|
-
|
|
1599
|
-
`;
|
|
1600
|
-
return `
|
|
1601
|
-
${chalk.bold(` \u25A0 ${text}`)}
|
|
1602
|
-
|
|
1603
|
-
`;
|
|
1604
|
-
}
|
|
1605
|
-
function renderInline(tokens) {
|
|
1606
|
-
if (!tokens) return "";
|
|
1607
|
-
return tokens.map((t) => {
|
|
1608
|
-
if (typeof t === "string") return t;
|
|
1609
|
-
if (t.type === "strong") return chalk.bold(renderInline(t.tokens));
|
|
1610
|
-
if (t.type === "em") return chalk.italic(renderInline(t.tokens));
|
|
1611
|
-
if (t.type === "del") return chalk.dim.strikethrough(renderInline(t.tokens));
|
|
1612
|
-
if (t.type === "codespan") return chalk.yellow(t.text);
|
|
1613
|
-
if (t.type === "link") return `${chalk.blue.underline(renderInline(t.tokens))} ${chalk.dim(`(${t.href})`)}`;
|
|
1614
|
-
if (t.type === "image") return chalk.blue(`\u{1F5BC} ${t.title || t.href}`);
|
|
1615
|
-
if (t.type === "text") {
|
|
1616
|
-
return t.tokens ? renderInline(t.tokens) : t.text || "";
|
|
1617
|
-
}
|
|
1618
|
-
if (t.type === "html") return t.text || "";
|
|
1619
|
-
return t.text || "";
|
|
1620
|
-
}).join("");
|
|
1621
|
-
}
|
|
1622
|
-
function renderCodeBlock(t) {
|
|
1623
|
-
const lines = t.text.split("\n").map((l) => `${chalk.dim(" ")}${chalk.yellow(l)}`).join("\n");
|
|
1624
|
-
const langStr = t.lang ? chalk.dim(` [${t.lang}]`) : "";
|
|
1625
|
-
return `
|
|
1626
|
-
${langStr}
|
|
1627
|
-
${lines}
|
|
1628
|
-
|
|
1629
|
-
`;
|
|
1630
|
-
}
|
|
1631
|
-
function renderList(t) {
|
|
1632
|
-
const lines = [];
|
|
1633
|
-
const items = t.items || [];
|
|
1634
|
-
items.forEach((item, i) => {
|
|
1635
|
-
const bullet = t.ordered ? `${i + 1}.` : "\u2022";
|
|
1636
|
-
const firstLine = renderInline(item.tokens?.[0]?.tokens || [{ text: item.text }]);
|
|
1637
|
-
lines.push(` ${chalk.dim(bullet)} ${firstLine}`);
|
|
1638
|
-
const restTokens = (item.tokens || []).slice(1);
|
|
1639
|
-
for (const sub of restTokens) {
|
|
1640
|
-
if (sub.type === "list") {
|
|
1641
|
-
const subLines = renderList(sub).split("\n").filter(Boolean).map((l) => ` ${l}`).join("\n");
|
|
1642
|
-
lines.push(subLines);
|
|
1643
|
-
} else if (sub.type === "text") {
|
|
1644
|
-
lines.push(` ${chalk.dim("\u2022")} ${renderInline(sub.tokens)}`);
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
});
|
|
1648
|
-
return lines.join("\n") + "\n\n";
|
|
1649
|
-
}
|
|
1650
|
-
function renderBlockquote(t) {
|
|
1651
|
-
const content = renderTokens(t.tokens || []);
|
|
1652
|
-
const lines = content.split("\n").filter((l) => l.trim()).map((l) => `${chalk.dim("\u2502 ")}${chalk.gray(l)}`).join("\n");
|
|
1653
|
-
return `
|
|
1654
|
-
${lines}
|
|
1655
|
-
|
|
1656
|
-
`;
|
|
1657
|
-
}
|
|
1658
|
-
function renderTable(t) {
|
|
1659
|
-
const headers = (t.header || []).map((h) => chalk.bold(renderInline(h.tokens)));
|
|
1660
|
-
const colWidths = (t.header || []).map((h, i) => {
|
|
1661
|
-
const hLen = (h.text || "").length;
|
|
1662
|
-
const rowLens = (t.rows || []).map((row) => {
|
|
1663
|
-
const cell = row[i];
|
|
1664
|
-
return cell?.text?.length ?? 0;
|
|
1665
|
-
});
|
|
1666
|
-
return Math.max(hLen, ...rowLens) + 2;
|
|
1667
|
-
});
|
|
1668
|
-
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(chalk.dim(" \u2502 "));
|
|
1669
|
-
const separator = colWidths.map((w) => "\u2500".repeat(w)).join(chalk.dim("\u2500\u253C\u2500"));
|
|
1670
|
-
const dataLines = (t.rows || []).map(
|
|
1671
|
-
(row) => row.map((cell, i) => {
|
|
1672
|
-
const text = renderInline(cell.tokens) || cell.text || "";
|
|
1673
|
-
return text.padEnd(colWidths[i]);
|
|
1674
|
-
}).join(chalk.dim(" \u2502 "))
|
|
1675
|
-
);
|
|
1676
|
-
return `
|
|
1677
|
-
${headerLine}
|
|
1678
|
-
${chalk.dim(separator)}
|
|
1679
|
-
${dataLines.join("\n")}
|
|
1680
|
-
|
|
1681
|
-
`;
|
|
1682
|
-
}
|
|
1683
|
-
function escapeHtml(text) {
|
|
1684
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1685
|
-
}
|
|
1686
|
-
function mdToTelegram(text) {
|
|
1687
|
-
let out = text;
|
|
1688
|
-
const codeBlocks = [];
|
|
1689
|
-
out = out.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
|
|
1690
|
-
const placeholder = `__CODEBLOCK_${codeBlocks.length}__`;
|
|
1691
|
-
codeBlocks.push(`<pre><code class="${lang}">${escapeHtml(code)}</code></pre>`);
|
|
1692
|
-
return placeholder;
|
|
1693
|
-
});
|
|
1694
|
-
const inlineCodes = [];
|
|
1695
|
-
out = out.replace(/`([^`]+)`/g, (_match, code) => {
|
|
1696
|
-
const placeholder = `__INLINECODE_${inlineCodes.length}__`;
|
|
1697
|
-
inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
|
|
1698
|
-
return placeholder;
|
|
1699
|
-
});
|
|
1700
|
-
const links = [];
|
|
1701
|
-
out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, href) => {
|
|
1702
|
-
const placeholder = `__LINK_${links.length}__`;
|
|
1703
|
-
links.push(`<a href="${escapeHtml(href)}">${escapeHtml(label)}</a>`);
|
|
1704
|
-
return placeholder;
|
|
1705
|
-
});
|
|
1706
|
-
out = escapeHtml(out);
|
|
1707
|
-
out = out.replace(/^### (.+)$/gm, "<b><i>$1</i></b>");
|
|
1708
|
-
out = out.replace(/^## (.+)$/gm, "<b>$1</b>");
|
|
1709
|
-
out = out.replace(/^# (.+)$/gm, "<b>$1</b>");
|
|
1710
|
-
out = out.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
|
|
1711
|
-
out = out.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<i>$1</i>");
|
|
1712
|
-
out = out.replace(/~~([^~]+)~~/g, "<s>$1</s>");
|
|
1713
|
-
for (let i = 0; i < inlineCodes.length; i++) {
|
|
1714
|
-
out = out.replace(`__INLINECODE_${i}__`, inlineCodes[i]);
|
|
1715
|
-
}
|
|
1716
|
-
for (let i = 0; i < codeBlocks.length; i++) {
|
|
1717
|
-
out = out.replace(`__CODEBLOCK_${i}__`, codeBlocks[i]);
|
|
2473
|
+
var Scheduler = class {
|
|
2474
|
+
constructor(config, onScheduledTask) {
|
|
2475
|
+
this.onScheduledTask = onScheduledTask;
|
|
2476
|
+
this.heartbeatIntervalMinutes = config.heartbeat.intervalMinutes;
|
|
1718
2477
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
2478
|
+
onScheduledTask;
|
|
2479
|
+
tasks = /* @__PURE__ */ new Map();
|
|
2480
|
+
delayedTasks = /* @__PURE__ */ new Map();
|
|
2481
|
+
taskManifests = /* @__PURE__ */ new Map();
|
|
2482
|
+
heartbeatIntervalMinutes;
|
|
2483
|
+
heartbeatHandler;
|
|
2484
|
+
heartbeatTimer = null;
|
|
2485
|
+
setOnScheduledTask(handler) {
|
|
2486
|
+
this.onScheduledTask = handler;
|
|
1721
2487
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
2488
|
+
onHeartbeat(handler) {
|
|
2489
|
+
this.heartbeatHandler = handler;
|
|
1724
2490
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
2491
|
+
startHeartbeat() {
|
|
2492
|
+
if (this.heartbeatTimer) return;
|
|
2493
|
+
const ms = this.heartbeatIntervalMinutes * 60 * 1e3;
|
|
2494
|
+
logger.info({ intervalMin: this.heartbeatIntervalMinutes }, "Heartbeat started");
|
|
2495
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
2496
|
+
try {
|
|
2497
|
+
await this.heartbeatHandler?.();
|
|
2498
|
+
} catch (err) {
|
|
2499
|
+
logger.error({ err }, "Heartbeat error");
|
|
2500
|
+
}
|
|
2501
|
+
}, ms);
|
|
1736
2502
|
}
|
|
1737
|
-
|
|
1738
|
-
this.
|
|
2503
|
+
stopHeartbeat() {
|
|
2504
|
+
if (this.heartbeatTimer) {
|
|
2505
|
+
clearInterval(this.heartbeatTimer);
|
|
2506
|
+
this.heartbeatTimer = null;
|
|
2507
|
+
logger.info("Heartbeat stopped");
|
|
2508
|
+
}
|
|
1739
2509
|
}
|
|
1740
|
-
|
|
1741
|
-
this.
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
this.showPrompt();
|
|
1750
|
-
return;
|
|
2510
|
+
addTask(task) {
|
|
2511
|
+
if (this.tasks.has(task.id)) {
|
|
2512
|
+
this.removeTask(task.id);
|
|
2513
|
+
}
|
|
2514
|
+
const scheduled = cron.schedule(task.cron, async () => {
|
|
2515
|
+
try {
|
|
2516
|
+
await task.handler();
|
|
2517
|
+
} catch (err) {
|
|
2518
|
+
logger.error({ task: task.id, err }, "Scheduled task error");
|
|
1751
2519
|
}
|
|
1752
|
-
const msg = {
|
|
1753
|
-
id: Date.now().toString(36),
|
|
1754
|
-
channelId: "cli",
|
|
1755
|
-
channelType: "cli",
|
|
1756
|
-
senderId: "owner",
|
|
1757
|
-
content: trimmed,
|
|
1758
|
-
timestamp: Date.now()
|
|
1759
|
-
};
|
|
1760
|
-
this.emit(msg);
|
|
1761
2520
|
});
|
|
1762
|
-
this.
|
|
1763
|
-
|
|
1764
|
-
logger.info("CLI channel started");
|
|
2521
|
+
this.tasks.set(task.id, scheduled);
|
|
2522
|
+
logger.info({ id: task.id, cron: task.cron, desc: task.description }, "Task scheduled");
|
|
1765
2523
|
}
|
|
1766
|
-
|
|
1767
|
-
this.
|
|
1768
|
-
this.
|
|
1769
|
-
|
|
2524
|
+
addPersistedTask(manifest) {
|
|
2525
|
+
this.taskManifests.set(manifest.id, manifest);
|
|
2526
|
+
this.addTask({
|
|
2527
|
+
id: manifest.id,
|
|
2528
|
+
cron: manifest.cron,
|
|
2529
|
+
description: manifest.description,
|
|
2530
|
+
handler: async () => {
|
|
2531
|
+
logger.info({ task: manifest.id }, "Scheduled task firing");
|
|
2532
|
+
if (this.onScheduledTask) {
|
|
2533
|
+
await this.onScheduledTask(manifest);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
});
|
|
1770
2537
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2538
|
+
addDelayedTask(manifest) {
|
|
2539
|
+
this.taskManifests.set(manifest.id, manifest);
|
|
2540
|
+
const delayMs = (manifest.delaySeconds || 60) * 1e3;
|
|
2541
|
+
const timer = setTimeout(async () => {
|
|
2542
|
+
try {
|
|
2543
|
+
logger.info({ task: manifest.id }, "Delayed task firing");
|
|
2544
|
+
if (this.onScheduledTask) {
|
|
2545
|
+
await this.onScheduledTask(manifest);
|
|
2546
|
+
}
|
|
2547
|
+
} catch (err) {
|
|
2548
|
+
logger.error({ task: manifest.id, err }, "Delayed task error");
|
|
2549
|
+
} finally {
|
|
2550
|
+
this.delayedTasks.delete(manifest.id);
|
|
2551
|
+
this.taskManifests.delete(manifest.id);
|
|
2552
|
+
this.persistSchedules();
|
|
2553
|
+
}
|
|
2554
|
+
}, delayMs);
|
|
2555
|
+
this.delayedTasks.set(manifest.id, timer);
|
|
2556
|
+
logger.info({ id: manifest.id, delaySeconds: manifest.delaySeconds }, "Delayed task scheduled");
|
|
1780
2557
|
}
|
|
1781
|
-
|
|
1782
|
-
const
|
|
1783
|
-
if (
|
|
1784
|
-
|
|
1785
|
-
|
|
2558
|
+
removeTask(id) {
|
|
2559
|
+
const task = this.tasks.get(id);
|
|
2560
|
+
if (task) {
|
|
2561
|
+
task.stop();
|
|
2562
|
+
this.tasks.delete(id);
|
|
1786
2563
|
}
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
console.log(chalk2.dim(` path: ${resolved}`));
|
|
1792
|
-
console.log(chalk2.dim(` size: ${sizeStr}`));
|
|
1793
|
-
console.log("");
|
|
1794
|
-
this.showPrompt();
|
|
1795
|
-
}
|
|
1796
|
-
async stream(content, _targetId) {
|
|
1797
|
-
console.log("");
|
|
1798
|
-
process.stdout.write(chalk2.cyan(` ${this.agentName}: `));
|
|
1799
|
-
let full = "";
|
|
1800
|
-
for await (const chunk of content) {
|
|
1801
|
-
process.stdout.write(chunk);
|
|
1802
|
-
full += chunk;
|
|
2564
|
+
const timer = this.delayedTasks.get(id);
|
|
2565
|
+
if (timer) {
|
|
2566
|
+
clearTimeout(timer);
|
|
2567
|
+
this.delayedTasks.delete(id);
|
|
1803
2568
|
}
|
|
1804
|
-
|
|
1805
|
-
this.showPrompt();
|
|
1806
|
-
return full;
|
|
2569
|
+
this.taskManifests.delete(id);
|
|
1807
2570
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
2571
|
+
getManifests() {
|
|
2572
|
+
return [...this.taskManifests.values()];
|
|
1810
2573
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
2574
|
+
restorePersistedTasks() {
|
|
2575
|
+
const persisted = loadSchedules();
|
|
2576
|
+
for (const manifest of persisted) {
|
|
2577
|
+
if (manifest.delaySeconds) {
|
|
2578
|
+
const executeAt = manifest.executeAt ? new Date(manifest.executeAt) : null;
|
|
2579
|
+
const now = Date.now();
|
|
2580
|
+
if (executeAt && executeAt.getTime() > now) {
|
|
2581
|
+
const remainingMs = executeAt.getTime() - now;
|
|
2582
|
+
manifest.delaySeconds = Math.ceil(remainingMs / 1e3);
|
|
2583
|
+
this.addDelayedTask(manifest);
|
|
2584
|
+
} else {
|
|
2585
|
+
logger.info({ id: manifest.id }, "Delayed task already expired, skipping");
|
|
2586
|
+
}
|
|
2587
|
+
} else if (manifest.cron && cron.validate(manifest.cron)) {
|
|
2588
|
+
this.addPersistedTask(manifest);
|
|
2589
|
+
} else {
|
|
2590
|
+
logger.warn({ id: manifest.id, cron: manifest.cron }, "Skipping invalid task");
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
if (persisted.length > 0) {
|
|
2594
|
+
logger.info({ count: persisted.length }, "Restored persisted scheduled tasks");
|
|
1815
2595
|
}
|
|
1816
2596
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
this.rl?.question(question, (answer) => resolve13(answer.trim()));
|
|
1820
|
-
});
|
|
2597
|
+
persistSchedules() {
|
|
2598
|
+
saveSchedules(this.getManifests());
|
|
1821
2599
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
}
|
|
2600
|
+
stopAll() {
|
|
2601
|
+
this.stopHeartbeat();
|
|
2602
|
+
for (const [, task] of this.tasks) {
|
|
2603
|
+
task.stop();
|
|
2604
|
+
}
|
|
2605
|
+
for (const [, timer] of this.delayedTasks) {
|
|
2606
|
+
clearTimeout(timer);
|
|
2607
|
+
}
|
|
2608
|
+
this.tasks.clear();
|
|
2609
|
+
this.delayedTasks.clear();
|
|
2610
|
+
this.taskManifests.clear();
|
|
1830
2611
|
}
|
|
1831
2612
|
};
|
|
1832
2613
|
|
|
@@ -1836,16 +2617,16 @@ import path2 from "path";
|
|
|
1836
2617
|
import { Bot, InputFile, InlineKeyboard } from "grammy";
|
|
1837
2618
|
import { autoRetry } from "@grammyjs/auto-retry";
|
|
1838
2619
|
var MAX_MESSAGE_LENGTH = 4096;
|
|
2620
|
+
var ACCESS_ACTION_PREFIX = "tg_access";
|
|
1839
2621
|
var TelegramChannel = class extends BaseChannel {
|
|
1840
2622
|
constructor(config) {
|
|
1841
2623
|
super();
|
|
1842
2624
|
this.config = config;
|
|
1843
|
-
this.ownerChatId = config.channels.telegram.pairedChatId ?? null;
|
|
1844
2625
|
}
|
|
1845
2626
|
config;
|
|
1846
2627
|
type = "telegram";
|
|
1847
2628
|
bot = null;
|
|
1848
|
-
|
|
2629
|
+
lastActiveChatId = null;
|
|
1849
2630
|
typingInterval = null;
|
|
1850
2631
|
chatCommandContext;
|
|
1851
2632
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
@@ -1863,30 +2644,41 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1863
2644
|
bot.on("message:text", async (ctx) => {
|
|
1864
2645
|
const chatId = ctx.chat.id;
|
|
1865
2646
|
const userId = ctx.from?.id;
|
|
2647
|
+
const username = ctx.from?.username;
|
|
2648
|
+
const firstName = ctx.from?.first_name;
|
|
1866
2649
|
const text = ctx.message.text?.trim() || "";
|
|
2650
|
+
const command = this.getCommandName(text);
|
|
1867
2651
|
if (!userId) return;
|
|
1868
2652
|
if (ctx.chat.type !== "private") {
|
|
1869
2653
|
await this.sendDirectMessage(chatId, "This bot is only available in private one-to-one chats.");
|
|
1870
2654
|
return;
|
|
1871
2655
|
}
|
|
1872
|
-
if (
|
|
1873
|
-
await this.
|
|
2656
|
+
if (command === "/start" || command === "/pair") {
|
|
2657
|
+
await this.handleAccessRequest(userId, chatId, username, firstName);
|
|
1874
2658
|
return;
|
|
1875
2659
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
2660
|
+
const approvedUser = findTelegramApprovedUser(this.config, userId);
|
|
2661
|
+
if (!approvedUser) {
|
|
2662
|
+
const pending = findTelegramPendingRequest(this.config, userId);
|
|
2663
|
+
if (pending) {
|
|
2664
|
+
await this.sendDirectMessage(chatId, this.getPendingStatusMessage());
|
|
2665
|
+
} else {
|
|
2666
|
+
await this.sendDirectMessage(chatId, "This bot is not available to you. Send /start to request access.");
|
|
2667
|
+
}
|
|
1878
2668
|
return;
|
|
1879
2669
|
}
|
|
1880
|
-
this.
|
|
2670
|
+
this.lastActiveChatId = chatId;
|
|
1881
2671
|
logger.info({ chatId, text: ctx.message.text?.slice(0, 50) }, "Telegram message received");
|
|
1882
|
-
const command = text.toLowerCase();
|
|
1883
|
-
if (command === "/start" || command === "/pair") {
|
|
1884
|
-
await this.sendDirectMessage(chatId, this.getPairingStatusMessage());
|
|
1885
|
-
return;
|
|
1886
|
-
}
|
|
1887
2672
|
if (command === "/unpair") {
|
|
1888
|
-
this.
|
|
1889
|
-
|
|
2673
|
+
if (!this.isAdminUser(userId)) {
|
|
2674
|
+
await this.sendDirectMessage(chatId, "Only Telegram admins can reset Telegram access.");
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
this.resetAccess();
|
|
2678
|
+
await this.sendDirectMessage(
|
|
2679
|
+
chatId,
|
|
2680
|
+
"Telegram access reset. New users can send /start to request access. The first request must be approved from the Mercury CLI."
|
|
2681
|
+
);
|
|
1890
2682
|
return;
|
|
1891
2683
|
}
|
|
1892
2684
|
const msg = {
|
|
@@ -1903,6 +2695,10 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1903
2695
|
});
|
|
1904
2696
|
bot.on("callback_query:data", async (ctx) => {
|
|
1905
2697
|
const data = ctx.callbackQuery.data;
|
|
2698
|
+
if (data.startsWith(`${ACCESS_ACTION_PREFIX}:`)) {
|
|
2699
|
+
await this.handleAccessCallback(ctx, data);
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
1906
2702
|
const resolver = this.pendingApprovals.get(data);
|
|
1907
2703
|
if (!resolver) {
|
|
1908
2704
|
await ctx.answerCallbackQuery({ text: "Expired" });
|
|
@@ -1917,19 +2713,33 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1917
2713
|
logger.error({ err: err.message }, "Telegram bot error");
|
|
1918
2714
|
});
|
|
1919
2715
|
this.bot = bot;
|
|
1920
|
-
await
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2716
|
+
await new Promise((resolve13, reject) => {
|
|
2717
|
+
let settled = false;
|
|
2718
|
+
void bot.start({
|
|
2719
|
+
onStart: async (info) => {
|
|
2720
|
+
logger.info({ bot: info.username }, "Telegram bot started \u2014 long polling active");
|
|
2721
|
+
this.ready = true;
|
|
2722
|
+
await this.registerCommands();
|
|
2723
|
+
if (!settled) {
|
|
2724
|
+
settled = true;
|
|
2725
|
+
resolve13();
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}).catch((err) => {
|
|
2729
|
+
if (!settled) {
|
|
2730
|
+
settled = true;
|
|
2731
|
+
reject(err);
|
|
2732
|
+
return;
|
|
2733
|
+
}
|
|
2734
|
+
logger.error({ err: err.message }, "Telegram bot start loop failed after startup");
|
|
2735
|
+
});
|
|
1926
2736
|
});
|
|
1927
2737
|
}
|
|
1928
2738
|
async registerCommands() {
|
|
1929
2739
|
if (!this.bot) return;
|
|
1930
2740
|
const commands = [
|
|
1931
|
-
{ command: "start", description: "
|
|
1932
|
-
{ command: "pair", description: "
|
|
2741
|
+
{ command: "start", description: "Request Telegram access to this Mercury instance" },
|
|
2742
|
+
{ command: "pair", description: "Request Telegram access to this Mercury instance" },
|
|
1933
2743
|
{ command: "help", description: "Show capabilities and commands manual" },
|
|
1934
2744
|
{ command: "status", description: "Show agent config, budget, and uptime" },
|
|
1935
2745
|
{ command: "tools", description: "List all loaded tools" },
|
|
@@ -1939,7 +2749,7 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1939
2749
|
{ command: "budget_reset", description: "Reset token usage to zero" },
|
|
1940
2750
|
{ command: "budget_set", description: "Set new daily token budget" },
|
|
1941
2751
|
{ command: "stream", description: "Toggle text streaming on/off" },
|
|
1942
|
-
{ command: "unpair", description: "
|
|
2752
|
+
{ command: "unpair", description: "Reset all Telegram access for this Mercury instance" }
|
|
1943
2753
|
];
|
|
1944
2754
|
try {
|
|
1945
2755
|
await this.bot.api.setMyCommands(commands);
|
|
@@ -1954,9 +2764,9 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1954
2764
|
this.stopTypingLoop();
|
|
1955
2765
|
}
|
|
1956
2766
|
async send(content, targetId, elapsedMs) {
|
|
1957
|
-
const
|
|
1958
|
-
if (
|
|
1959
|
-
logger.warn({ targetId,
|
|
2767
|
+
const chatIds = this.resolveTargetChatIds(targetId);
|
|
2768
|
+
if (chatIds.length === 0 || !this.bot) {
|
|
2769
|
+
logger.warn({ targetId, chatIds }, "Telegram send: no valid chat IDs");
|
|
1960
2770
|
return;
|
|
1961
2771
|
}
|
|
1962
2772
|
const timeSuffix = elapsedMs != null ? `
|
|
@@ -1964,69 +2774,79 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1964
2774
|
const fullContent = content + timeSuffix;
|
|
1965
2775
|
const html = mdToTelegram(fullContent);
|
|
1966
2776
|
const chunks = this.splitMessage(html, MAX_MESSAGE_LENGTH);
|
|
1967
|
-
for (const
|
|
1968
|
-
|
|
1969
|
-
await this.bot.api.sendMessage(chatId, chunk, { parse_mode: "HTML" });
|
|
1970
|
-
} catch (err) {
|
|
1971
|
-
logger.warn({ err: err.message }, "HTML parse failed, sending as plain text");
|
|
2777
|
+
for (const chatId of chatIds) {
|
|
2778
|
+
for (const chunk of chunks) {
|
|
1972
2779
|
try {
|
|
1973
|
-
await this.bot.api.sendMessage(chatId,
|
|
1974
|
-
} catch (
|
|
1975
|
-
logger.
|
|
2780
|
+
await this.bot.api.sendMessage(chatId, chunk, { parse_mode: "HTML" });
|
|
2781
|
+
} catch (err) {
|
|
2782
|
+
logger.warn({ err: err.message, chatId }, "HTML parse failed, sending as plain text");
|
|
2783
|
+
try {
|
|
2784
|
+
await this.bot.api.sendMessage(chatId, this.stripHtml(chunk));
|
|
2785
|
+
} catch (err2) {
|
|
2786
|
+
logger.error({ err: err2.message, chatId }, "Telegram send failed");
|
|
2787
|
+
}
|
|
1976
2788
|
}
|
|
1977
2789
|
}
|
|
1978
2790
|
}
|
|
1979
2791
|
}
|
|
1980
2792
|
async sendFile(filePath, targetId) {
|
|
1981
|
-
const
|
|
1982
|
-
if (
|
|
1983
|
-
logger.warn({ targetId,
|
|
2793
|
+
const chatIds = this.resolveTargetChatIds(targetId);
|
|
2794
|
+
if (chatIds.length === 0 || !this.bot) {
|
|
2795
|
+
logger.warn({ targetId, chatIds }, "Telegram sendFile: no valid chat IDs");
|
|
1984
2796
|
return;
|
|
1985
2797
|
}
|
|
1986
2798
|
const resolved = path2.resolve(filePath);
|
|
1987
2799
|
if (!fs2.existsSync(resolved)) {
|
|
1988
|
-
|
|
2800
|
+
for (const chatId of chatIds) {
|
|
2801
|
+
await this.bot.api.sendMessage(chatId, `File not found: ${filePath}`).catch(() => {
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
1989
2804
|
return;
|
|
1990
2805
|
}
|
|
1991
|
-
const inputFile = new InputFile(resolved);
|
|
1992
2806
|
const filename = path2.basename(resolved);
|
|
1993
2807
|
const ext = path2.extname(resolved).toLowerCase();
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2808
|
+
for (const chatId of chatIds) {
|
|
2809
|
+
const inputFile = new InputFile(resolved);
|
|
2810
|
+
try {
|
|
2811
|
+
if (this.isImageFile(ext)) {
|
|
2812
|
+
await this.bot.api.sendPhoto(chatId, inputFile, { caption: filename });
|
|
2813
|
+
} else if (this.isAudioFile(ext)) {
|
|
2814
|
+
await this.bot.api.sendAudio(chatId, inputFile, { title: filename });
|
|
2815
|
+
} else if (this.isVideoFile(ext)) {
|
|
2816
|
+
await this.bot.api.sendVideo(chatId, inputFile, { caption: filename });
|
|
2817
|
+
} else {
|
|
2818
|
+
await this.bot.api.sendDocument(chatId, inputFile, { caption: filename });
|
|
2819
|
+
}
|
|
2820
|
+
logger.info({ file: resolved, chatId }, "File sent via Telegram");
|
|
2821
|
+
} catch (err) {
|
|
2822
|
+
logger.error({ err: err.message, file: resolved, chatId }, "Telegram sendFile failed");
|
|
2823
|
+
await this.bot.api.sendMessage(chatId, `Failed to send file: ${err.message}`).catch(() => {
|
|
2824
|
+
});
|
|
2003
2825
|
}
|
|
2004
|
-
logger.info({ file: resolved, chatId }, "File sent via Telegram");
|
|
2005
|
-
} catch (err) {
|
|
2006
|
-
logger.error({ err: err.message, file: resolved }, "Telegram sendFile failed");
|
|
2007
|
-
await this.bot.api.sendMessage(chatId, `Failed to send file: ${err.message}`).catch(() => {
|
|
2008
|
-
});
|
|
2009
2826
|
}
|
|
2010
2827
|
}
|
|
2011
2828
|
async stream(content, targetId) {
|
|
2012
|
-
const
|
|
2013
|
-
if (
|
|
2829
|
+
const chatIds = this.resolveTargetChatIds(targetId);
|
|
2830
|
+
if (chatIds.length === 0 || !this.bot) return "";
|
|
2014
2831
|
let full = "";
|
|
2015
2832
|
for await (const chunk of content) {
|
|
2016
2833
|
full += chunk;
|
|
2017
2834
|
}
|
|
2018
2835
|
const html = mdToTelegram(full);
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2836
|
+
for (const chatId of chatIds) {
|
|
2837
|
+
try {
|
|
2838
|
+
await this.bot.api.sendMessage(chatId, html, { parse_mode: "HTML" });
|
|
2839
|
+
} catch (err) {
|
|
2840
|
+
await this.bot.api.sendMessage(chatId, this.stripHtml(html)).catch(() => {
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2023
2843
|
}
|
|
2024
2844
|
return full;
|
|
2025
2845
|
}
|
|
2026
2846
|
async typing(targetId) {
|
|
2027
|
-
const
|
|
2028
|
-
if (
|
|
2029
|
-
await this.bot.api.sendChatAction(
|
|
2847
|
+
const chatIds = this.resolveTargetChatIds(targetId);
|
|
2848
|
+
if (chatIds.length === 0 || !this.bot) return;
|
|
2849
|
+
await this.bot.api.sendChatAction(chatIds[0], "typing");
|
|
2030
2850
|
}
|
|
2031
2851
|
startTypingLoop(chatId) {
|
|
2032
2852
|
this.stopTypingLoop();
|
|
@@ -2100,7 +2920,8 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2100
2920
|
}
|
|
2101
2921
|
}
|
|
2102
2922
|
async askPermission(prompt, targetId) {
|
|
2103
|
-
const
|
|
2923
|
+
const chatIds = this.resolveTargetChatIds(targetId);
|
|
2924
|
+
const chatId = chatIds[0];
|
|
2104
2925
|
if (!chatId || !this.bot) return "no";
|
|
2105
2926
|
const id = `perm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2106
2927
|
const keyboard = new InlineKeyboard().text("Allow", `${id}:yes`).text("Always", `${id}:always`).text("Deny", `${id}:no`);
|
|
@@ -2115,17 +2936,189 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2115
2936
|
reply_markup: keyboard
|
|
2116
2937
|
});
|
|
2117
2938
|
}
|
|
2118
|
-
return new Promise((resolve13) => {
|
|
2119
|
-
this.pendingApprovals.set(`${id}:yes`, () => resolve13("yes"));
|
|
2120
|
-
this.pendingApprovals.set(`${id}:always`, () => resolve13("always"));
|
|
2121
|
-
this.pendingApprovals.set(`${id}:no`, () => resolve13("no"));
|
|
2122
|
-
setTimeout(() => {
|
|
2123
|
-
this.pendingApprovals.delete(`${id}:yes`);
|
|
2124
|
-
this.pendingApprovals.delete(`${id}:always`);
|
|
2125
|
-
this.pendingApprovals.delete(`${id}:no`);
|
|
2126
|
-
resolve13("no");
|
|
2127
|
-
}, 12e4);
|
|
2128
|
-
});
|
|
2939
|
+
return new Promise((resolve13) => {
|
|
2940
|
+
this.pendingApprovals.set(`${id}:yes`, () => resolve13("yes"));
|
|
2941
|
+
this.pendingApprovals.set(`${id}:always`, () => resolve13("always"));
|
|
2942
|
+
this.pendingApprovals.set(`${id}:no`, () => resolve13("no"));
|
|
2943
|
+
setTimeout(() => {
|
|
2944
|
+
this.pendingApprovals.delete(`${id}:yes`);
|
|
2945
|
+
this.pendingApprovals.delete(`${id}:always`);
|
|
2946
|
+
this.pendingApprovals.delete(`${id}:no`);
|
|
2947
|
+
resolve13("no");
|
|
2948
|
+
}, 12e4);
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
async handleAccessRequest(userId, chatId, username, firstName) {
|
|
2952
|
+
const approvedUser = findTelegramApprovedUser(this.config, userId);
|
|
2953
|
+
if (approvedUser) {
|
|
2954
|
+
await this.sendDirectMessage(chatId, this.getApprovedStatusMessage(approvedUser));
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
const existingRequest = findTelegramPendingRequest(this.config, userId);
|
|
2958
|
+
if (existingRequest) {
|
|
2959
|
+
await this.sendDirectMessage(chatId, this.getPendingStatusMessage(existingRequest));
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
if (!hasTelegramAdmins(this.config) && this.config.channels.telegram.pending.length > 0) {
|
|
2963
|
+
await this.sendDirectMessage(
|
|
2964
|
+
chatId,
|
|
2965
|
+
"Initial Telegram pairing is already in progress for another user. Ask the Mercury operator to finish setup or reset Telegram access first."
|
|
2966
|
+
);
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
const request = addTelegramPendingRequest(this.config, {
|
|
2970
|
+
userId,
|
|
2971
|
+
chatId,
|
|
2972
|
+
username,
|
|
2973
|
+
firstName,
|
|
2974
|
+
pairingCode: hasTelegramAdmins(this.config) ? void 0 : this.generatePairingCode()
|
|
2975
|
+
});
|
|
2976
|
+
saveConfig(this.config);
|
|
2977
|
+
logger.info({ chatId, userId, username }, "Telegram access request recorded");
|
|
2978
|
+
await this.sendDirectMessage(chatId, this.getPendingStatusMessage(request));
|
|
2979
|
+
if (!hasTelegramAdmins(this.config)) {
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
await this.notifyAdminsOfPendingRequest(request);
|
|
2983
|
+
}
|
|
2984
|
+
async notifyAdminsOfPendingRequest(request) {
|
|
2985
|
+
if (!this.bot) return;
|
|
2986
|
+
const keyboard = new InlineKeyboard().text("Approve", `${ACCESS_ACTION_PREFIX}:approve:${request.userId}`).text("Reject", `${ACCESS_ACTION_PREFIX}:reject:${request.userId}`);
|
|
2987
|
+
const username = request.username ? ` (@${request.username})` : "";
|
|
2988
|
+
const firstName = request.firstName ? ` (${request.firstName})` : "";
|
|
2989
|
+
const message = [
|
|
2990
|
+
"Telegram access request pending approval.",
|
|
2991
|
+
"",
|
|
2992
|
+
`User ID: ${request.userId}${username}${firstName}`,
|
|
2993
|
+
`Requested: ${new Date(request.requestedAt).toLocaleString()}`,
|
|
2994
|
+
"",
|
|
2995
|
+
"Use the buttons below to approve or reject this user."
|
|
2996
|
+
].join("\n");
|
|
2997
|
+
for (const admin of getTelegramAdmins(this.config)) {
|
|
2998
|
+
try {
|
|
2999
|
+
await this.bot.api.sendMessage(admin.chatId, mdToTelegram(message), {
|
|
3000
|
+
parse_mode: "HTML",
|
|
3001
|
+
reply_markup: keyboard
|
|
3002
|
+
});
|
|
3003
|
+
} catch {
|
|
3004
|
+
await this.bot.api.sendMessage(admin.chatId, message, {
|
|
3005
|
+
reply_markup: keyboard
|
|
3006
|
+
}).catch(() => {
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
async handleAccessCallback(ctx, data) {
|
|
3012
|
+
const actorUserId = ctx.from?.id;
|
|
3013
|
+
const actorChatId = ctx.chat?.id;
|
|
3014
|
+
if (!actorUserId || !actorChatId) {
|
|
3015
|
+
await ctx.answerCallbackQuery({ text: "Unavailable" });
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
if (!this.isAdminUser(actorUserId)) {
|
|
3019
|
+
await ctx.answerCallbackQuery({ text: "Admins only" });
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
const [, action, rawUserId] = data.split(":");
|
|
3023
|
+
const requestUserId = Number(rawUserId);
|
|
3024
|
+
if (!requestUserId) {
|
|
3025
|
+
await ctx.answerCallbackQuery({ text: "Invalid request" });
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
const request = findTelegramPendingRequest(this.config, requestUserId);
|
|
3029
|
+
if (!request) {
|
|
3030
|
+
await ctx.answerCallbackQuery({ text: "Already handled" });
|
|
3031
|
+
await ctx.editMessageReplyMarkup({ reply_markup: void 0 }).catch(() => {
|
|
3032
|
+
});
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
if (action === "approve") {
|
|
3036
|
+
const approved = approveTelegramPendingRequest(this.config, requestUserId, "member");
|
|
3037
|
+
if (!approved) {
|
|
3038
|
+
await ctx.answerCallbackQuery({ text: "Already handled" });
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
saveConfig(this.config);
|
|
3042
|
+
await ctx.answerCallbackQuery({ text: "Approved" });
|
|
3043
|
+
await ctx.editMessageReplyMarkup({ reply_markup: void 0 }).catch(() => {
|
|
3044
|
+
});
|
|
3045
|
+
await this.sendDirectMessage(
|
|
3046
|
+
request.chatId,
|
|
3047
|
+
`Telegram access approved. You can now chat with Mercury.
|
|
3048
|
+
|
|
3049
|
+
Telegram access: ${getTelegramAccessSummary(this.config)}`
|
|
3050
|
+
);
|
|
3051
|
+
await this.sendDirectMessage(actorChatId, `Approved Telegram access for ${this.formatRequestLabel(request)}.`);
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
if (action === "reject") {
|
|
3055
|
+
const rejected = rejectTelegramPendingRequest(this.config, requestUserId);
|
|
3056
|
+
if (!rejected) {
|
|
3057
|
+
await ctx.answerCallbackQuery({ text: "Already handled" });
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
saveConfig(this.config);
|
|
3061
|
+
await ctx.answerCallbackQuery({ text: "Rejected" });
|
|
3062
|
+
await ctx.editMessageReplyMarkup({ reply_markup: void 0 }).catch(() => {
|
|
3063
|
+
});
|
|
3064
|
+
await this.sendDirectMessage(
|
|
3065
|
+
request.chatId,
|
|
3066
|
+
"Your Telegram access request was rejected. This bot is not available to you."
|
|
3067
|
+
);
|
|
3068
|
+
await this.sendDirectMessage(actorChatId, `Rejected Telegram access for ${this.formatRequestLabel(request)}.`);
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
await ctx.answerCallbackQuery({ text: "Unknown action" });
|
|
3072
|
+
}
|
|
3073
|
+
resolveTargetChatIds(targetId) {
|
|
3074
|
+
if (!targetId || targetId === "notification") {
|
|
3075
|
+
return getTelegramApprovedChatIds(this.config);
|
|
3076
|
+
}
|
|
3077
|
+
if (targetId.startsWith("telegram:")) {
|
|
3078
|
+
const raw = Number(targetId.split(":")[1]);
|
|
3079
|
+
return isNaN(raw) ? [] : [raw];
|
|
3080
|
+
}
|
|
3081
|
+
const num = Number(targetId);
|
|
3082
|
+
return isNaN(num) ? [] : [num];
|
|
3083
|
+
}
|
|
3084
|
+
isAdminUser(userId) {
|
|
3085
|
+
return !!findTelegramAdmin(this.config, userId);
|
|
3086
|
+
}
|
|
3087
|
+
getCommandName(text) {
|
|
3088
|
+
return text.trim().split(/\s+/)[0]?.toLowerCase() || "";
|
|
3089
|
+
}
|
|
3090
|
+
getPendingStatusMessage(request) {
|
|
3091
|
+
if (!hasTelegramAdmins(this.config)) {
|
|
3092
|
+
const pairingCode = request?.pairingCode ?? "unknown";
|
|
3093
|
+
return [
|
|
3094
|
+
"Your Telegram pairing request has been recorded.",
|
|
3095
|
+
"",
|
|
3096
|
+
`Pairing code: ${pairingCode}`,
|
|
3097
|
+
"",
|
|
3098
|
+
"Enter this code in the Mercury terminal to finish setup."
|
|
3099
|
+
].join("\n");
|
|
3100
|
+
}
|
|
3101
|
+
return "Your Telegram access request has been recorded and is waiting for approval from a Telegram admin.";
|
|
3102
|
+
}
|
|
3103
|
+
getApprovedStatusMessage(user) {
|
|
3104
|
+
const role = this.isAdminUser(user.userId) ? "admin" : "member";
|
|
3105
|
+
return `You are already approved as a Telegram ${role}.
|
|
3106
|
+
|
|
3107
|
+
Telegram access: ${getTelegramAccessSummary(this.config)}`;
|
|
3108
|
+
}
|
|
3109
|
+
formatRequestLabel(request) {
|
|
3110
|
+
const username = request.username ? ` (@${request.username})` : "";
|
|
3111
|
+
const firstName = request.firstName ? ` ${request.firstName}` : "";
|
|
3112
|
+
return `${request.userId}${username}${firstName}`;
|
|
3113
|
+
}
|
|
3114
|
+
resetAccess() {
|
|
3115
|
+
clearTelegramAccess(this.config);
|
|
3116
|
+
saveConfig(this.config);
|
|
3117
|
+
this.lastActiveChatId = null;
|
|
3118
|
+
logger.info("Telegram access reset");
|
|
3119
|
+
}
|
|
3120
|
+
generatePairingCode() {
|
|
3121
|
+
return Math.floor(1e5 + Math.random() * 9e5).toString();
|
|
2129
3122
|
}
|
|
2130
3123
|
escapeHtml(text) {
|
|
2131
3124
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -2159,50 +3152,6 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2159
3152
|
isVideoFile(ext) {
|
|
2160
3153
|
return [".mp4", ".mov", ".avi", ".mkv", ".webm"].includes(ext);
|
|
2161
3154
|
}
|
|
2162
|
-
parseChatId(targetId) {
|
|
2163
|
-
if (!targetId) return this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null;
|
|
2164
|
-
if (targetId.startsWith("telegram:")) {
|
|
2165
|
-
const raw = Number(targetId.split(":")[1]);
|
|
2166
|
-
return isNaN(raw) ? this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null : raw;
|
|
2167
|
-
}
|
|
2168
|
-
if (targetId === "notification") return this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null;
|
|
2169
|
-
const num = Number(targetId);
|
|
2170
|
-
return isNaN(num) ? this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null : num;
|
|
2171
|
-
}
|
|
2172
|
-
isPaired() {
|
|
2173
|
-
return typeof this.config.channels.telegram.pairedUserId === "number";
|
|
2174
|
-
}
|
|
2175
|
-
isAuthorizedUser(userId) {
|
|
2176
|
-
return this.config.channels.telegram.pairedUserId === userId;
|
|
2177
|
-
}
|
|
2178
|
-
async handleUnpairedMessage(userId, chatId, text, username) {
|
|
2179
|
-
const command = text.toLowerCase();
|
|
2180
|
-
if (command === "/start" || command === "/pair") {
|
|
2181
|
-
setTelegramPairing(this.config, userId, chatId, username);
|
|
2182
|
-
saveConfig(this.config);
|
|
2183
|
-
this.ownerChatId = chatId;
|
|
2184
|
-
logger.info({ chatId, userId, username }, "Telegram paired to owner");
|
|
2185
|
-
await this.sendDirectMessage(chatId, this.getPairingStatusMessage(true));
|
|
2186
|
-
return;
|
|
2187
|
-
}
|
|
2188
|
-
await this.sendDirectMessage(
|
|
2189
|
-
chatId,
|
|
2190
|
-
"This Mercury instance is not paired yet. Send /start to pair this bot to your Telegram account."
|
|
2191
|
-
);
|
|
2192
|
-
}
|
|
2193
|
-
getPairingStatusMessage(newlyPaired = false) {
|
|
2194
|
-
const username = this.config.channels.telegram.pairedUsername ? ` (@${this.config.channels.telegram.pairedUsername})` : "";
|
|
2195
|
-
const prefix = newlyPaired ? "Telegram paired successfully." : "This Telegram account is already paired.";
|
|
2196
|
-
return `${prefix}
|
|
2197
|
-
|
|
2198
|
-
Owner user ID: ${this.config.channels.telegram.pairedUserId}${username}`;
|
|
2199
|
-
}
|
|
2200
|
-
unpair() {
|
|
2201
|
-
clearTelegramPairing(this.config);
|
|
2202
|
-
saveConfig(this.config);
|
|
2203
|
-
this.ownerChatId = null;
|
|
2204
|
-
logger.info("Telegram pairing cleared");
|
|
2205
|
-
}
|
|
2206
3155
|
async sendDirectMessage(chatId, content) {
|
|
2207
3156
|
if (!this.bot) return;
|
|
2208
3157
|
try {
|
|
@@ -3016,7 +3965,7 @@ import { existsSync as existsSync12, statSync as statSync2 } from "fs";
|
|
|
3016
3965
|
import { resolve as resolve9, basename, isAbsolute as isAbsolute7 } from "path";
|
|
3017
3966
|
function createSendFileTool(permissions, getCwd, sendFile) {
|
|
3018
3967
|
return tool7({
|
|
3019
|
-
description: "Send a file to the user. On Telegram the file is uploaded as an attachment. On CLI the file path and size are displayed. The path must be within an allowed read scope.",
|
|
3968
|
+
description: "Send a file to the user. On Telegram the file is uploaded as an attachment to the relevant approved recipients. On CLI the file path and size are displayed. The path must be within an allowed read scope.",
|
|
3020
3969
|
parameters: z7.object({
|
|
3021
3970
|
path: z7.string().describe("Absolute or relative path to the file to send")
|
|
3022
3971
|
}),
|
|
@@ -3054,9 +4003,9 @@ import { tool as tool8 } from "ai";
|
|
|
3054
4003
|
import { z as z8 } from "zod";
|
|
3055
4004
|
function createSendMessageTool(sendMessage) {
|
|
3056
4005
|
return tool8({
|
|
3057
|
-
description: "Send a message
|
|
4006
|
+
description: "Send a message through the configured outbound channel. For Telegram this sends to the approved Telegram recipients. Use this only when the user explicitly asks you to send something to Telegram or asks for scheduled results to be sent there.",
|
|
3058
4007
|
parameters: z8.object({
|
|
3059
|
-
content: z8.string().describe("The message content to send to the
|
|
4008
|
+
content: z8.string().describe("The message content to send to the approved Telegram recipients")
|
|
3060
4009
|
}),
|
|
3061
4010
|
execute: async ({ content }) => {
|
|
3062
4011
|
const trimmed = content.trim();
|
|
@@ -3065,7 +4014,7 @@ function createSendMessageTool(sendMessage) {
|
|
|
3065
4014
|
}
|
|
3066
4015
|
try {
|
|
3067
4016
|
await sendMessage(trimmed);
|
|
3068
|
-
return "Message sent to the
|
|
4017
|
+
return "Message sent to the approved Telegram recipients.";
|
|
3069
4018
|
} catch (err) {
|
|
3070
4019
|
return `Error sending message: ${err.message}`;
|
|
3071
4020
|
}
|
|
@@ -3828,11 +4777,42 @@ function createCreateIssueTool() {
|
|
|
3828
4777
|
// src/capabilities/github/github-api.ts
|
|
3829
4778
|
import { tool as tool30 } from "ai";
|
|
3830
4779
|
import { z as z30 } from "zod";
|
|
4780
|
+
var CO_AUTHOR_NAME = "Mercury";
|
|
4781
|
+
var CO_AUTHOR_EMAIL = "mercury@cosmicstack.org";
|
|
4782
|
+
var CO_AUTHOR_TRAILER = `Co-authored-by: ${CO_AUTHOR_NAME} <${CO_AUTHOR_EMAIL}>`;
|
|
4783
|
+
function isContentCreatePath(path3) {
|
|
4784
|
+
return /^\/repos\/[^/]+\/[^/]+\/contents\//.test(path3);
|
|
4785
|
+
}
|
|
4786
|
+
function injectCoAuthor(body) {
|
|
4787
|
+
const result = { ...body };
|
|
4788
|
+
if (typeof result.message === "string" && !result.message.includes(CO_AUTHOR_TRAILER)) {
|
|
4789
|
+
result.message += `
|
|
4790
|
+
|
|
4791
|
+
${CO_AUTHOR_TRAILER}`;
|
|
4792
|
+
}
|
|
4793
|
+
if (!result.committer || typeof result.committer !== "object") {
|
|
4794
|
+
result.committer = { name: CO_AUTHOR_NAME, email: CO_AUTHOR_EMAIL };
|
|
4795
|
+
}
|
|
4796
|
+
if (!result.author || typeof result.author !== "object") {
|
|
4797
|
+
result.author = { name: CO_AUTHOR_NAME, email: CO_AUTHOR_EMAIL };
|
|
4798
|
+
}
|
|
4799
|
+
return result;
|
|
4800
|
+
}
|
|
3831
4801
|
function createGithubApiTool() {
|
|
3832
4802
|
return tool30({
|
|
3833
|
-
description:
|
|
4803
|
+
description: `Make a raw request to the GitHub API. GET requests (read-only) are always allowed. Write operations (POST, PUT, PATCH, DELETE) may require user approval.
|
|
4804
|
+
|
|
4805
|
+
Common operations you can perform:
|
|
4806
|
+
- Push a file: PUT /repos/{owner}/{repo}/contents/{path} \u2014 body must include "message" (commit message) and "content" (base64-encoded file). For updates, also include "sha" from the current file. Co-authored-by Mercury is automatically included.
|
|
4807
|
+
- Delete a file: DELETE /repos/{owner}/{repo}/contents/{path} \u2014 body must include "message" and "sha".
|
|
4808
|
+
- List branches: GET /repos/{owner}/{repo}/branches
|
|
4809
|
+
- Get file contents: GET /repos/{owner}/{repo}/contents/{path}
|
|
4810
|
+
- Search code: GET /search/code?q={query}
|
|
4811
|
+
- Any other GitHub API v3 endpoint.
|
|
4812
|
+
|
|
4813
|
+
IMPORTANT: When the user wants to push code or files to GitHub and git push fails (auth issues, no SSH key, etc.), use PUT /repos/{owner}/{repo}/contents/{path} to create or update files directly through the API. This bypasses local git and creates a commit with Mercury as co-author.`,
|
|
3834
4814
|
parameters: z30.object({
|
|
3835
|
-
path: z30.string().describe("Full API path (e.g., /repos/owner/repo/issues or /
|
|
4815
|
+
path: z30.string().describe("Full API path (e.g., /repos/owner/repo/issues or /repos/owner/repo/contents/path/to/file)"),
|
|
3836
4816
|
method: z30.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).describe("HTTP method").default("GET"),
|
|
3837
4817
|
body: z30.string().describe("JSON body for write requests (as a JSON string)").optional()
|
|
3838
4818
|
}),
|
|
@@ -3846,6 +4826,9 @@ function createGithubApiTool() {
|
|
|
3846
4826
|
return "Error: body must be valid JSON.";
|
|
3847
4827
|
}
|
|
3848
4828
|
}
|
|
4829
|
+
if (parsedBody && isContentCreatePath(path3) && (method === "PUT" || method === "POST" || method === "PATCH")) {
|
|
4830
|
+
parsedBody = injectCoAuthor(parsedBody);
|
|
4831
|
+
}
|
|
3849
4832
|
const result = await githubRequest(path3, {
|
|
3850
4833
|
method,
|
|
3851
4834
|
body: parsedBody
|
|
@@ -4198,15 +5181,15 @@ Describe what this skill enables Mercury to do. When invoked via the use_skill t
|
|
|
4198
5181
|
};
|
|
4199
5182
|
|
|
4200
5183
|
// src/utils/manual.ts
|
|
4201
|
-
import
|
|
5184
|
+
import chalk4 from "chalk";
|
|
4202
5185
|
function getManual() {
|
|
4203
5186
|
const sections = [];
|
|
4204
5187
|
sections.push("");
|
|
4205
|
-
sections.push(
|
|
4206
|
-
sections.push(
|
|
5188
|
+
sections.push(chalk4.bold.cyan(" MERCURY \u2014 Capabilities & Commands"));
|
|
5189
|
+
sections.push(chalk4.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4207
5190
|
sections.push("");
|
|
4208
|
-
sections.push(
|
|
4209
|
-
sections.push(
|
|
5191
|
+
sections.push(chalk4.bold.white(" Built-in Tools"));
|
|
5192
|
+
sections.push(chalk4.dim(" Tools Mercury can use during conversations."));
|
|
4210
5193
|
sections.push("");
|
|
4211
5194
|
const tools = [
|
|
4212
5195
|
["read_file", "Read file contents", "path (required)"],
|
|
@@ -4215,7 +5198,7 @@ function getManual() {
|
|
|
4215
5198
|
["edit_file", "Replace specific text in a file", "path, old_string, new_string"],
|
|
4216
5199
|
["list_dir", "List directory contents", "path"],
|
|
4217
5200
|
["delete_file", "Delete a file", "path"],
|
|
4218
|
-
["send_message", "Send a message to
|
|
5201
|
+
["send_message", "Send a message to approved Telegram users", "content"],
|
|
4219
5202
|
["run_command", "Execute a shell command", "command"],
|
|
4220
5203
|
["approve_command", "Permanently approve a command type", 'command (e.g. "curl")'],
|
|
4221
5204
|
["fetch_url", "Fetch a URL and return content", "url, format? (text/markdown)"],
|
|
@@ -4234,12 +5217,12 @@ function getManual() {
|
|
|
4234
5217
|
["budget_status", "Check token budget", "\u2014"]
|
|
4235
5218
|
];
|
|
4236
5219
|
for (const [name, desc, params] of tools) {
|
|
4237
|
-
sections.push(` ${
|
|
4238
|
-
sections.push(` ${" ".repeat(24)} ${
|
|
5220
|
+
sections.push(` ${chalk4.cyan(name.padEnd(24))} ${desc}`);
|
|
5221
|
+
sections.push(` ${" ".repeat(24)} ${chalk4.dim(params)}`);
|
|
4239
5222
|
}
|
|
4240
5223
|
sections.push("");
|
|
4241
|
-
sections.push(
|
|
4242
|
-
sections.push(
|
|
5224
|
+
sections.push(chalk4.bold.white(" CLI Commands"));
|
|
5225
|
+
sections.push(chalk4.dim(" Run these from your terminal (no API calls consumed)."));
|
|
4243
5226
|
sections.push("");
|
|
4244
5227
|
const commands = [
|
|
4245
5228
|
["mercury up", "Start persistently (install service + daemon)"],
|
|
@@ -4252,7 +5235,13 @@ function getManual() {
|
|
|
4252
5235
|
["mercury doctor", "Reconfigure settings (Enter keeps current)"],
|
|
4253
5236
|
["mercury setup", "Re-run the setup wizard"],
|
|
4254
5237
|
["mercury status", "Show config and daemon status"],
|
|
4255
|
-
["mercury telegram
|
|
5238
|
+
["mercury telegram list", "Show Telegram admins, members, and pending requests"],
|
|
5239
|
+
["mercury telegram approve <code|id>", "Approve the first Telegram pairing code or a later Telegram request"],
|
|
5240
|
+
["mercury telegram reject <id>", "Reject a pending Telegram request"],
|
|
5241
|
+
["mercury telegram remove <id>", "Remove an approved Telegram user"],
|
|
5242
|
+
["mercury telegram promote <id>", "Promote a Telegram member to admin"],
|
|
5243
|
+
["mercury telegram demote <id>", "Demote a Telegram admin to member"],
|
|
5244
|
+
["mercury telegram unpair", "Reset all Telegram access"],
|
|
4256
5245
|
["mercury help", "Show this manual"],
|
|
4257
5246
|
["mercury service install", "Install as system service (auto-start)"],
|
|
4258
5247
|
["mercury service uninstall", "Uninstall system service"],
|
|
@@ -4260,29 +5249,40 @@ function getManual() {
|
|
|
4260
5249
|
["mercury --verbose", "Start with debug logging on stderr"]
|
|
4261
5250
|
];
|
|
4262
5251
|
for (const [cmd, desc] of commands) {
|
|
4263
|
-
sections.push(` ${
|
|
5252
|
+
sections.push(` ${chalk4.white(cmd.padEnd(26))} ${desc}`);
|
|
4264
5253
|
}
|
|
4265
5254
|
sections.push("");
|
|
4266
|
-
sections.push(
|
|
4267
|
-
sections.push(
|
|
5255
|
+
sections.push(chalk4.bold.white(" In-Chat Commands"));
|
|
5256
|
+
sections.push(chalk4.dim(" Type these during a conversation (no API calls)."));
|
|
4268
5257
|
sections.push("");
|
|
4269
5258
|
const chat = [
|
|
4270
|
-
["/start", "
|
|
4271
|
-
["/pair", "
|
|
5259
|
+
["/start", "Start Telegram pairing or request Telegram access"],
|
|
5260
|
+
["/pair", "Start Telegram pairing or request Telegram access"],
|
|
5261
|
+
["/", "Open the CLI command picker with arrow-key navigation"],
|
|
5262
|
+
["/menu", "Open the CLI command picker with arrow-key navigation"],
|
|
4272
5263
|
["/help", "Show this manual"],
|
|
4273
5264
|
["/status", "Show config and budget info"],
|
|
5265
|
+
["/telegram", "CLI chat only: open the Telegram management menu"],
|
|
5266
|
+
["/telegram pending", "CLI chat only: list pending Telegram requests"],
|
|
5267
|
+
["/telegram users", "CLI chat only: list approved Telegram users"],
|
|
5268
|
+
["/telegram approve <code|id>", "CLI chat only: approve the first pairing code or a later request"],
|
|
5269
|
+
["/telegram reject <id>", "CLI chat only: reject a pending Telegram request"],
|
|
5270
|
+
["/telegram remove <id>", "CLI chat only: remove an approved Telegram user"],
|
|
5271
|
+
["/telegram promote <id>", "CLI chat only: promote a Telegram member to admin"],
|
|
5272
|
+
["/telegram demote <id>", "CLI chat only: demote a Telegram admin to member"],
|
|
5273
|
+
["/telegram reset", "CLI chat only: reset all Telegram access"],
|
|
4274
5274
|
["/tools", "List currently loaded tools"],
|
|
4275
5275
|
["/skills", "List installed skills"],
|
|
4276
5276
|
["/stream", "Toggle text streaming on/off (Telegram)"],
|
|
4277
5277
|
["/stream on", "Enable streaming (live text updates)"],
|
|
4278
5278
|
["/stream off", "Disable streaming (single message)"],
|
|
4279
|
-
["/unpair", "
|
|
5279
|
+
["/unpair", "Reset all Telegram access for this Mercury instance (admins only)"]
|
|
4280
5280
|
];
|
|
4281
5281
|
for (const [cmd, desc] of chat) {
|
|
4282
|
-
sections.push(` ${
|
|
5282
|
+
sections.push(` ${chalk4.white(cmd.padEnd(16))} ${desc}`);
|
|
4283
5283
|
}
|
|
4284
5284
|
sections.push("");
|
|
4285
|
-
sections.push(
|
|
5285
|
+
sections.push(chalk4.bold.white(" Permissions"));
|
|
4286
5286
|
sections.push("");
|
|
4287
5287
|
const perms = [
|
|
4288
5288
|
"Commands are blocked (never run), auto-approved, or need approval.",
|
|
@@ -4291,10 +5291,10 @@ function getManual() {
|
|
|
4291
5291
|
"File access is scoped \u2014 new paths need approval (y/n/always)."
|
|
4292
5292
|
];
|
|
4293
5293
|
for (const p of perms) {
|
|
4294
|
-
sections.push(` ${
|
|
5294
|
+
sections.push(` ${chalk4.dim("\u2022")} ${p}`);
|
|
4295
5295
|
}
|
|
4296
5296
|
sections.push("");
|
|
4297
|
-
sections.push(
|
|
5297
|
+
sections.push(chalk4.bold.white(" Skills"));
|
|
4298
5298
|
sections.push("");
|
|
4299
5299
|
const skillInfo = [
|
|
4300
5300
|
"Skills live in ~/.mercury/skills/<name>/SKILL.md",
|
|
@@ -4303,10 +5303,10 @@ function getManual() {
|
|
|
4303
5303
|
'Schedule: "remind me daily at 9am to run daily-digest skill"'
|
|
4304
5304
|
];
|
|
4305
5305
|
for (const s of skillInfo) {
|
|
4306
|
-
sections.push(` ${
|
|
5306
|
+
sections.push(` ${chalk4.dim("\u2022")} ${s}`);
|
|
4307
5307
|
}
|
|
4308
5308
|
sections.push("");
|
|
4309
|
-
sections.push(
|
|
5309
|
+
sections.push(chalk4.bold.white(" Scheduling"));
|
|
4310
5310
|
sections.push("");
|
|
4311
5311
|
const schedInfo = [
|
|
4312
5312
|
'Recurring: "every day at 9am remind me to\u2026"',
|
|
@@ -4314,10 +5314,10 @@ function getManual() {
|
|
|
4314
5314
|
"Tasks persist to ~/.mercury/schedules.yaml"
|
|
4315
5315
|
];
|
|
4316
5316
|
for (const s of schedInfo) {
|
|
4317
|
-
sections.push(` ${
|
|
5317
|
+
sections.push(` ${chalk4.dim("\u2022")} ${s}`);
|
|
4318
5318
|
}
|
|
4319
5319
|
sections.push("");
|
|
4320
|
-
sections.push(
|
|
5320
|
+
sections.push(chalk4.bold.white(" Configuration"));
|
|
4321
5321
|
sections.push("");
|
|
4322
5322
|
const configInfo = [
|
|
4323
5323
|
["~/.mercury/mercury.yaml", "Main config (providers, channels, budget)"],
|
|
@@ -4329,10 +5329,10 @@ function getManual() {
|
|
|
4329
5329
|
["~/.mercury/memory/", "Short-term, long-term, episodic memory"]
|
|
4330
5330
|
];
|
|
4331
5331
|
for (const [path3, desc] of configInfo) {
|
|
4332
|
-
sections.push(` ${
|
|
5332
|
+
sections.push(` ${chalk4.dim(path3.padEnd(36))} ${desc}`);
|
|
4333
5333
|
}
|
|
4334
5334
|
sections.push("");
|
|
4335
|
-
sections.push(
|
|
5335
|
+
sections.push(chalk4.dim(" mercury.cosmicstack.org"));
|
|
4336
5336
|
sections.push("");
|
|
4337
5337
|
return sections.join("\n");
|
|
4338
5338
|
}
|
|
@@ -4342,7 +5342,7 @@ import { spawn } from "child_process";
|
|
|
4342
5342
|
import { existsSync as existsSync16, readFileSync as readFileSync11, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, mkdirSync as mkdirSync9, openSync } from "fs";
|
|
4343
5343
|
import { join as join9 } from "path";
|
|
4344
5344
|
import process2 from "process";
|
|
4345
|
-
import
|
|
5345
|
+
import chalk5 from "chalk";
|
|
4346
5346
|
var PID_FILE = "daemon.pid";
|
|
4347
5347
|
var LOG_FILE = "daemon.log";
|
|
4348
5348
|
function pidPath() {
|
|
@@ -4378,8 +5378,8 @@ function getDaemonStatus() {
|
|
|
4378
5378
|
function startBackground() {
|
|
4379
5379
|
const status = getDaemonStatus();
|
|
4380
5380
|
if (status.running && status.pid) {
|
|
4381
|
-
console.log(
|
|
4382
|
-
console.log(
|
|
5381
|
+
console.log(chalk5.yellow(` Mercury is already running (PID: ${status.pid})`));
|
|
5382
|
+
console.log(chalk5.dim(` Use \`mercury stop\` to stop it first.`));
|
|
4383
5383
|
console.log("");
|
|
4384
5384
|
process2.exit(1);
|
|
4385
5385
|
}
|
|
@@ -4405,21 +5405,21 @@ function startBackground() {
|
|
|
4405
5405
|
child.unref();
|
|
4406
5406
|
writeFileSync11(pidPath(), String(child.pid));
|
|
4407
5407
|
console.log("");
|
|
4408
|
-
console.log(
|
|
4409
|
-
console.log(
|
|
4410
|
-
console.log(
|
|
4411
|
-
console.log(
|
|
5408
|
+
console.log(chalk5.green(` Mercury started in background (PID: ${child.pid})`));
|
|
5409
|
+
console.log(chalk5.dim(` Logs: ${logFile}`));
|
|
5410
|
+
console.log(chalk5.dim(` Use \`mercury stop\` to stop.`));
|
|
5411
|
+
console.log(chalk5.dim(` Use \`mercury logs\` to view logs.`));
|
|
4412
5412
|
console.log("");
|
|
4413
5413
|
}
|
|
4414
5414
|
function stopDaemon() {
|
|
4415
5415
|
const status = getDaemonStatus();
|
|
4416
5416
|
if (!status.pid) {
|
|
4417
|
-
console.log(
|
|
5417
|
+
console.log(chalk5.yellow(" Mercury is not running as a daemon."));
|
|
4418
5418
|
console.log("");
|
|
4419
5419
|
process2.exit(0);
|
|
4420
5420
|
}
|
|
4421
5421
|
if (!status.running) {
|
|
4422
|
-
console.log(
|
|
5422
|
+
console.log(chalk5.yellow(` Stale PID file found (PID: ${status.pid} is not running). Cleaning up.`));
|
|
4423
5423
|
try {
|
|
4424
5424
|
unlinkSync3(pidPath());
|
|
4425
5425
|
} catch {
|
|
@@ -4433,9 +5433,9 @@ function stopDaemon() {
|
|
|
4433
5433
|
} else {
|
|
4434
5434
|
process2.kill(status.pid, "SIGTERM");
|
|
4435
5435
|
}
|
|
4436
|
-
console.log(
|
|
5436
|
+
console.log(chalk5.green(` Mercury stopped (PID: ${status.pid})`));
|
|
4437
5437
|
} catch {
|
|
4438
|
-
console.log(
|
|
5438
|
+
console.log(chalk5.red(` Failed to stop PID ${status.pid}. You may need to kill it manually.`));
|
|
4439
5439
|
}
|
|
4440
5440
|
try {
|
|
4441
5441
|
unlinkSync3(pidPath());
|
|
@@ -4446,7 +5446,7 @@ function stopDaemon() {
|
|
|
4446
5446
|
function restartDaemon() {
|
|
4447
5447
|
const status = getDaemonStatus();
|
|
4448
5448
|
if (status.running && status.pid) {
|
|
4449
|
-
console.log(
|
|
5449
|
+
console.log(chalk5.yellow(` Stopping Mercury (PID: ${status.pid})...`));
|
|
4450
5450
|
try {
|
|
4451
5451
|
if (process2.platform === "win32") {
|
|
4452
5452
|
process2.kill(status.pid);
|
|
@@ -4459,20 +5459,20 @@ function restartDaemon() {
|
|
|
4459
5459
|
unlinkSync3(pidPath());
|
|
4460
5460
|
} catch {
|
|
4461
5461
|
}
|
|
4462
|
-
console.log(
|
|
5462
|
+
console.log(chalk5.green(" Mercury stopped."));
|
|
4463
5463
|
} else if (status.pid) {
|
|
4464
5464
|
try {
|
|
4465
5465
|
unlinkSync3(pidPath());
|
|
4466
5466
|
} catch {
|
|
4467
5467
|
}
|
|
4468
5468
|
}
|
|
4469
|
-
console.log(
|
|
5469
|
+
console.log(chalk5.yellow(" Starting Mercury..."));
|
|
4470
5470
|
startBackground();
|
|
4471
5471
|
}
|
|
4472
5472
|
function showLogs() {
|
|
4473
5473
|
const logFile = logPath();
|
|
4474
5474
|
if (!existsSync16(logFile)) {
|
|
4475
|
-
console.log(
|
|
5475
|
+
console.log(chalk5.dim(" No daemon log file found."));
|
|
4476
5476
|
console.log("");
|
|
4477
5477
|
return;
|
|
4478
5478
|
}
|
|
@@ -4520,7 +5520,7 @@ function tryAutoDaemonize() {
|
|
|
4520
5520
|
import { existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4 } from "fs";
|
|
4521
5521
|
import { join as join10 } from "path";
|
|
4522
5522
|
import { homedir as homedir3 } from "os";
|
|
4523
|
-
import
|
|
5523
|
+
import chalk6 from "chalk";
|
|
4524
5524
|
import { execSync as execSync8 } from "child_process";
|
|
4525
5525
|
var SERVICE_DESC = "Mercury \u2014 Soul-Driven AI Agent";
|
|
4526
5526
|
var WIN_TASK_NAME = "MercuryAgent";
|
|
@@ -4555,7 +5555,7 @@ function installService() {
|
|
|
4555
5555
|
} else if (platform === "win32") {
|
|
4556
5556
|
installWindows();
|
|
4557
5557
|
} else {
|
|
4558
|
-
console.log(
|
|
5558
|
+
console.log(chalk6.red(` Unsupported platform: ${platform}`));
|
|
4559
5559
|
process.exit(1);
|
|
4560
5560
|
}
|
|
4561
5561
|
}
|
|
@@ -4568,7 +5568,7 @@ function uninstallService() {
|
|
|
4568
5568
|
} else if (platform === "win32") {
|
|
4569
5569
|
uninstallWindows();
|
|
4570
5570
|
} else {
|
|
4571
|
-
console.log(
|
|
5571
|
+
console.log(chalk6.red(` Unsupported platform: ${platform}`));
|
|
4572
5572
|
process.exit(1);
|
|
4573
5573
|
}
|
|
4574
5574
|
}
|
|
@@ -4632,22 +5632,22 @@ function installMac() {
|
|
|
4632
5632
|
try {
|
|
4633
5633
|
execSync8(`launchctl load ${plistPath}`, { stdio: "inherit" });
|
|
4634
5634
|
} catch {
|
|
4635
|
-
console.log(
|
|
4636
|
-
console.log(
|
|
5635
|
+
console.log(chalk6.yellow(" launchctl load failed. Try running:"));
|
|
5636
|
+
console.log(chalk6.dim(` launchctl load ${plistPath}`));
|
|
4637
5637
|
}
|
|
4638
5638
|
console.log("");
|
|
4639
|
-
console.log(
|
|
4640
|
-
console.log(
|
|
4641
|
-
console.log(
|
|
4642
|
-
console.log(
|
|
5639
|
+
console.log(chalk6.green(" Mercury service installed (macOS LaunchAgent)"));
|
|
5640
|
+
console.log(chalk6.dim(` Plist: ${plistPath}`));
|
|
5641
|
+
console.log(chalk6.dim(` Logs: ${logPath2}`));
|
|
5642
|
+
console.log(chalk6.dim(" Auto-starts on login. Auto-restarts on crash."));
|
|
4643
5643
|
console.log("");
|
|
4644
|
-
console.log(
|
|
5644
|
+
console.log(chalk6.dim(" Uninstall: mercury service uninstall"));
|
|
4645
5645
|
console.log("");
|
|
4646
5646
|
}
|
|
4647
5647
|
function uninstallMac() {
|
|
4648
5648
|
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
4649
5649
|
if (!existsSync17(plistPath)) {
|
|
4650
|
-
console.log(
|
|
5650
|
+
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
4651
5651
|
console.log("");
|
|
4652
5652
|
process.exit(0);
|
|
4653
5653
|
}
|
|
@@ -4658,28 +5658,28 @@ function uninstallMac() {
|
|
|
4658
5658
|
try {
|
|
4659
5659
|
unlinkSync4(plistPath);
|
|
4660
5660
|
} catch {
|
|
4661
|
-
console.log(
|
|
4662
|
-
console.log(
|
|
5661
|
+
console.log(chalk6.yellow(" Failed to remove plist file. Remove manually:"));
|
|
5662
|
+
console.log(chalk6.dim(` rm ${plistPath}`));
|
|
4663
5663
|
}
|
|
4664
5664
|
console.log("");
|
|
4665
|
-
console.log(
|
|
5665
|
+
console.log(chalk6.green(" Mercury service uninstalled"));
|
|
4666
5666
|
console.log("");
|
|
4667
5667
|
}
|
|
4668
5668
|
function showMacStatus() {
|
|
4669
5669
|
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
4670
5670
|
if (!existsSync17(plistPath)) {
|
|
4671
|
-
console.log(
|
|
4672
|
-
console.log(
|
|
5671
|
+
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
5672
|
+
console.log(chalk6.dim(" Run `mercury service install` to set it up."));
|
|
4673
5673
|
console.log("");
|
|
4674
5674
|
return;
|
|
4675
5675
|
}
|
|
4676
5676
|
try {
|
|
4677
5677
|
const output = execSync8("launchctl list | grep com.cosmicstack.mercury", { encoding: "utf-8" }).trim();
|
|
4678
|
-
console.log(` ${
|
|
4679
|
-
console.log(
|
|
5678
|
+
console.log(` ${chalk6.green("Service installed and loaded")}`);
|
|
5679
|
+
console.log(chalk6.dim(` ${output}`));
|
|
4680
5680
|
} catch {
|
|
4681
|
-
console.log(` ${
|
|
4682
|
-
console.log(
|
|
5681
|
+
console.log(` ${chalk6.yellow("Service installed but not loaded")}`);
|
|
5682
|
+
console.log(chalk6.dim(` Plist: ${plistPath}`));
|
|
4683
5683
|
}
|
|
4684
5684
|
console.log("");
|
|
4685
5685
|
}
|
|
@@ -4715,30 +5715,30 @@ WantedBy=default.target`;
|
|
|
4715
5715
|
execSync8("systemctl --user enable mercury.service", { stdio: "inherit" });
|
|
4716
5716
|
execSync8("systemctl --user start mercury.service", { stdio: "inherit" });
|
|
4717
5717
|
} catch (err) {
|
|
4718
|
-
console.log(
|
|
4719
|
-
console.log(
|
|
4720
|
-
console.log(
|
|
4721
|
-
console.log(
|
|
5718
|
+
console.log(chalk6.yellow(" systemd commands failed. Try running manually:"));
|
|
5719
|
+
console.log(chalk6.dim(" systemctl --user daemon-reload"));
|
|
5720
|
+
console.log(chalk6.dim(" systemctl --user enable mercury.service"));
|
|
5721
|
+
console.log(chalk6.dim(" systemctl --user start mercury.service"));
|
|
4722
5722
|
}
|
|
4723
5723
|
try {
|
|
4724
5724
|
execSync8(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
|
|
4725
5725
|
} catch {
|
|
4726
|
-
console.log(
|
|
4727
|
-
console.log(
|
|
5726
|
+
console.log(chalk6.yellow(" Enable linger failed (needed for boot-without-login). Try:"));
|
|
5727
|
+
console.log(chalk6.dim(` sudo loginctl enable-linger ${process.env.USER || "$USER"}`));
|
|
4728
5728
|
}
|
|
4729
5729
|
console.log("");
|
|
4730
|
-
console.log(
|
|
4731
|
-
console.log(
|
|
4732
|
-
console.log(
|
|
4733
|
-
console.log(
|
|
5730
|
+
console.log(chalk6.green(" Mercury service installed (systemd --user)"));
|
|
5731
|
+
console.log(chalk6.dim(` Service: ${servicePath}`));
|
|
5732
|
+
console.log(chalk6.dim(` Logs: ${join10(home, "daemon.log")}`));
|
|
5733
|
+
console.log(chalk6.dim(" Auto-starts on login. Auto-restarts on crash (5s delay)."));
|
|
4734
5734
|
console.log("");
|
|
4735
|
-
console.log(
|
|
5735
|
+
console.log(chalk6.dim(" Uninstall: mercury service uninstall"));
|
|
4736
5736
|
console.log("");
|
|
4737
5737
|
}
|
|
4738
5738
|
function uninstallLinux() {
|
|
4739
5739
|
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4740
5740
|
if (!existsSync17(servicePath)) {
|
|
4741
|
-
console.log(
|
|
5741
|
+
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
4742
5742
|
console.log("");
|
|
4743
5743
|
process.exit(0);
|
|
4744
5744
|
}
|
|
@@ -4750,22 +5750,22 @@ function uninstallLinux() {
|
|
|
4750
5750
|
try {
|
|
4751
5751
|
unlinkSync4(servicePath);
|
|
4752
5752
|
} catch {
|
|
4753
|
-
console.log(
|
|
4754
|
-
console.log(
|
|
5753
|
+
console.log(chalk6.yellow(" Failed to remove service file. Remove manually:"));
|
|
5754
|
+
console.log(chalk6.dim(` rm ${servicePath}`));
|
|
4755
5755
|
}
|
|
4756
5756
|
try {
|
|
4757
5757
|
execSync8("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
4758
5758
|
} catch {
|
|
4759
5759
|
}
|
|
4760
5760
|
console.log("");
|
|
4761
|
-
console.log(
|
|
5761
|
+
console.log(chalk6.green(" Mercury service uninstalled"));
|
|
4762
5762
|
console.log("");
|
|
4763
5763
|
}
|
|
4764
5764
|
function showLinuxStatus() {
|
|
4765
5765
|
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4766
5766
|
if (!existsSync17(servicePath)) {
|
|
4767
|
-
console.log(
|
|
4768
|
-
console.log(
|
|
5767
|
+
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
5768
|
+
console.log(chalk6.dim(" Run `mercury service install` to set it up."));
|
|
4769
5769
|
console.log("");
|
|
4770
5770
|
return;
|
|
4771
5771
|
}
|
|
@@ -4773,8 +5773,8 @@ function showLinuxStatus() {
|
|
|
4773
5773
|
const output = execSync8("systemctl --user status mercury.service", { encoding: "utf-8" }).trim();
|
|
4774
5774
|
console.log(output);
|
|
4775
5775
|
} catch (err) {
|
|
4776
|
-
console.log(
|
|
4777
|
-
console.log(
|
|
5776
|
+
console.log(chalk6.yellow(" Could not get service status:"));
|
|
5777
|
+
console.log(chalk6.dim(` ${err.message || err}`));
|
|
4778
5778
|
}
|
|
4779
5779
|
console.log("");
|
|
4780
5780
|
}
|
|
@@ -4790,33 +5790,33 @@ function installWindows() {
|
|
|
4790
5790
|
{ stdio: "inherit", shell: "cmd.exe" }
|
|
4791
5791
|
);
|
|
4792
5792
|
} catch {
|
|
4793
|
-
console.log(
|
|
4794
|
-
console.log(
|
|
5793
|
+
console.log(chalk6.yellow(" schtasks create failed. Try running from an Administrator cmd:"));
|
|
5794
|
+
console.log(chalk6.dim(` schtasks /create /tn "${WIN_TASK_NAME}" /tr "${cmd}" /sc onlogon /rl limited /f`));
|
|
4795
5795
|
}
|
|
4796
5796
|
try {
|
|
4797
5797
|
execSync8(`schtasks /run /tn "${WIN_TASK_NAME}"`, { stdio: "inherit", shell: "cmd.exe" });
|
|
4798
5798
|
} catch {
|
|
4799
|
-
console.log(
|
|
5799
|
+
console.log(chalk6.yellow(" Task created but failed to start immediately. It will start on next login."));
|
|
4800
5800
|
}
|
|
4801
5801
|
console.log("");
|
|
4802
|
-
console.log(
|
|
4803
|
-
console.log(
|
|
4804
|
-
console.log(
|
|
4805
|
-
console.log(
|
|
4806
|
-
console.log(
|
|
5802
|
+
console.log(chalk6.green(" Mercury service installed (Windows Task Scheduler)"));
|
|
5803
|
+
console.log(chalk6.dim(` Task: ${WIN_TASK_NAME}`));
|
|
5804
|
+
console.log(chalk6.dim(` Trigger: on logon`));
|
|
5805
|
+
console.log(chalk6.dim(` Logs: ${logPath2}`));
|
|
5806
|
+
console.log(chalk6.dim(" Auto-starts on login. Use --daemon flag for crash recovery."));
|
|
4807
5807
|
console.log("");
|
|
4808
|
-
console.log(
|
|
5808
|
+
console.log(chalk6.dim(" Uninstall: mercury service uninstall"));
|
|
4809
5809
|
console.log("");
|
|
4810
5810
|
}
|
|
4811
5811
|
function uninstallWindows() {
|
|
4812
5812
|
try {
|
|
4813
5813
|
execSync8(`schtasks /delete /tn "${WIN_TASK_NAME}" /f`, { stdio: "inherit", shell: "cmd.exe" });
|
|
4814
5814
|
console.log("");
|
|
4815
|
-
console.log(
|
|
5815
|
+
console.log(chalk6.green(" Mercury service uninstalled"));
|
|
4816
5816
|
console.log("");
|
|
4817
5817
|
} catch {
|
|
4818
|
-
console.log(
|
|
4819
|
-
console.log(
|
|
5818
|
+
console.log(chalk6.yellow(" Task not found or failed to delete. Remove manually:"));
|
|
5819
|
+
console.log(chalk6.dim(` schtasks /delete /tn "${WIN_TASK_NAME}" /f`));
|
|
4820
5820
|
console.log("");
|
|
4821
5821
|
}
|
|
4822
5822
|
}
|
|
@@ -4829,8 +5829,8 @@ function showWindowsStatus() {
|
|
|
4829
5829
|
console.log(output);
|
|
4830
5830
|
console.log("");
|
|
4831
5831
|
} catch {
|
|
4832
|
-
console.log(
|
|
4833
|
-
console.log(
|
|
5832
|
+
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
5833
|
+
console.log(chalk6.dim(" Run `mercury service install` to set it up."));
|
|
4834
5834
|
console.log("");
|
|
4835
5835
|
}
|
|
4836
5836
|
}
|
|
@@ -4865,11 +5865,208 @@ function sleep(ms) {
|
|
|
4865
5865
|
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
4866
5866
|
}
|
|
4867
5867
|
|
|
5868
|
+
// src/utils/provider-models.ts
|
|
5869
|
+
var MAX_MODEL_OPTIONS = 7;
|
|
5870
|
+
var OPENAI_PREFERRED_MODELS = [
|
|
5871
|
+
"gpt-5.2",
|
|
5872
|
+
"gpt-5.2-chat-latest",
|
|
5873
|
+
"gpt-5.2-pro",
|
|
5874
|
+
"gpt-5-mini",
|
|
5875
|
+
"gpt-5-nano",
|
|
5876
|
+
"gpt-4.1",
|
|
5877
|
+
"gpt-4.1-mini",
|
|
5878
|
+
"gpt-oss-120b",
|
|
5879
|
+
"gpt-oss-20b"
|
|
5880
|
+
];
|
|
5881
|
+
var ANTHROPIC_PREFERRED_MODELS = [
|
|
5882
|
+
"claude-sonnet-4-20250514",
|
|
5883
|
+
"claude-opus-4-20250514",
|
|
5884
|
+
"claude-3-7-sonnet-latest",
|
|
5885
|
+
"claude-3-5-sonnet-latest",
|
|
5886
|
+
"claude-3-5-haiku-latest"
|
|
5887
|
+
];
|
|
5888
|
+
var DEEPSEEK_PREFERRED_MODELS = [
|
|
5889
|
+
"deepseek-chat",
|
|
5890
|
+
"deepseek-reasoner"
|
|
5891
|
+
];
|
|
5892
|
+
var GROK_PREFERRED_MODELS = [
|
|
5893
|
+
"grok-4",
|
|
5894
|
+
"grok-4-latest",
|
|
5895
|
+
"grok-4.20",
|
|
5896
|
+
"grok-3",
|
|
5897
|
+
"grok-3-latest"
|
|
5898
|
+
];
|
|
5899
|
+
var OLLAMA_CLOUD_PREFERRED_MODELS = [
|
|
5900
|
+
"gpt-oss:120b",
|
|
5901
|
+
"gpt-oss:120b-cloud",
|
|
5902
|
+
"gpt-oss:20b"
|
|
5903
|
+
];
|
|
5904
|
+
var OLLAMA_LOCAL_PREFERRED_MODELS = [
|
|
5905
|
+
"gpt-oss:20b",
|
|
5906
|
+
"gpt-oss:120b"
|
|
5907
|
+
];
|
|
5908
|
+
var ProviderModelFetchError = class extends Error {
|
|
5909
|
+
constructor(message) {
|
|
5910
|
+
super(message);
|
|
5911
|
+
this.name = "ProviderModelFetchError";
|
|
5912
|
+
}
|
|
5913
|
+
};
|
|
5914
|
+
function trimTrailingSlash(value) {
|
|
5915
|
+
return value.replace(/\/+$/, "");
|
|
5916
|
+
}
|
|
5917
|
+
async function fetchJson(url, init, invalidMessage) {
|
|
5918
|
+
let response;
|
|
5919
|
+
try {
|
|
5920
|
+
response = await fetch(url, {
|
|
5921
|
+
...init,
|
|
5922
|
+
signal: AbortSignal.timeout(1e4)
|
|
5923
|
+
});
|
|
5924
|
+
} catch {
|
|
5925
|
+
throw new ProviderModelFetchError(invalidMessage);
|
|
5926
|
+
}
|
|
5927
|
+
if (!response.ok) {
|
|
5928
|
+
throw new ProviderModelFetchError(invalidMessage);
|
|
5929
|
+
}
|
|
5930
|
+
try {
|
|
5931
|
+
return await response.json();
|
|
5932
|
+
} catch {
|
|
5933
|
+
throw new ProviderModelFetchError("Mercury could not read the model list returned by this provider.");
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
function uniq(values) {
|
|
5937
|
+
return [...new Set(values.filter(Boolean))];
|
|
5938
|
+
}
|
|
5939
|
+
function prioritizeModels(models, preferred) {
|
|
5940
|
+
const preferredSet = new Set(preferred);
|
|
5941
|
+
const preferredMatches = preferred.filter((model) => models.includes(model));
|
|
5942
|
+
const others = models.filter((model) => !preferredSet.has(model)).sort((a, b) => a.localeCompare(b));
|
|
5943
|
+
return [...preferredMatches, ...others];
|
|
5944
|
+
}
|
|
5945
|
+
function limitModels(models) {
|
|
5946
|
+
return models.slice(0, MAX_MODEL_OPTIONS);
|
|
5947
|
+
}
|
|
5948
|
+
function isOpenAIChatModel(id) {
|
|
5949
|
+
const lower = id.toLowerCase();
|
|
5950
|
+
if (lower.includes("image") || lower.includes("audio") || lower.includes("tts") || lower.includes("transcribe") || lower.includes("embedding") || lower.includes("moderation") || lower.includes("realtime") || lower.includes("whisper") || lower.includes("search") || lower.includes("computer")) {
|
|
5951
|
+
return false;
|
|
5952
|
+
}
|
|
5953
|
+
return lower.startsWith("gpt-") || /^o\d/.test(lower);
|
|
5954
|
+
}
|
|
5955
|
+
function chooseRecommendedModel(provider, models, currentModel) {
|
|
5956
|
+
const preferredByProvider = {
|
|
5957
|
+
deepseek: DEEPSEEK_PREFERRED_MODELS,
|
|
5958
|
+
openai: OPENAI_PREFERRED_MODELS,
|
|
5959
|
+
anthropic: ANTHROPIC_PREFERRED_MODELS,
|
|
5960
|
+
grok: GROK_PREFERRED_MODELS,
|
|
5961
|
+
ollamaCloud: OLLAMA_CLOUD_PREFERRED_MODELS,
|
|
5962
|
+
ollamaLocal: OLLAMA_LOCAL_PREFERRED_MODELS
|
|
5963
|
+
};
|
|
5964
|
+
for (const candidate of preferredByProvider[provider]) {
|
|
5965
|
+
if (models.includes(candidate)) {
|
|
5966
|
+
return candidate;
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
if (currentModel && models.includes(currentModel)) {
|
|
5970
|
+
return currentModel;
|
|
5971
|
+
}
|
|
5972
|
+
return models[0];
|
|
5973
|
+
}
|
|
5974
|
+
function buildModelCatalog(provider, models, currentModel) {
|
|
5975
|
+
const filtered = uniq(models);
|
|
5976
|
+
if (filtered.length === 0) {
|
|
5977
|
+
throw new ProviderModelFetchError("Mercury could not find any supported chat models for this provider.");
|
|
5978
|
+
}
|
|
5979
|
+
const recommendedModel = chooseRecommendedModel(provider, filtered, currentModel);
|
|
5980
|
+
const preferredByProvider = {
|
|
5981
|
+
deepseek: DEEPSEEK_PREFERRED_MODELS,
|
|
5982
|
+
openai: OPENAI_PREFERRED_MODELS,
|
|
5983
|
+
anthropic: ANTHROPIC_PREFERRED_MODELS,
|
|
5984
|
+
grok: GROK_PREFERRED_MODELS,
|
|
5985
|
+
ollamaCloud: OLLAMA_CLOUD_PREFERRED_MODELS,
|
|
5986
|
+
ollamaLocal: OLLAMA_LOCAL_PREFERRED_MODELS
|
|
5987
|
+
};
|
|
5988
|
+
const withoutRecommended = filtered.filter((model) => model !== recommendedModel);
|
|
5989
|
+
const prioritized = prioritizeModels(withoutRecommended, preferredByProvider[provider]);
|
|
5990
|
+
return {
|
|
5991
|
+
recommendedModel,
|
|
5992
|
+
models: limitModels(prioritized)
|
|
5993
|
+
};
|
|
5994
|
+
}
|
|
5995
|
+
async function fetchOpenAICompatModels(provider, config) {
|
|
5996
|
+
const data = await fetchJson(
|
|
5997
|
+
`${trimTrailingSlash(config.baseUrl)}/models`,
|
|
5998
|
+
{
|
|
5999
|
+
headers: {
|
|
6000
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
6001
|
+
}
|
|
6002
|
+
},
|
|
6003
|
+
`Mercury could not fetch models for this ${provider === "grok" ? "Grok" : provider === "deepseek" ? "DeepSeek" : "OpenAI"} key. Please re-enter it.`
|
|
6004
|
+
);
|
|
6005
|
+
const ids = (data.data ?? []).map((model) => model.id?.trim() ?? "").filter((id) => {
|
|
6006
|
+
if (provider === "deepseek") {
|
|
6007
|
+
return id.startsWith("deepseek-");
|
|
6008
|
+
}
|
|
6009
|
+
return isOpenAIChatModel(id);
|
|
6010
|
+
});
|
|
6011
|
+
return buildModelCatalog(provider, ids, config.model);
|
|
6012
|
+
}
|
|
6013
|
+
async function fetchAnthropicModels(config) {
|
|
6014
|
+
const data = await fetchJson(
|
|
6015
|
+
"https://api.anthropic.com/v1/models",
|
|
6016
|
+
{
|
|
6017
|
+
headers: {
|
|
6018
|
+
"x-api-key": config.apiKey,
|
|
6019
|
+
"anthropic-version": "2023-06-01"
|
|
6020
|
+
}
|
|
6021
|
+
},
|
|
6022
|
+
"Mercury could not fetch models for this Anthropic key. Please re-enter it."
|
|
6023
|
+
);
|
|
6024
|
+
const ids = (data.data ?? []).map((model) => model.id?.trim() ?? "").filter((id) => id.startsWith("claude-"));
|
|
6025
|
+
return buildModelCatalog("anthropic", ids, config.model);
|
|
6026
|
+
}
|
|
6027
|
+
async function fetchGrokModels(config) {
|
|
6028
|
+
const data = await fetchJson(
|
|
6029
|
+
`${trimTrailingSlash(config.baseUrl)}/language-models`,
|
|
6030
|
+
{
|
|
6031
|
+
headers: {
|
|
6032
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
6033
|
+
}
|
|
6034
|
+
},
|
|
6035
|
+
"Mercury could not fetch models for this Grok key. Please re-enter it."
|
|
6036
|
+
);
|
|
6037
|
+
const ids = (data.data ?? []).filter((model) => model.output_modalities?.includes("text") || model.output_modalities == null).map((model) => model.id?.trim() ?? "").filter((id) => id.startsWith("grok-"));
|
|
6038
|
+
return buildModelCatalog("grok", ids, config.model);
|
|
6039
|
+
}
|
|
6040
|
+
async function fetchOllamaModels(provider, config) {
|
|
6041
|
+
const headers = config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : void 0;
|
|
6042
|
+
const data = await fetchJson(
|
|
6043
|
+
`${trimTrailingSlash(config.baseUrl)}/tags`,
|
|
6044
|
+
{
|
|
6045
|
+
headers
|
|
6046
|
+
},
|
|
6047
|
+
provider === "ollamaCloud" ? "Mercury could not fetch models for this Ollama Cloud key. Please re-enter it." : "Mercury could not fetch models from this Ollama Local server. Please check the base URL and try again."
|
|
6048
|
+
);
|
|
6049
|
+
const ids = (data.models ?? []).map((model) => model.model?.trim() || model.name?.trim() || "").filter(Boolean);
|
|
6050
|
+
return buildModelCatalog(provider, ids, config.model);
|
|
6051
|
+
}
|
|
6052
|
+
async function fetchProviderModelCatalog(provider, config) {
|
|
6053
|
+
if (provider === "anthropic") {
|
|
6054
|
+
return fetchAnthropicModels(config);
|
|
6055
|
+
}
|
|
6056
|
+
if (provider === "grok") {
|
|
6057
|
+
return fetchGrokModels(config);
|
|
6058
|
+
}
|
|
6059
|
+
if (provider === "ollamaCloud" || provider === "ollamaLocal") {
|
|
6060
|
+
return fetchOllamaModels(provider, config);
|
|
6061
|
+
}
|
|
6062
|
+
return fetchOpenAICompatModels(provider, config);
|
|
6063
|
+
}
|
|
6064
|
+
|
|
4868
6065
|
// src/index.ts
|
|
4869
6066
|
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
4870
6067
|
var pkgVersion = JSON.parse(readFileSync12(join11(__dirname, "..", "package.json"), "utf8")).version;
|
|
4871
6068
|
function hr() {
|
|
4872
|
-
console.log(
|
|
6069
|
+
console.log(chalk7.dim("\u2500".repeat(50)));
|
|
4873
6070
|
}
|
|
4874
6071
|
var MERCURY_ASCII = [
|
|
4875
6072
|
" __ _____________ ________ ________ __",
|
|
@@ -4881,26 +6078,26 @@ var MERCURY_ASCII = [
|
|
|
4881
6078
|
function banner() {
|
|
4882
6079
|
console.log("");
|
|
4883
6080
|
for (const line of MERCURY_ASCII) {
|
|
4884
|
-
console.log(
|
|
6081
|
+
console.log(chalk7.bold.cyan(` ${line}`));
|
|
4885
6082
|
}
|
|
4886
6083
|
console.log("");
|
|
4887
|
-
console.log(
|
|
4888
|
-
console.log(
|
|
6084
|
+
console.log(chalk7.white(" an AI agent for personal tasks"));
|
|
6085
|
+
console.log(chalk7.dim(` v${pkgVersion} \xB7 by Cosmic Stack \xB7 mercury.cosmicstack.org`));
|
|
4889
6086
|
console.log("");
|
|
4890
6087
|
}
|
|
4891
6088
|
function splashScreen() {
|
|
4892
6089
|
console.log("");
|
|
4893
6090
|
for (const line of MERCURY_ASCII) {
|
|
4894
|
-
console.log(
|
|
6091
|
+
console.log(chalk7.bold.cyan(` ${line}`));
|
|
4895
6092
|
}
|
|
4896
6093
|
console.log("");
|
|
4897
|
-
console.log(
|
|
4898
|
-
console.log(
|
|
4899
|
-
console.log(
|
|
6094
|
+
console.log(chalk7.dim(" an AI agent for personal tasks"));
|
|
6095
|
+
console.log(chalk7.cyan(" by Cosmic Stack"));
|
|
6096
|
+
console.log(chalk7.dim(" mercury.cosmicstack.org"));
|
|
4900
6097
|
console.log("");
|
|
4901
6098
|
}
|
|
4902
6099
|
async function ask(prompt) {
|
|
4903
|
-
const rl =
|
|
6100
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
4904
6101
|
return new Promise((resolve13) => {
|
|
4905
6102
|
rl.question(prompt, (answer) => {
|
|
4906
6103
|
rl.close();
|
|
@@ -4949,14 +6146,14 @@ async function chooseProvidersToConfigure(config, isReconfig) {
|
|
|
4949
6146
|
for (let i = 0; i < PROVIDER_OPTIONS.length; i++) {
|
|
4950
6147
|
const option = PROVIDER_OPTIONS[i];
|
|
4951
6148
|
const status = configured.includes(option.key) ? " (configured)" : "";
|
|
4952
|
-
console.log(
|
|
6149
|
+
console.log(chalk7.white(` ${i + 1}. ${option.label}${status}`));
|
|
4953
6150
|
}
|
|
4954
6151
|
console.log("");
|
|
4955
|
-
const prompt = isReconfig ?
|
|
6152
|
+
const prompt = isReconfig ? chalk7.white(" Choose providers to configure [comma-separated, Enter keeps current]: ") : chalk7.white(" Choose providers to configure [comma-separated, Enter for DeepSeek]: ");
|
|
4956
6153
|
const input = await ask(prompt);
|
|
4957
6154
|
const parsed = parseProviderSelection(input);
|
|
4958
6155
|
if (parsed === null) {
|
|
4959
|
-
console.log(
|
|
6156
|
+
console.log(chalk7.red(" Please choose valid provider numbers, like `1` or `1,3,5`."));
|
|
4960
6157
|
console.log("");
|
|
4961
6158
|
continue;
|
|
4962
6159
|
}
|
|
@@ -4972,23 +6169,23 @@ async function chooseDefaultProvider(config) {
|
|
|
4972
6169
|
}
|
|
4973
6170
|
if (configured.length === 1) {
|
|
4974
6171
|
config.providers.default = configured[0];
|
|
4975
|
-
console.log(
|
|
6172
|
+
console.log(chalk7.dim(` Default provider set to ${getProviderLabel(configured[0])}`));
|
|
4976
6173
|
return;
|
|
4977
6174
|
}
|
|
4978
6175
|
const suggested = configured.includes("deepseek") ? "deepseek" : configured[0];
|
|
4979
6176
|
console.log("");
|
|
4980
|
-
console.log(
|
|
4981
|
-
console.log(
|
|
6177
|
+
console.log(chalk7.bold.white(" Default Provider"));
|
|
6178
|
+
console.log(chalk7.dim(" Select the LLM provider Mercury should use first."));
|
|
4982
6179
|
console.log("");
|
|
4983
6180
|
for (let i = 0; i < configured.length; i++) {
|
|
4984
6181
|
const provider = configured[i];
|
|
4985
6182
|
const recommended = provider === suggested ? " (recommended)" : "";
|
|
4986
6183
|
const current = provider === config.providers.default ? " (current)" : "";
|
|
4987
|
-
console.log(
|
|
6184
|
+
console.log(chalk7.white(` ${i + 1}. ${getProviderLabel(provider)}${recommended}${current}`));
|
|
4988
6185
|
}
|
|
4989
6186
|
console.log("");
|
|
4990
6187
|
while (true) {
|
|
4991
|
-
const choice = await ask(
|
|
6188
|
+
const choice = await ask(chalk7.white(` Choose [1-${configured.length}] [Enter for ${getProviderLabel(suggested)}]: `));
|
|
4992
6189
|
if (!choice) {
|
|
4993
6190
|
config.providers.default = suggested;
|
|
4994
6191
|
return;
|
|
@@ -4998,7 +6195,7 @@ async function chooseDefaultProvider(config) {
|
|
|
4998
6195
|
config.providers.default = configured[num - 1];
|
|
4999
6196
|
return;
|
|
5000
6197
|
}
|
|
5001
|
-
console.log(
|
|
6198
|
+
console.log(chalk7.red(" Please choose a valid number from the list above."));
|
|
5002
6199
|
}
|
|
5003
6200
|
}
|
|
5004
6201
|
function looksLikeToken(value, minLength = 20) {
|
|
@@ -5038,18 +6235,117 @@ function validateModelName(value) {
|
|
|
5038
6235
|
if (/\s/.test(value)) return "Model name cannot contain spaces.";
|
|
5039
6236
|
return null;
|
|
5040
6237
|
}
|
|
6238
|
+
async function chooseProviderModel(providerLabel, recommendedModel, models) {
|
|
6239
|
+
const selection = await selectWithArrowKeys(
|
|
6240
|
+
`${providerLabel} Models`,
|
|
6241
|
+
[
|
|
6242
|
+
{
|
|
6243
|
+
value: "__default__",
|
|
6244
|
+
label: `Use provider default (${recommendedModel})`
|
|
6245
|
+
},
|
|
6246
|
+
...models.map((model) => ({
|
|
6247
|
+
value: model,
|
|
6248
|
+
label: model
|
|
6249
|
+
})),
|
|
6250
|
+
{
|
|
6251
|
+
value: "__custom__",
|
|
6252
|
+
label: "Enter a custom model name"
|
|
6253
|
+
}
|
|
6254
|
+
]
|
|
6255
|
+
);
|
|
6256
|
+
if (!selection || selection === "__default__") {
|
|
6257
|
+
return recommendedModel;
|
|
6258
|
+
}
|
|
6259
|
+
if (selection !== "__custom__") {
|
|
6260
|
+
return selection;
|
|
6261
|
+
}
|
|
6262
|
+
while (true) {
|
|
6263
|
+
const customModel = await ask(chalk7.white(` ${providerLabel} model [Enter or "none" for ${recommendedModel}]: `));
|
|
6264
|
+
if (!customModel || customModel.toLowerCase() === "none") {
|
|
6265
|
+
return recommendedModel;
|
|
6266
|
+
}
|
|
6267
|
+
const error = validateModelName(customModel);
|
|
6268
|
+
if (!error) {
|
|
6269
|
+
return customModel;
|
|
6270
|
+
}
|
|
6271
|
+
console.log(chalk7.red(` ${error}`));
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
async function promptApiKeyWithModelSelection(config, provider, providerLabel, prompt, isReconfig) {
|
|
6275
|
+
const existingConfig = config.providers[provider];
|
|
6276
|
+
while (true) {
|
|
6277
|
+
const value = await ask(prompt);
|
|
6278
|
+
if (!value) {
|
|
6279
|
+
if (isReconfig && existingConfig.apiKey) {
|
|
6280
|
+
return {
|
|
6281
|
+
apiKey: existingConfig.apiKey,
|
|
6282
|
+
model: existingConfig.model,
|
|
6283
|
+
skipped: true
|
|
6284
|
+
};
|
|
6285
|
+
}
|
|
6286
|
+
return { skipped: true };
|
|
6287
|
+
}
|
|
6288
|
+
const formatError = validateApiKey(provider, value);
|
|
6289
|
+
if (formatError) {
|
|
6290
|
+
console.log(chalk7.red(` ${formatError}`));
|
|
6291
|
+
continue;
|
|
6292
|
+
}
|
|
6293
|
+
console.log(chalk7.dim(` Validating ${providerLabel} and fetching models...`));
|
|
6294
|
+
try {
|
|
6295
|
+
const catalog = await fetchProviderModelCatalog(provider, {
|
|
6296
|
+
...existingConfig,
|
|
6297
|
+
apiKey: value
|
|
6298
|
+
});
|
|
6299
|
+
const model = await chooseProviderModel(
|
|
6300
|
+
providerLabel,
|
|
6301
|
+
catalog.recommendedModel,
|
|
6302
|
+
catalog.models
|
|
6303
|
+
);
|
|
6304
|
+
return { apiKey: value, model, skipped: false };
|
|
6305
|
+
} catch (error) {
|
|
6306
|
+
const message = error instanceof ProviderModelFetchError ? error.message : `Mercury could not fetch models for ${providerLabel}. Please re-enter the key.`;
|
|
6307
|
+
console.log(chalk7.red(` ${message}`));
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
async function promptOllamaLocalModelSelection(config) {
|
|
6312
|
+
const existingConfig = config.providers.ollamaLocal;
|
|
6313
|
+
while (true) {
|
|
6314
|
+
const baseUrl = await promptValidatedValue(
|
|
6315
|
+
chalk7.white(` Ollama Local base URL [${existingConfig.baseUrl}]: `),
|
|
6316
|
+
validateBaseUrl,
|
|
6317
|
+
existingConfig.baseUrl
|
|
6318
|
+
);
|
|
6319
|
+
console.log(chalk7.dim(" Fetching Ollama Local models..."));
|
|
6320
|
+
try {
|
|
6321
|
+
const catalog = await fetchProviderModelCatalog("ollamaLocal", {
|
|
6322
|
+
...existingConfig,
|
|
6323
|
+
baseUrl
|
|
6324
|
+
});
|
|
6325
|
+
const model = await chooseProviderModel(
|
|
6326
|
+
"Ollama Local",
|
|
6327
|
+
catalog.recommendedModel,
|
|
6328
|
+
catalog.models
|
|
6329
|
+
);
|
|
6330
|
+
return { baseUrl, model, skipped: false };
|
|
6331
|
+
} catch (error) {
|
|
6332
|
+
const message = error instanceof ProviderModelFetchError ? error.message : "Mercury could not fetch Ollama Local models. Please check the base URL and try again.";
|
|
6333
|
+
console.log(chalk7.red(` ${message}`));
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6336
|
+
}
|
|
5041
6337
|
async function promptValidatedValue(prompt, validator, existingValue, options) {
|
|
5042
6338
|
while (true) {
|
|
5043
6339
|
const value = await ask(prompt);
|
|
5044
6340
|
if (!value) {
|
|
5045
6341
|
if (existingValue) return existingValue;
|
|
5046
6342
|
if (options?.allowSkip) return void 0;
|
|
5047
|
-
console.log(
|
|
6343
|
+
console.log(chalk7.red(" A value is required here."));
|
|
5048
6344
|
continue;
|
|
5049
6345
|
}
|
|
5050
6346
|
const error = validator(value);
|
|
5051
6347
|
if (!error) return value;
|
|
5052
|
-
console.log(
|
|
6348
|
+
console.log(chalk7.red(` ${error}`));
|
|
5053
6349
|
}
|
|
5054
6350
|
}
|
|
5055
6351
|
function appendToEnv(key, value) {
|
|
@@ -5071,43 +6367,103 @@ function parseGithubRepo(input) {
|
|
|
5071
6367
|
if (shortMatch) return { owner: shortMatch[1], repo: shortMatch[2] };
|
|
5072
6368
|
return null;
|
|
5073
6369
|
}
|
|
6370
|
+
function formatTelegramUser(user) {
|
|
6371
|
+
const username = user.username ? ` (@${user.username})` : "";
|
|
6372
|
+
const firstName = user.firstName ? ` ${user.firstName}` : "";
|
|
6373
|
+
return `${user.userId}${username}${firstName}`;
|
|
6374
|
+
}
|
|
6375
|
+
function printTelegramAccessState(config) {
|
|
6376
|
+
const admins = config.channels.telegram.admins;
|
|
6377
|
+
const members = config.channels.telegram.members;
|
|
6378
|
+
const pending = config.channels.telegram.pending;
|
|
6379
|
+
const pendingSummary = pending.length > 0 ? pending.map((entry) => {
|
|
6380
|
+
const code = entry.pairingCode ? ` [code: ${entry.pairingCode}]` : "";
|
|
6381
|
+
return `${formatTelegramUser(entry)}${code}`;
|
|
6382
|
+
}).join(", ") : "";
|
|
6383
|
+
console.log("");
|
|
6384
|
+
console.log(` Telegram Access: ${chalk7.white(getTelegramAccessSummary(config))}`);
|
|
6385
|
+
console.log(` Admins: ${admins.length > 0 ? chalk7.green(admins.map(formatTelegramUser).join(", ")) : chalk7.dim("none")}`);
|
|
6386
|
+
console.log(` Members: ${members.length > 0 ? chalk7.green(members.map(formatTelegramUser).join(", ")) : chalk7.dim("none")}`);
|
|
6387
|
+
console.log(` Pending: ${pending.length > 0 ? chalk7.yellow(pendingSummary) : chalk7.dim("none")}`);
|
|
6388
|
+
}
|
|
6389
|
+
function restartDaemonIfRunning(message) {
|
|
6390
|
+
const daemon = getDaemonStatus();
|
|
6391
|
+
if (!daemon.running) return;
|
|
6392
|
+
if (message) {
|
|
6393
|
+
console.log(chalk7.dim(` ${message}`));
|
|
6394
|
+
}
|
|
6395
|
+
restartDaemon();
|
|
6396
|
+
}
|
|
6397
|
+
async function completeInitialTelegramPairing(config) {
|
|
6398
|
+
if (!config.channels.telegram.enabled || !config.channels.telegram.botToken || hasTelegramAdmins(config)) {
|
|
6399
|
+
return;
|
|
6400
|
+
}
|
|
6401
|
+
console.log("");
|
|
6402
|
+
console.log(chalk7.bold.white(" Telegram Pairing"));
|
|
6403
|
+
console.log(chalk7.dim(" 1. Open Telegram and message your bot."));
|
|
6404
|
+
console.log(chalk7.dim(" 2. Send /start to receive your pairing code in Telegram."));
|
|
6405
|
+
console.log(chalk7.dim(" 3. Paste that pairing code below to finish setup."));
|
|
6406
|
+
console.log("");
|
|
6407
|
+
const telegram = new TelegramChannel(config);
|
|
6408
|
+
try {
|
|
6409
|
+
await telegram.start();
|
|
6410
|
+
while (true) {
|
|
6411
|
+
const pairingCode = await ask(chalk7.white(" Telegram Pairing Code: "));
|
|
6412
|
+
if (!pairingCode) {
|
|
6413
|
+
console.log(chalk7.red(" Telegram pairing code is required to continue."));
|
|
6414
|
+
continue;
|
|
6415
|
+
}
|
|
6416
|
+
const approved = approveTelegramPendingRequestByPairingCode(config, pairingCode);
|
|
6417
|
+
if (!approved) {
|
|
6418
|
+
console.log(chalk7.red(" That pairing code is not valid yet. Send /start in Telegram, then paste the exact code here."));
|
|
6419
|
+
continue;
|
|
6420
|
+
}
|
|
6421
|
+
saveConfig(config);
|
|
6422
|
+
console.log(chalk7.green(` \u2713 Telegram paired. First admin: ${formatTelegramUser(approved)}.`));
|
|
6423
|
+
console.log("");
|
|
6424
|
+
break;
|
|
6425
|
+
}
|
|
6426
|
+
} finally {
|
|
6427
|
+
await telegram.stop();
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
5074
6430
|
async function configure(existingConfig) {
|
|
5075
6431
|
const isReconfig = !!existingConfig;
|
|
5076
6432
|
const config = existingConfig ?? loadConfig();
|
|
5077
6433
|
if (isReconfig) {
|
|
5078
6434
|
banner();
|
|
5079
|
-
console.log(
|
|
6435
|
+
console.log(chalk7.yellow(" Reconfiguring Mercury \u2014 press Enter to keep current value."));
|
|
5080
6436
|
} else {
|
|
5081
6437
|
splashScreen();
|
|
5082
|
-
console.log(
|
|
6438
|
+
console.log(chalk7.yellow(" First run detected \u2014 let's set you up."));
|
|
5083
6439
|
}
|
|
5084
6440
|
hr();
|
|
5085
6441
|
console.log("");
|
|
5086
|
-
console.log(
|
|
6442
|
+
console.log(chalk7.bold.white(" Identity"));
|
|
5087
6443
|
console.log("");
|
|
5088
6444
|
if (isReconfig) {
|
|
5089
|
-
const ownerName = await ask(
|
|
6445
|
+
const ownerName = await ask(chalk7.white(` Your name [${config.identity.owner}]: `));
|
|
5090
6446
|
if (ownerName) config.identity.owner = ownerName;
|
|
5091
|
-
const agentName = await ask(
|
|
6447
|
+
const agentName = await ask(chalk7.white(` Agent name [${config.identity.name}]: `));
|
|
5092
6448
|
if (agentName) config.identity.name = agentName;
|
|
5093
6449
|
} else {
|
|
5094
|
-
const ownerName = await ask(
|
|
6450
|
+
const ownerName = await ask(chalk7.white(" Your name: "));
|
|
5095
6451
|
if (!ownerName) {
|
|
5096
|
-
console.log(
|
|
6452
|
+
console.log(chalk7.red(" Name is required."));
|
|
5097
6453
|
process.exit(1);
|
|
5098
6454
|
}
|
|
5099
6455
|
config.identity.owner = ownerName;
|
|
5100
|
-
const agentName = await ask(
|
|
6456
|
+
const agentName = await ask(chalk7.white(` Agent name [${config.identity.name}]: `));
|
|
5101
6457
|
if (agentName) config.identity.name = agentName;
|
|
5102
6458
|
}
|
|
5103
6459
|
config.identity.creator = config.identity.creator || "Cosmic Stack";
|
|
5104
6460
|
hr();
|
|
5105
6461
|
console.log("");
|
|
5106
|
-
console.log(
|
|
6462
|
+
console.log(chalk7.bold.white(" LLM Providers"));
|
|
5107
6463
|
if (isReconfig) {
|
|
5108
|
-
console.log(
|
|
6464
|
+
console.log(chalk7.dim(" Choose which providers to configure now. Existing values are shown where available."));
|
|
5109
6465
|
} else {
|
|
5110
|
-
console.log(
|
|
6466
|
+
console.log(chalk7.dim(" Choose one or more providers. Press Enter to configure DeepSeek by default."));
|
|
5111
6467
|
}
|
|
5112
6468
|
console.log("");
|
|
5113
6469
|
while (true) {
|
|
@@ -5116,92 +6472,97 @@ async function configure(existingConfig) {
|
|
|
5116
6472
|
for (const provider of selectedProviders) {
|
|
5117
6473
|
if (provider === "deepseek") {
|
|
5118
6474
|
const mask = isReconfig && config.providers.deepseek.apiKey ? ` [${maskKey(config.providers.deepseek.apiKey)}]` : "";
|
|
5119
|
-
const
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
{
|
|
6475
|
+
const result = await promptApiKeyWithModelSelection(
|
|
6476
|
+
config,
|
|
6477
|
+
"deepseek",
|
|
6478
|
+
"DeepSeek",
|
|
6479
|
+
chalk7.white(` DeepSeek API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
6480
|
+
isReconfig
|
|
5124
6481
|
);
|
|
5125
|
-
if (
|
|
5126
|
-
config.providers.deepseek.apiKey =
|
|
6482
|
+
if (!result.skipped && result.apiKey && result.model) {
|
|
6483
|
+
config.providers.deepseek.apiKey = result.apiKey;
|
|
6484
|
+
config.providers.deepseek.model = result.model;
|
|
5127
6485
|
config.providers.deepseek.enabled = true;
|
|
5128
6486
|
}
|
|
5129
6487
|
continue;
|
|
5130
6488
|
}
|
|
5131
6489
|
if (provider === "openai") {
|
|
5132
6490
|
const mask = isReconfig && config.providers.openai.apiKey ? ` [${maskKey(config.providers.openai.apiKey)}]` : "";
|
|
5133
|
-
const
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
{
|
|
6491
|
+
const result = await promptApiKeyWithModelSelection(
|
|
6492
|
+
config,
|
|
6493
|
+
"openai",
|
|
6494
|
+
"OpenAI",
|
|
6495
|
+
chalk7.white(` OpenAI API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
6496
|
+
isReconfig
|
|
5138
6497
|
);
|
|
5139
|
-
if (
|
|
5140
|
-
config.providers.openai.apiKey =
|
|
6498
|
+
if (!result.skipped && result.apiKey && result.model) {
|
|
6499
|
+
config.providers.openai.apiKey = result.apiKey;
|
|
6500
|
+
config.providers.openai.model = result.model;
|
|
5141
6501
|
config.providers.openai.enabled = true;
|
|
5142
6502
|
}
|
|
5143
6503
|
continue;
|
|
5144
6504
|
}
|
|
5145
6505
|
if (provider === "anthropic") {
|
|
5146
6506
|
const mask = isReconfig && config.providers.anthropic.apiKey ? ` [${maskKey(config.providers.anthropic.apiKey)}]` : "";
|
|
5147
|
-
const
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
{
|
|
6507
|
+
const result = await promptApiKeyWithModelSelection(
|
|
6508
|
+
config,
|
|
6509
|
+
"anthropic",
|
|
6510
|
+
"Anthropic",
|
|
6511
|
+
chalk7.white(` Anthropic API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
6512
|
+
isReconfig
|
|
5152
6513
|
);
|
|
5153
|
-
if (
|
|
5154
|
-
config.providers.anthropic.apiKey =
|
|
6514
|
+
if (!result.skipped && result.apiKey && result.model) {
|
|
6515
|
+
config.providers.anthropic.apiKey = result.apiKey;
|
|
6516
|
+
config.providers.anthropic.model = result.model;
|
|
5155
6517
|
config.providers.anthropic.enabled = true;
|
|
5156
6518
|
}
|
|
5157
6519
|
continue;
|
|
5158
6520
|
}
|
|
5159
6521
|
if (provider === "grok") {
|
|
5160
6522
|
const mask = isReconfig && config.providers.grok.apiKey ? ` [${maskKey(config.providers.grok.apiKey)}]` : "";
|
|
5161
|
-
const
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
{
|
|
6523
|
+
const result = await promptApiKeyWithModelSelection(
|
|
6524
|
+
config,
|
|
6525
|
+
"grok",
|
|
6526
|
+
"Grok",
|
|
6527
|
+
chalk7.white(` Grok API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
6528
|
+
isReconfig
|
|
5166
6529
|
);
|
|
5167
|
-
if (
|
|
5168
|
-
config.providers.grok.apiKey =
|
|
6530
|
+
if (!result.skipped && result.apiKey && result.model) {
|
|
6531
|
+
config.providers.grok.apiKey = result.apiKey;
|
|
6532
|
+
config.providers.grok.model = result.model;
|
|
5169
6533
|
config.providers.grok.enabled = true;
|
|
5170
6534
|
}
|
|
5171
6535
|
continue;
|
|
5172
6536
|
}
|
|
5173
6537
|
if (provider === "ollamaCloud") {
|
|
5174
6538
|
const mask = isReconfig && config.providers.ollamaCloud.apiKey ? ` [${maskKey(config.providers.ollamaCloud.apiKey)}]` : "";
|
|
5175
|
-
const
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
{
|
|
6539
|
+
const result = await promptApiKeyWithModelSelection(
|
|
6540
|
+
config,
|
|
6541
|
+
"ollamaCloud",
|
|
6542
|
+
"Ollama Cloud",
|
|
6543
|
+
chalk7.white(` Ollama Cloud API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
6544
|
+
isReconfig
|
|
5180
6545
|
);
|
|
5181
|
-
if (
|
|
5182
|
-
config.providers.ollamaCloud.apiKey =
|
|
6546
|
+
if (!result.skipped && result.apiKey && result.model) {
|
|
6547
|
+
config.providers.ollamaCloud.apiKey = result.apiKey;
|
|
6548
|
+
config.providers.ollamaCloud.model = result.model;
|
|
5183
6549
|
config.providers.ollamaCloud.enabled = true;
|
|
5184
6550
|
}
|
|
5185
6551
|
continue;
|
|
5186
6552
|
}
|
|
5187
6553
|
if (provider === "ollamaLocal") {
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
config.providers.ollamaLocal.
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
chalk6.white(` Ollama Local model [${config.providers.ollamaLocal.model}]: `),
|
|
5195
|
-
validateModelName,
|
|
5196
|
-
config.providers.ollamaLocal.model
|
|
5197
|
-
);
|
|
5198
|
-
config.providers.ollamaLocal.enabled = true;
|
|
6554
|
+
const result = await promptOllamaLocalModelSelection(config);
|
|
6555
|
+
if (!result.skipped && result.baseUrl && result.model) {
|
|
6556
|
+
config.providers.ollamaLocal.baseUrl = result.baseUrl;
|
|
6557
|
+
config.providers.ollamaLocal.model = result.model;
|
|
6558
|
+
config.providers.ollamaLocal.enabled = true;
|
|
6559
|
+
}
|
|
5199
6560
|
}
|
|
5200
6561
|
}
|
|
5201
6562
|
const configuredProviders = getConfiguredProviderNames(config);
|
|
5202
6563
|
if (configuredProviders.length === 0) {
|
|
5203
|
-
console.log(
|
|
5204
|
-
console.log(
|
|
6564
|
+
console.log(chalk7.red(" You need to configure at least one LLM provider to continue."));
|
|
6565
|
+
console.log(chalk7.dim(" Let\u2019s try that step again."));
|
|
5205
6566
|
console.log("");
|
|
5206
6567
|
continue;
|
|
5207
6568
|
}
|
|
@@ -5210,78 +6571,81 @@ async function configure(existingConfig) {
|
|
|
5210
6571
|
}
|
|
5211
6572
|
hr();
|
|
5212
6573
|
console.log("");
|
|
5213
|
-
console.log(
|
|
6574
|
+
console.log(chalk7.bold.white(" Telegram (optional)"));
|
|
5214
6575
|
if (isReconfig) {
|
|
5215
|
-
console.log(
|
|
6576
|
+
console.log(chalk7.dim(' Leave empty to keep current value. Enter "none" to disable.'));
|
|
5216
6577
|
} else {
|
|
5217
|
-
console.log(
|
|
5218
|
-
console.log(
|
|
5219
|
-
console.log(
|
|
5220
|
-
console.log(
|
|
5221
|
-
console.log(
|
|
5222
|
-
console.log(
|
|
6578
|
+
console.log(chalk7.dim(" Leave empty to skip. You can add it later."));
|
|
6579
|
+
console.log(chalk7.dim(" To create a bot token:"));
|
|
6580
|
+
console.log(chalk7.dim(" 1. Open Telegram and message @BotFather"));
|
|
6581
|
+
console.log(chalk7.dim(" 2. Run /newbot and follow the prompts"));
|
|
6582
|
+
console.log(chalk7.dim(" 3. Copy the bot token BotFather gives you"));
|
|
6583
|
+
console.log(chalk7.dim(" 4. Paste that token here"));
|
|
6584
|
+
console.log(chalk7.dim(" After setup, users send /start to request access."));
|
|
6585
|
+
console.log(chalk7.dim(" The first Telegram user gets a pairing code, and you approve that code from the CLI."));
|
|
5223
6586
|
}
|
|
5224
6587
|
console.log("");
|
|
5225
6588
|
const tgMask = isReconfig && config.channels.telegram.botToken ? ` [${maskKey(config.channels.telegram.botToken)}]` : "";
|
|
5226
|
-
const telegramToken = await ask(
|
|
6589
|
+
const telegramToken = await ask(chalk7.white(` Telegram Bot Token${tgMask}: `));
|
|
5227
6590
|
if (isReconfig && telegramToken.toLowerCase() === "none") {
|
|
5228
6591
|
config.channels.telegram.enabled = false;
|
|
5229
6592
|
config.channels.telegram.botToken = "";
|
|
5230
|
-
|
|
6593
|
+
clearTelegramAccess(config);
|
|
5231
6594
|
} else if (telegramToken) {
|
|
5232
6595
|
if (telegramToken !== config.channels.telegram.botToken) {
|
|
5233
|
-
|
|
6596
|
+
clearTelegramAccess(config);
|
|
5234
6597
|
}
|
|
5235
6598
|
config.channels.telegram.botToken = telegramToken;
|
|
5236
6599
|
config.channels.telegram.enabled = true;
|
|
5237
6600
|
}
|
|
6601
|
+
await completeInitialTelegramPairing(config);
|
|
5238
6602
|
hr();
|
|
5239
6603
|
console.log("");
|
|
5240
|
-
console.log(
|
|
5241
|
-
console.log(
|
|
5242
|
-
console.log(
|
|
5243
|
-
console.log(
|
|
6604
|
+
console.log(chalk7.bold.white(" GitHub Integration (optional)"));
|
|
6605
|
+
console.log(chalk7.dim(" Connect Mercury to GitHub so it can create PRs, manage issues,"));
|
|
6606
|
+
console.log(chalk7.dim(" review code, and co-author commits on your behalf."));
|
|
6607
|
+
console.log(chalk7.dim(" Leave empty to skip. You can add it later with mercury doctor."));
|
|
5244
6608
|
console.log("");
|
|
5245
6609
|
const ghUserCurrent = isReconfig && config.github.username ? ` [${config.github.username}]` : "";
|
|
5246
|
-
const ghUsername = await ask(
|
|
6610
|
+
const ghUsername = await ask(chalk7.white(` 1. Your GitHub username${ghUserCurrent}: `));
|
|
5247
6611
|
if (ghUsername) config.github.username = ghUsername;
|
|
5248
6612
|
if (!config.github.email) {
|
|
5249
6613
|
config.github.email = "mercury@cosmicstack.org";
|
|
5250
6614
|
}
|
|
5251
6615
|
console.log("");
|
|
5252
|
-
console.log(
|
|
5253
|
-
console.log(
|
|
5254
|
-
console.log(
|
|
5255
|
-
console.log(
|
|
5256
|
-
console.log(
|
|
6616
|
+
console.log(chalk7.dim(" You need a Personal Access Token (PAT) with repo access."));
|
|
6617
|
+
console.log(chalk7.dim(" Fine-grained (recommended): github.com/settings/personal-access-tokens/new"));
|
|
6618
|
+
console.log(chalk7.dim(" \u2192 Permissions: Contents (R/W), Pull requests (R/W), Issues (R/W)"));
|
|
6619
|
+
console.log(chalk7.dim(" Classic: github.com/settings/tokens/new"));
|
|
6620
|
+
console.log(chalk7.dim(" \u2192 Scope: repo (full control)"));
|
|
5257
6621
|
const ghTokenCurrent = process.env.GITHUB_TOKEN ? ` [${maskKey(process.env.GITHUB_TOKEN)}]` : "";
|
|
5258
|
-
const ghToken = await ask(
|
|
6622
|
+
const ghToken = await ask(chalk7.white(` 2. GitHub PAT${ghTokenCurrent}: `));
|
|
5259
6623
|
if (ghToken) {
|
|
5260
6624
|
appendToEnv("GITHUB_TOKEN", ghToken);
|
|
5261
6625
|
}
|
|
5262
6626
|
if (config.github.username || process.env.GITHUB_TOKEN) {
|
|
5263
6627
|
console.log("");
|
|
5264
|
-
console.log(
|
|
5265
|
-
console.log(
|
|
5266
|
-
console.log(
|
|
5267
|
-
console.log(
|
|
6628
|
+
console.log(chalk7.dim(' Set a default repo so you can say "create an issue" without'));
|
|
6629
|
+
console.log(chalk7.dim(" specifying the repo every time. Enter owner/name or a full URL."));
|
|
6630
|
+
console.log(chalk7.dim(" Example: hotheadhacker/mercury-agent"));
|
|
6631
|
+
console.log(chalk7.dim(" Example: https://github.com/hotheadhacker/mercury-agent"));
|
|
5268
6632
|
const ghOwnerCurrent = isReconfig && config.github.defaultOwner ? ` [${config.github.defaultOwner}/${config.github.defaultRepo}]` : "";
|
|
5269
|
-
const ghRepoInput = await ask(
|
|
6633
|
+
const ghRepoInput = await ask(chalk7.white(` 3. Default repo${ghOwnerCurrent}: `));
|
|
5270
6634
|
if (ghRepoInput) {
|
|
5271
6635
|
const parsed = parseGithubRepo(ghRepoInput);
|
|
5272
6636
|
if (parsed) {
|
|
5273
6637
|
config.github.defaultOwner = parsed.owner;
|
|
5274
6638
|
config.github.defaultRepo = parsed.repo;
|
|
5275
6639
|
} else {
|
|
5276
|
-
console.log(
|
|
6640
|
+
console.log(chalk7.yellow(" Could not parse repo. Use format: owner/repo or a GitHub URL."));
|
|
5277
6641
|
}
|
|
5278
6642
|
}
|
|
5279
6643
|
}
|
|
5280
6644
|
hr();
|
|
5281
6645
|
console.log("");
|
|
5282
|
-
console.log(
|
|
6646
|
+
console.log(chalk7.bold.white(" Token Budget"));
|
|
5283
6647
|
console.log("");
|
|
5284
|
-
const budgetPrompt = isReconfig ?
|
|
6648
|
+
const budgetPrompt = isReconfig ? chalk7.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `) : chalk7.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `);
|
|
5285
6649
|
const budgetStr = await ask(budgetPrompt);
|
|
5286
6650
|
if (budgetStr) {
|
|
5287
6651
|
const budget = parseInt(budgetStr.replace(/,/g, ""), 10);
|
|
@@ -5293,14 +6657,14 @@ async function configure(existingConfig) {
|
|
|
5293
6657
|
saveConfig(config);
|
|
5294
6658
|
const home = getMercuryHome();
|
|
5295
6659
|
console.log("");
|
|
5296
|
-
console.log(
|
|
5297
|
-
console.log(
|
|
5298
|
-
console.log(
|
|
5299
|
-
console.log(
|
|
5300
|
-
console.log(
|
|
6660
|
+
console.log(chalk7.green(` \u2713 Config saved to ${home}/mercury.yaml`));
|
|
6661
|
+
console.log(chalk7.green(` \u2713 Soul files seeded in ${home}/soul/`));
|
|
6662
|
+
console.log(chalk7.green(` \u2713 Memory stored in ${home}/memory/`));
|
|
6663
|
+
console.log(chalk7.green(` \u2713 Permissions seeded in ${home}/permissions.yaml`));
|
|
6664
|
+
console.log(chalk7.green(` \u2713 Skills directory ready in ${home}/skills/`));
|
|
5301
6665
|
console.log("");
|
|
5302
|
-
console.log(
|
|
5303
|
-
console.log(
|
|
6666
|
+
console.log(chalk7.cyan(` ${config.identity.name} is ready. Run \`mercury start\` to chat.`));
|
|
6667
|
+
console.log(chalk7.dim(" mercury.cosmicstack.org"));
|
|
5304
6668
|
console.log("");
|
|
5305
6669
|
}
|
|
5306
6670
|
function autoDaemonize() {
|
|
@@ -5308,22 +6672,22 @@ function autoDaemonize() {
|
|
|
5308
6672
|
if (daemon.running) {
|
|
5309
6673
|
return;
|
|
5310
6674
|
}
|
|
5311
|
-
console.log(
|
|
6675
|
+
console.log(chalk7.dim(" Setting up background mode..."));
|
|
5312
6676
|
try {
|
|
5313
6677
|
if (!isServiceInstalled()) {
|
|
5314
6678
|
installService();
|
|
5315
6679
|
}
|
|
5316
6680
|
} catch {
|
|
5317
|
-
console.log(
|
|
6681
|
+
console.log(chalk7.dim(" Service install skipped (can run `mercury service install` later)."));
|
|
5318
6682
|
}
|
|
5319
6683
|
const ok = tryAutoDaemonize();
|
|
5320
6684
|
if (ok) {
|
|
5321
6685
|
const status = getDaemonStatus();
|
|
5322
|
-
console.log(
|
|
5323
|
-
console.log(
|
|
5324
|
-
console.log(
|
|
6686
|
+
console.log(chalk7.green(` \u2713 Mercury is running in background (PID: ${status.pid})`));
|
|
6687
|
+
console.log(chalk7.green(" \u2713 Auto-starts on login. Auto-restarts on crash."));
|
|
6688
|
+
console.log(chalk7.dim(" Use `mercury stop` to stop. `mercury restart` to restart."));
|
|
5325
6689
|
} else {
|
|
5326
|
-
console.log(
|
|
6690
|
+
console.log(chalk7.dim(" Background mode not available. Run `mercury up` to set it up."));
|
|
5327
6691
|
}
|
|
5328
6692
|
console.log("");
|
|
5329
6693
|
}
|
|
@@ -5333,7 +6697,7 @@ async function runAgent(isDaemon = false) {
|
|
|
5333
6697
|
const name = config.identity.name;
|
|
5334
6698
|
if (!isDaemon) {
|
|
5335
6699
|
banner();
|
|
5336
|
-
console.log(
|
|
6700
|
+
console.log(chalk7.white(` ${name} is waking up...`));
|
|
5337
6701
|
console.log("");
|
|
5338
6702
|
} else {
|
|
5339
6703
|
logger.info(`${name} is waking up (daemon mode)...`);
|
|
@@ -5345,19 +6709,25 @@ async function runAgent(isDaemon = false) {
|
|
|
5345
6709
|
logger.error("No LLM providers available. Run `mercury doctor` to configure providers.");
|
|
5346
6710
|
return;
|
|
5347
6711
|
}
|
|
5348
|
-
console.log(
|
|
6712
|
+
console.log(chalk7.red(" No LLM providers available. Run `mercury doctor` to configure providers."));
|
|
5349
6713
|
process.exit(1);
|
|
5350
6714
|
}
|
|
5351
6715
|
const available = providers.listAvailable();
|
|
6716
|
+
const providerLabels = available.map((provider) => getProviderLabel(provider));
|
|
6717
|
+
const providerModels = available.map((provider) => {
|
|
6718
|
+
const key = provider;
|
|
6719
|
+
return `${getProviderLabel(key)}: ${config.providers[key].model}`;
|
|
6720
|
+
});
|
|
5352
6721
|
if (!isDaemon) {
|
|
5353
|
-
console.log(
|
|
6722
|
+
console.log(chalk7.dim(` Providers: ${providerLabels.join(", ")}`));
|
|
6723
|
+
console.log(chalk7.dim(` Models: ${providerModels.join(" | ")}`));
|
|
5354
6724
|
} else {
|
|
5355
6725
|
logger.info({ providers: available }, "Providers loaded");
|
|
5356
6726
|
}
|
|
5357
6727
|
const skillLoader = new SkillLoader();
|
|
5358
6728
|
const skills = skillLoader.discover();
|
|
5359
6729
|
if (!isDaemon) {
|
|
5360
|
-
console.log(
|
|
6730
|
+
console.log(chalk7.dim(` Skills: ${skills.length > 0 ? skills.map((s) => s.name).join(", ") : "none installed"}`));
|
|
5361
6731
|
}
|
|
5362
6732
|
const scheduler = new Scheduler(config);
|
|
5363
6733
|
const identity = new Identity();
|
|
@@ -5374,22 +6744,30 @@ async function runAgent(isDaemon = false) {
|
|
|
5374
6744
|
manual: () => getManual()
|
|
5375
6745
|
});
|
|
5376
6746
|
capabilities.setSendFileHandler(async (filePath) => {
|
|
5377
|
-
const
|
|
5378
|
-
|
|
5379
|
-
|
|
6747
|
+
const { channelId, channelType } = capabilities.getChannelContext();
|
|
6748
|
+
const telegram = channels.get("telegram");
|
|
6749
|
+
if (channelType === "telegram" && telegram) {
|
|
6750
|
+
await telegram.sendFile(filePath, channelId);
|
|
6751
|
+
return;
|
|
6752
|
+
}
|
|
6753
|
+
if (config.channels.telegram.enabled && telegram && getTelegramApprovedUsers(config).length > 0) {
|
|
6754
|
+
await telegram.sendFile(filePath);
|
|
6755
|
+
return;
|
|
6756
|
+
}
|
|
6757
|
+
const cli = channels.get("cli");
|
|
6758
|
+
if (cli) {
|
|
6759
|
+
await cli.sendFile(filePath);
|
|
5380
6760
|
}
|
|
5381
6761
|
});
|
|
5382
6762
|
capabilities.setSendMessageHandler(async (content) => {
|
|
5383
6763
|
const telegram = channels.get("telegram");
|
|
5384
|
-
const pairedChatId = config.channels.telegram.pairedChatId;
|
|
5385
|
-
const pairedUserId = config.channels.telegram.pairedUserId;
|
|
5386
6764
|
if (!config.channels.telegram.enabled || !telegram) {
|
|
5387
6765
|
throw new Error("Telegram is not configured. Add a bot token in setup or run `mercury doctor`.");
|
|
5388
6766
|
}
|
|
5389
|
-
if (
|
|
5390
|
-
throw new Error("Telegram
|
|
6767
|
+
if (getTelegramApprovedUsers(config).length === 0) {
|
|
6768
|
+
throw new Error("Telegram has no approved users. Ask someone to send /start, then approve the request from Mercury.");
|
|
5391
6769
|
}
|
|
5392
|
-
await telegram.send(content
|
|
6770
|
+
await telegram.send(content);
|
|
5393
6771
|
});
|
|
5394
6772
|
if (process.env.GITHUB_TOKEN) {
|
|
5395
6773
|
setGitHubToken(process.env.GITHUB_TOKEN);
|
|
@@ -5424,17 +6802,13 @@ async function runAgent(isDaemon = false) {
|
|
|
5424
6802
|
const activeCh = channels.getActiveChannels();
|
|
5425
6803
|
const toolNames = capabilities.getToolNames();
|
|
5426
6804
|
if (!isDaemon) {
|
|
5427
|
-
console.log(chalk6.dim(` Channels: ${activeCh.join(", ")}`));
|
|
5428
|
-
console.log(chalk6.dim(` Tools: ${toolNames.join(", ")}`));
|
|
5429
|
-
console.log(chalk6.dim(` Permissions: ${getMercuryHome()}/permissions.yaml`));
|
|
5430
|
-
console.log(chalk6.dim(` Schedules: ${getMercuryHome()}/schedules.yaml`));
|
|
5431
6805
|
if (config.identity.creator) {
|
|
5432
|
-
console.log(
|
|
6806
|
+
console.log(chalk7.dim(` Creator: ${config.identity.creator}`));
|
|
5433
6807
|
}
|
|
5434
6808
|
hr();
|
|
5435
6809
|
console.log("");
|
|
5436
|
-
console.log(
|
|
5437
|
-
console.log(
|
|
6810
|
+
console.log(chalk7.green(` ${name} is live. Type a message and press Enter.`));
|
|
6811
|
+
console.log(chalk7.dim(" Ctrl+C to exit \xB7 /help for commands"));
|
|
5438
6812
|
console.log("");
|
|
5439
6813
|
} else {
|
|
5440
6814
|
logger.info({ channels: activeCh, tools: toolNames }, "Mercury is live (daemon mode)");
|
|
@@ -5442,7 +6816,7 @@ async function runAgent(isDaemon = false) {
|
|
|
5442
6816
|
const shutdown = async () => {
|
|
5443
6817
|
if (!isDaemon) {
|
|
5444
6818
|
console.log("");
|
|
5445
|
-
console.log(
|
|
6819
|
+
console.log(chalk7.dim(` ${name} is shutting down...`));
|
|
5446
6820
|
} else {
|
|
5447
6821
|
logger.info("Mercury is shutting down (daemon mode)");
|
|
5448
6822
|
}
|
|
@@ -5489,17 +6863,17 @@ program.command("up").description("Ensure Mercury is running persistently \u2014
|
|
|
5489
6863
|
const daemon = getDaemonStatus();
|
|
5490
6864
|
if (daemon.running && daemon.pid) {
|
|
5491
6865
|
console.log("");
|
|
5492
|
-
console.log(
|
|
5493
|
-
console.log(
|
|
6866
|
+
console.log(chalk7.green(` Mercury is already running (PID: ${daemon.pid})`));
|
|
6867
|
+
console.log(chalk7.dim(` Logs: ${daemon.logPath}`));
|
|
5494
6868
|
console.log("");
|
|
5495
6869
|
return;
|
|
5496
6870
|
}
|
|
5497
6871
|
if (!isServiceInstalled()) {
|
|
5498
6872
|
console.log("");
|
|
5499
|
-
console.log(
|
|
6873
|
+
console.log(chalk7.cyan(" Installing Mercury as a system service..."));
|
|
5500
6874
|
installService();
|
|
5501
6875
|
}
|
|
5502
|
-
console.log(
|
|
6876
|
+
console.log(chalk7.cyan(" Starting Mercury in background..."));
|
|
5503
6877
|
startBackground();
|
|
5504
6878
|
});
|
|
5505
6879
|
program.command("logs").description("Show recent daemon logs").action(() => {
|
|
@@ -5526,43 +6900,175 @@ program.command("status").description("Show current configuration and daemon sta
|
|
|
5526
6900
|
const skills = skillLoader.discover();
|
|
5527
6901
|
const daemon = getDaemonStatus();
|
|
5528
6902
|
banner();
|
|
5529
|
-
console.log(` Name: ${
|
|
5530
|
-
console.log(` Owner: ${
|
|
6903
|
+
console.log(` Name: ${chalk7.cyan(config.identity.name)}`);
|
|
6904
|
+
console.log(` Owner: ${chalk7.white(config.identity.owner || "(not set)")}`);
|
|
5531
6905
|
if (config.identity.creator) {
|
|
5532
|
-
console.log(` Creator: ${
|
|
5533
|
-
}
|
|
5534
|
-
console.log(` Provider: ${
|
|
5535
|
-
console.log(` Telegram: ${config.channels.telegram.enabled ?
|
|
5536
|
-
console.log(` Telegram
|
|
5537
|
-
console.log(` Skills: ${skills.length > 0 ?
|
|
5538
|
-
console.log(` Budget: ${
|
|
5539
|
-
console.log(` Setup: ${isSetupComplete() ?
|
|
5540
|
-
console.log(` Daemon: ${daemon.running ?
|
|
5541
|
-
console.log(` Home: ${
|
|
6906
|
+
console.log(` Creator: ${chalk7.white(config.identity.creator)}`);
|
|
6907
|
+
}
|
|
6908
|
+
console.log(` Provider: ${chalk7.white(getProviderLabel(config.providers.default))}`);
|
|
6909
|
+
console.log(` Telegram: ${config.channels.telegram.enabled ? chalk7.green("enabled") : chalk7.dim("disabled")}`);
|
|
6910
|
+
console.log(` Telegram Access: ${chalk7.white(getTelegramAccessSummary(config))}`);
|
|
6911
|
+
console.log(` Skills: ${skills.length > 0 ? chalk7.green(skills.map((s) => s.name).join(", ")) : chalk7.dim("none")}`);
|
|
6912
|
+
console.log(` Budget: ${chalk7.white(config.tokens.dailyBudget.toLocaleString())} tokens/day`);
|
|
6913
|
+
console.log(` Setup: ${isSetupComplete() ? chalk7.green("complete") : chalk7.red("not done")}`);
|
|
6914
|
+
console.log(` Daemon: ${daemon.running ? chalk7.green(`running (PID: ${daemon.pid})`) : chalk7.dim("not running")}`);
|
|
6915
|
+
console.log(` Home: ${chalk7.dim(home)}`);
|
|
6916
|
+
printTelegramAccessState(config);
|
|
5542
6917
|
console.log("");
|
|
5543
6918
|
});
|
|
5544
6919
|
program.command("help").description("Show capabilities and commands manual").action(() => {
|
|
5545
6920
|
console.log(getManual());
|
|
5546
6921
|
});
|
|
5547
|
-
var telegramCmd = program.command("telegram").description("Manage Telegram
|
|
5548
|
-
telegramCmd.command("
|
|
6922
|
+
var telegramCmd = program.command("telegram").description("Manage Telegram access approvals and admins");
|
|
6923
|
+
telegramCmd.command("list").description("Show approved Telegram users and pending access requests").action(() => {
|
|
5549
6924
|
const config = loadConfig();
|
|
5550
|
-
|
|
5551
|
-
|
|
6925
|
+
console.log("");
|
|
6926
|
+
printTelegramAccessState(config);
|
|
6927
|
+
console.log("");
|
|
6928
|
+
});
|
|
6929
|
+
telegramCmd.command("approve <codeOrUserId>").description("Approve a pending Telegram access request by pairing code or user ID").action((codeOrUserId) => {
|
|
6930
|
+
const config = loadConfig();
|
|
6931
|
+
const hasAdmins = hasTelegramAdmins(config);
|
|
6932
|
+
if (!hasAdmins) {
|
|
6933
|
+
const approved2 = approveTelegramPendingRequestByPairingCode(config, codeOrUserId.trim());
|
|
6934
|
+
if (!approved2) {
|
|
6935
|
+
console.log("");
|
|
6936
|
+
console.log(chalk7.red(` No pending first-time Telegram pairing found for code ${codeOrUserId}.`));
|
|
6937
|
+
console.log("");
|
|
6938
|
+
return;
|
|
6939
|
+
}
|
|
6940
|
+
saveConfig(config);
|
|
6941
|
+
console.log("");
|
|
6942
|
+
console.log(chalk7.green(` \u2713 Approved first Telegram admin ${formatTelegramUser(approved2)}.`));
|
|
6943
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
6944
|
+
console.log("");
|
|
6945
|
+
return;
|
|
6946
|
+
}
|
|
6947
|
+
const targetUserId = Number(codeOrUserId);
|
|
6948
|
+
if (isNaN(targetUserId)) {
|
|
6949
|
+
console.log("");
|
|
6950
|
+
console.log(chalk7.red(" Please provide a numeric Telegram user ID once Telegram already has an admin."));
|
|
6951
|
+
console.log("");
|
|
6952
|
+
return;
|
|
6953
|
+
}
|
|
6954
|
+
const approved = approveTelegramPendingRequest(config, targetUserId, "member");
|
|
6955
|
+
if (!approved) {
|
|
5552
6956
|
console.log("");
|
|
5553
|
-
console.log(
|
|
6957
|
+
console.log(chalk7.red(` No pending Telegram request found for user ${codeOrUserId}.`));
|
|
5554
6958
|
console.log("");
|
|
5555
6959
|
return;
|
|
5556
6960
|
}
|
|
5557
|
-
clearTelegramPairing(config);
|
|
5558
6961
|
saveConfig(config);
|
|
5559
6962
|
console.log("");
|
|
5560
|
-
console.log(
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
6963
|
+
console.log(chalk7.green(` \u2713 Approved Telegram member ${formatTelegramUser(approved)}.`));
|
|
6964
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
6965
|
+
console.log("");
|
|
6966
|
+
});
|
|
6967
|
+
telegramCmd.command("reject <userId>").description("Reject a pending Telegram access request").action((userId) => {
|
|
6968
|
+
const config = loadConfig();
|
|
6969
|
+
const targetUserId = Number(userId);
|
|
6970
|
+
if (isNaN(targetUserId)) {
|
|
6971
|
+
console.log("");
|
|
6972
|
+
console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
|
|
6973
|
+
console.log("");
|
|
6974
|
+
return;
|
|
6975
|
+
}
|
|
6976
|
+
const rejected = rejectTelegramPendingRequest(config, targetUserId);
|
|
6977
|
+
if (!rejected) {
|
|
6978
|
+
console.log("");
|
|
6979
|
+
console.log(chalk7.red(` No pending Telegram request found for user ${userId}.`));
|
|
6980
|
+
console.log("");
|
|
6981
|
+
return;
|
|
6982
|
+
}
|
|
6983
|
+
saveConfig(config);
|
|
6984
|
+
console.log("");
|
|
6985
|
+
console.log(chalk7.green(` \u2713 Rejected Telegram request for ${formatTelegramUser(rejected)}.`));
|
|
6986
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
6987
|
+
console.log("");
|
|
6988
|
+
});
|
|
6989
|
+
telegramCmd.command("remove <userId>").description("Remove an approved Telegram admin or member").action((userId) => {
|
|
6990
|
+
const config = loadConfig();
|
|
6991
|
+
const targetUserId = Number(userId);
|
|
6992
|
+
if (isNaN(targetUserId)) {
|
|
6993
|
+
console.log("");
|
|
6994
|
+
console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
|
|
6995
|
+
console.log("");
|
|
6996
|
+
return;
|
|
6997
|
+
}
|
|
6998
|
+
const removed = removeTelegramUser(config, targetUserId);
|
|
6999
|
+
if (!removed) {
|
|
7000
|
+
console.log("");
|
|
7001
|
+
console.log(chalk7.red(` No approved Telegram user found for ${userId}.`));
|
|
7002
|
+
console.log("");
|
|
7003
|
+
return;
|
|
7004
|
+
}
|
|
7005
|
+
saveConfig(config);
|
|
7006
|
+
console.log("");
|
|
7007
|
+
console.log(chalk7.green(` \u2713 Removed Telegram access for ${formatTelegramUser(removed)}.`));
|
|
7008
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
7009
|
+
console.log("");
|
|
7010
|
+
});
|
|
7011
|
+
telegramCmd.command("promote <userId>").description("Promote an approved Telegram member to admin").action((userId) => {
|
|
7012
|
+
const config = loadConfig();
|
|
7013
|
+
const targetUserId = Number(userId);
|
|
7014
|
+
if (isNaN(targetUserId)) {
|
|
7015
|
+
console.log("");
|
|
7016
|
+
console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
|
|
7017
|
+
console.log("");
|
|
7018
|
+
return;
|
|
7019
|
+
}
|
|
7020
|
+
const promoted = promoteTelegramUserToAdmin(config, targetUserId);
|
|
7021
|
+
if (!promoted) {
|
|
7022
|
+
console.log("");
|
|
7023
|
+
console.log(chalk7.red(` No Telegram member found for ${userId}.`));
|
|
7024
|
+
console.log("");
|
|
7025
|
+
return;
|
|
7026
|
+
}
|
|
7027
|
+
saveConfig(config);
|
|
7028
|
+
console.log("");
|
|
7029
|
+
console.log(chalk7.green(` \u2713 Promoted ${formatTelegramUser(promoted)} to Telegram admin.`));
|
|
7030
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
7031
|
+
console.log("");
|
|
7032
|
+
});
|
|
7033
|
+
telegramCmd.command("demote <userId>").description("Demote a Telegram admin to member").action((userId) => {
|
|
7034
|
+
const config = loadConfig();
|
|
7035
|
+
const targetUserId = Number(userId);
|
|
7036
|
+
if (isNaN(targetUserId)) {
|
|
7037
|
+
console.log("");
|
|
7038
|
+
console.log(chalk7.red(" Please provide a numeric Telegram user ID."));
|
|
7039
|
+
console.log("");
|
|
7040
|
+
return;
|
|
7041
|
+
}
|
|
7042
|
+
const demoted = demoteTelegramAdmin(config, targetUserId);
|
|
7043
|
+
if (!demoted) {
|
|
7044
|
+
console.log("");
|
|
7045
|
+
console.log(chalk7.red(" Could not demote that Telegram admin. Mercury must keep at least one admin."));
|
|
7046
|
+
console.log("");
|
|
7047
|
+
return;
|
|
7048
|
+
}
|
|
7049
|
+
saveConfig(config);
|
|
7050
|
+
console.log("");
|
|
7051
|
+
console.log(chalk7.green(` \u2713 Demoted ${formatTelegramUser(demoted)} to Telegram member.`));
|
|
7052
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
7053
|
+
console.log("");
|
|
7054
|
+
});
|
|
7055
|
+
telegramCmd.command("unpair").description("Reset all Telegram access for this Mercury instance").action(() => {
|
|
7056
|
+
const config = loadConfig();
|
|
7057
|
+
const hasAnyAccess = getTelegramApprovedUsers(config).length > 0 || getTelegramPendingRequests(config).length > 0;
|
|
7058
|
+
if (!hasAnyAccess) {
|
|
7059
|
+
console.log("");
|
|
7060
|
+
console.log(chalk7.dim(" Telegram access is already empty."));
|
|
7061
|
+
console.log("");
|
|
7062
|
+
return;
|
|
7063
|
+
}
|
|
7064
|
+
clearTelegramAccess(config);
|
|
7065
|
+
saveConfig(config);
|
|
7066
|
+
console.log("");
|
|
7067
|
+
console.log(chalk7.green(" \u2713 Telegram access reset."));
|
|
7068
|
+
restartDaemonIfRunning("Restarting the background daemon to apply the change immediately...");
|
|
7069
|
+
if (!getDaemonStatus().running) {
|
|
7070
|
+
console.log(chalk7.dim(" New private Telegram users can send /start to request access."));
|
|
7071
|
+
console.log(chalk7.dim(" The first request must be approved from the CLI with `mercury telegram approve <pairing-code>`."));
|
|
5566
7072
|
}
|
|
5567
7073
|
console.log("");
|
|
5568
7074
|
});
|