@cosmicstack/mercury-agent 0.1.0 → 0.2.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 +96 -10
- package/dist/index.js +1140 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import readline2 from "readline";
|
|
6
|
-
import
|
|
6
|
+
import chalk6 from "chalk";
|
|
7
7
|
import figlet from "figlet";
|
|
8
8
|
|
|
9
9
|
// src/utils/config.ts
|
|
@@ -67,7 +67,8 @@ function getDefaultConfig() {
|
|
|
67
67
|
enabled: getEnvBool("TELEGRAM_ENABLED", false),
|
|
68
68
|
botToken: getEnv("TELEGRAM_BOT_TOKEN", ""),
|
|
69
69
|
webhookUrl: getEnv("TELEGRAM_WEBHOOK_URL", ""),
|
|
70
|
-
allowedChatIds: getEnv("TELEGRAM_ALLOWED_CHAT_IDS", "").split(",").filter(Boolean).map(Number)
|
|
70
|
+
allowedChatIds: getEnv("TELEGRAM_ALLOWED_CHAT_IDS", "").split(",").filter(Boolean).map(Number),
|
|
71
|
+
streaming: getEnvBool("TELEGRAM_STREAMING", true)
|
|
71
72
|
}
|
|
72
73
|
},
|
|
73
74
|
memory: {
|
|
@@ -128,10 +129,6 @@ function deepMerge(target, source) {
|
|
|
128
129
|
return result;
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
// src/soul/identity.ts
|
|
132
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
133
|
-
import { join as join2 } from "path";
|
|
134
|
-
|
|
135
132
|
// src/utils/logger.ts
|
|
136
133
|
import pino from "pino";
|
|
137
134
|
var verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
@@ -145,6 +142,8 @@ var logger = pino(
|
|
|
145
142
|
);
|
|
146
143
|
|
|
147
144
|
// src/soul/identity.ts
|
|
145
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
146
|
+
import { join as join2 } from "path";
|
|
148
147
|
var DEFAULT_SOUL = `# Identity
|
|
149
148
|
|
|
150
149
|
You are {name}. You are not any underlying model \u2014 you are {name}.
|
|
@@ -679,6 +678,7 @@ var Agent = class {
|
|
|
679
678
|
this.lifecycle = new Lifecycle();
|
|
680
679
|
this.scheduler = scheduler;
|
|
681
680
|
this.capabilities = capabilities;
|
|
681
|
+
this.telegramStreaming = config.channels.telegram.streaming ?? true;
|
|
682
682
|
this.scheduler.setOnScheduledTask(async (manifest) => this.handleScheduledTask(manifest));
|
|
683
683
|
this.channels.onIncomingMessage((msg) => this.enqueueMessage(msg));
|
|
684
684
|
this.scheduler.onHeartbeat(async () => {
|
|
@@ -699,6 +699,7 @@ var Agent = class {
|
|
|
699
699
|
running = false;
|
|
700
700
|
messageQueue = [];
|
|
701
701
|
processing = false;
|
|
702
|
+
telegramStreaming;
|
|
702
703
|
enqueueMessage(msg) {
|
|
703
704
|
logger.info({ from: msg.channelType, content: msg.content.slice(0, 50) }, "Message enqueued");
|
|
704
705
|
this.messageQueue.push(msg);
|
|
@@ -758,6 +759,43 @@ var Agent = class {
|
|
|
758
759
|
this.lifecycle.transition("idle");
|
|
759
760
|
return;
|
|
760
761
|
}
|
|
762
|
+
if (trimmed === "/budget_override") {
|
|
763
|
+
await this.handleBudgetCommand("override", msg.channelType, msg.channelId);
|
|
764
|
+
this.lifecycle.transition("idle");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (trimmed === "/budget_reset") {
|
|
768
|
+
await this.handleBudgetCommand("reset", msg.channelType, msg.channelId);
|
|
769
|
+
this.lifecycle.transition("idle");
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (trimmed.startsWith("/budget_set")) {
|
|
773
|
+
const args = trimmed.slice("/budget_set".length).trim();
|
|
774
|
+
await this.handleBudgetCommand("set " + args, msg.channelType, msg.channelId);
|
|
775
|
+
this.lifecycle.transition("idle");
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (trimmed.startsWith("/stream")) {
|
|
779
|
+
const sub = trimmed.slice("/stream".length).trim().toLowerCase();
|
|
780
|
+
if (sub === "off") {
|
|
781
|
+
this.telegramStreaming = false;
|
|
782
|
+
} else if (sub === "on") {
|
|
783
|
+
this.telegramStreaming = true;
|
|
784
|
+
} else {
|
|
785
|
+
this.telegramStreaming = !this.telegramStreaming;
|
|
786
|
+
}
|
|
787
|
+
const ch = this.channels.get(msg.channelType);
|
|
788
|
+
if (ch) await ch.send(
|
|
789
|
+
this.telegramStreaming ? "Telegram streaming enabled. Responses will appear progressively." : "Telegram streaming disabled. Responses will arrive as a single message.",
|
|
790
|
+
msg.channelId
|
|
791
|
+
);
|
|
792
|
+
this.lifecycle.transition("idle");
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (await this.handleChatCommand(trimmed, msg.channelType, msg.channelId)) {
|
|
796
|
+
this.lifecycle.transition("idle");
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
761
799
|
if (this.tokenBudget.isOverBudget()) {
|
|
762
800
|
const channel2 = this.channels.getChannelForMessage(msg);
|
|
763
801
|
if (channel2 && msg.channelType !== "internal") {
|
|
@@ -815,7 +853,7 @@ You can override this:
|
|
|
815
853
|
let usedProvider = null;
|
|
816
854
|
let lastError = null;
|
|
817
855
|
let streamedText = "";
|
|
818
|
-
const canStream = msg.channelType === "cli";
|
|
856
|
+
const canStream = msg.channelType === "cli" || msg.channelType === "telegram" && this.telegramStreaming;
|
|
819
857
|
for (const provider of fallbackIterator) {
|
|
820
858
|
try {
|
|
821
859
|
logger.info({ provider: provider.name, model: provider.getModel(), steps: MAX_STEPS, stream: canStream }, "Generating agentic response");
|
|
@@ -835,7 +873,22 @@ You can override this:
|
|
|
835
873
|
}
|
|
836
874
|
}
|
|
837
875
|
});
|
|
838
|
-
|
|
876
|
+
let fullText;
|
|
877
|
+
if (msg.channelType === "telegram") {
|
|
878
|
+
const tgChannel = this.channels.get("telegram");
|
|
879
|
+
if (tgChannel && "sendStreamToChat" in tgChannel) {
|
|
880
|
+
const chatId = msg.channelId.startsWith("telegram:") ? Number(msg.channelId.split(":")[1]) : Number(msg.channelId);
|
|
881
|
+
if (!isNaN(chatId)) {
|
|
882
|
+
fullText = await tgChannel.sendStreamToChat(chatId, streamResult.textStream);
|
|
883
|
+
} else {
|
|
884
|
+
fullText = await channel.stream(streamResult.textStream, msg.channelId);
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
fullText = await channel.stream(streamResult.textStream, msg.channelId);
|
|
888
|
+
}
|
|
889
|
+
} else {
|
|
890
|
+
fullText = await channel.stream(streamResult.textStream, msg.channelId);
|
|
891
|
+
}
|
|
839
892
|
const [usage] = await Promise.all([
|
|
840
893
|
streamResult.usage
|
|
841
894
|
]);
|
|
@@ -1099,6 +1152,79 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
|
|
|
1099
1152
|
await channel.send(`Unknown budget command "${action}". Available: /budget, /budget override, /budget reset, /budget set <number>, /budget status`, channelId);
|
|
1100
1153
|
}
|
|
1101
1154
|
}
|
|
1155
|
+
async handleChatCommand(content, channelType, channelId) {
|
|
1156
|
+
const cmd = content.toLowerCase().trim();
|
|
1157
|
+
const channel = this.channels.get(channelType);
|
|
1158
|
+
if (!channel) return false;
|
|
1159
|
+
const ctx = this.capabilities.getChatCommandContext();
|
|
1160
|
+
if (!ctx) return false;
|
|
1161
|
+
if (cmd === "/help") {
|
|
1162
|
+
await channel.send(ctx.manual(), channelId);
|
|
1163
|
+
return true;
|
|
1164
|
+
}
|
|
1165
|
+
if (cmd === "/status") {
|
|
1166
|
+
const config = ctx.config();
|
|
1167
|
+
const budget = ctx.tokenBudget();
|
|
1168
|
+
const lines = [
|
|
1169
|
+
`**${config.identity.name}** \u2014 Status`,
|
|
1170
|
+
`Owner: ${config.identity.owner || "(not set)"}`,
|
|
1171
|
+
`Provider: ${config.providers.default}`,
|
|
1172
|
+
`Telegram: ${config.channels.telegram.enabled ? "enabled" : "disabled"}`,
|
|
1173
|
+
`Budget: ${budget.getStatusText()}`,
|
|
1174
|
+
`Skills: ${ctx.skillNames().length > 0 ? ctx.skillNames().join(", ") : "none"}`
|
|
1175
|
+
];
|
|
1176
|
+
await channel.send(lines.join("\n"), channelId);
|
|
1177
|
+
return true;
|
|
1178
|
+
}
|
|
1179
|
+
if (cmd === "/tools") {
|
|
1180
|
+
const tools = ctx.toolNames();
|
|
1181
|
+
const grouped = [
|
|
1182
|
+
`**${tools.length} tools loaded:**`,
|
|
1183
|
+
"",
|
|
1184
|
+
...tools.sort().map((t) => `\u2022 \`${t}\``)
|
|
1185
|
+
];
|
|
1186
|
+
await channel.send(grouped.join("\n"), channelId);
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
if (cmd === "/skills") {
|
|
1190
|
+
const names = ctx.skillNames();
|
|
1191
|
+
if (names.length === 0) {
|
|
1192
|
+
await channel.send('No skills installed. Ask me to "install skill from <url>" to add one.', channelId);
|
|
1193
|
+
} else {
|
|
1194
|
+
const lines = [
|
|
1195
|
+
`**${names.length} skill${names.length > 1 ? "s" : ""} installed:**`,
|
|
1196
|
+
"",
|
|
1197
|
+
...names.map((n) => `\u2022 ${n}`)
|
|
1198
|
+
];
|
|
1199
|
+
await channel.send(lines.join("\n"), channelId);
|
|
1200
|
+
}
|
|
1201
|
+
return true;
|
|
1202
|
+
}
|
|
1203
|
+
if (cmd === "/stream on") {
|
|
1204
|
+
this.telegramStreaming = true;
|
|
1205
|
+
await channel.send("Telegram streaming enabled. Responses will appear progressively.", channelId);
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
if (cmd === "/stream off") {
|
|
1209
|
+
this.telegramStreaming = false;
|
|
1210
|
+
await channel.send("Telegram streaming disabled. Responses will arrive as a single message.", channelId);
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
if (cmd === "/stream") {
|
|
1214
|
+
this.telegramStreaming = !this.telegramStreaming;
|
|
1215
|
+
await channel.send(
|
|
1216
|
+
this.telegramStreaming ? "Telegram streaming enabled. Responses will appear progressively." : "Telegram streaming disabled. Responses will arrive as a single message.",
|
|
1217
|
+
channelId
|
|
1218
|
+
);
|
|
1219
|
+
return true;
|
|
1220
|
+
}
|
|
1221
|
+
if (cmd === "/stream off") {
|
|
1222
|
+
this.telegramStreaming = false;
|
|
1223
|
+
await channel.send("Telegram streaming disabled. Responses will arrive as a single message.", channelId);
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1102
1228
|
};
|
|
1103
1229
|
|
|
1104
1230
|
// src/core/scheduler.ts
|
|
@@ -1607,6 +1733,10 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1607
1733
|
bot = null;
|
|
1608
1734
|
ownerChatId = null;
|
|
1609
1735
|
typingInterval = null;
|
|
1736
|
+
chatCommandContext;
|
|
1737
|
+
setChatCommandContext(ctx) {
|
|
1738
|
+
this.chatCommandContext = ctx;
|
|
1739
|
+
}
|
|
1610
1740
|
async start() {
|
|
1611
1741
|
const token = this.config.channels.telegram.botToken;
|
|
1612
1742
|
if (!token) {
|
|
@@ -1637,12 +1767,33 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1637
1767
|
});
|
|
1638
1768
|
this.bot = bot;
|
|
1639
1769
|
await bot.start({
|
|
1640
|
-
onStart: (info) => {
|
|
1770
|
+
onStart: async (info) => {
|
|
1641
1771
|
logger.info({ bot: info.username }, "Telegram bot started \u2014 long polling active");
|
|
1642
1772
|
this.ready = true;
|
|
1773
|
+
await this.registerCommands();
|
|
1643
1774
|
}
|
|
1644
1775
|
});
|
|
1645
1776
|
}
|
|
1777
|
+
async registerCommands() {
|
|
1778
|
+
if (!this.bot) return;
|
|
1779
|
+
const commands = [
|
|
1780
|
+
{ command: "help", description: "Show capabilities and commands manual" },
|
|
1781
|
+
{ command: "status", description: "Show agent config, budget, and uptime" },
|
|
1782
|
+
{ command: "tools", description: "List all loaded tools" },
|
|
1783
|
+
{ command: "skills", description: "List installed skills" },
|
|
1784
|
+
{ command: "budget", description: "Show token budget status" },
|
|
1785
|
+
{ command: "budget_override", description: "Override budget for one request" },
|
|
1786
|
+
{ command: "budget_reset", description: "Reset token usage to zero" },
|
|
1787
|
+
{ command: "budget_set", description: "Set new daily token budget" },
|
|
1788
|
+
{ command: "stream", description: "Toggle text streaming on/off" }
|
|
1789
|
+
];
|
|
1790
|
+
try {
|
|
1791
|
+
await this.bot.api.setMyCommands(commands);
|
|
1792
|
+
logger.info({ count: commands.length }, "Telegram bot commands registered");
|
|
1793
|
+
} catch (err) {
|
|
1794
|
+
logger.warn({ err: err.message }, "Failed to register Telegram commands (non-critical)");
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1646
1797
|
async stop() {
|
|
1647
1798
|
this.bot?.stop();
|
|
1648
1799
|
this.ready = false;
|
|
@@ -1739,23 +1890,64 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1739
1890
|
}
|
|
1740
1891
|
}
|
|
1741
1892
|
async sendStreamToChat(chatId, textStream) {
|
|
1742
|
-
if (!this.bot) return;
|
|
1893
|
+
if (!this.bot) return "";
|
|
1894
|
+
const STREAM_EDIT_INTERVAL = 1500;
|
|
1895
|
+
const STREAM_MIN_LENGTH = 20;
|
|
1743
1896
|
this.startTypingLoop(chatId);
|
|
1744
1897
|
try {
|
|
1745
1898
|
let full = "";
|
|
1899
|
+
let messageId = null;
|
|
1900
|
+
let lastEditTime = 0;
|
|
1901
|
+
let lastEditLength = 0;
|
|
1746
1902
|
for await (const chunk of textStream) {
|
|
1747
1903
|
full += chunk;
|
|
1904
|
+
const now = Date.now();
|
|
1905
|
+
const timeSinceLastEdit = now - lastEditTime;
|
|
1906
|
+
const charsSinceLastEdit = full.length - lastEditLength;
|
|
1907
|
+
if (messageId === null && full.length >= STREAM_MIN_LENGTH) {
|
|
1908
|
+
try {
|
|
1909
|
+
const msg = await this.bot.api.sendMessage(chatId, this.escapeHtml(full) + " \u258C", { parse_mode: "HTML" });
|
|
1910
|
+
messageId = msg.message_id;
|
|
1911
|
+
lastEditTime = now;
|
|
1912
|
+
lastEditLength = full.length;
|
|
1913
|
+
} catch {
|
|
1914
|
+
messageId = null;
|
|
1915
|
+
}
|
|
1916
|
+
} else if (messageId !== null && timeSinceLastEdit >= STREAM_EDIT_INTERVAL && charsSinceLastEdit >= 20) {
|
|
1917
|
+
try {
|
|
1918
|
+
await this.bot.api.editMessageText(chatId, messageId, this.escapeHtml(full) + " \u258C", { parse_mode: "HTML" });
|
|
1919
|
+
lastEditTime = now;
|
|
1920
|
+
lastEditLength = full.length;
|
|
1921
|
+
} catch {
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1748
1924
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1925
|
+
if (messageId !== null) {
|
|
1926
|
+
const html = mdToTelegram(full);
|
|
1927
|
+
try {
|
|
1928
|
+
await this.bot.api.editMessageText(chatId, messageId, html, { parse_mode: "HTML" });
|
|
1929
|
+
} catch {
|
|
1930
|
+
try {
|
|
1931
|
+
await this.bot.api.editMessageText(chatId, messageId, this.stripHtml(html));
|
|
1932
|
+
} catch {
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
} else {
|
|
1936
|
+
const html = mdToTelegram(full);
|
|
1937
|
+
try {
|
|
1938
|
+
await this.bot.api.sendMessage(chatId, html, { parse_mode: "HTML" });
|
|
1939
|
+
} catch {
|
|
1940
|
+
await this.bot.api.sendMessage(chatId, this.stripHtml(html));
|
|
1941
|
+
}
|
|
1754
1942
|
}
|
|
1943
|
+
return full;
|
|
1755
1944
|
} finally {
|
|
1756
1945
|
this.stopTypingLoop();
|
|
1757
1946
|
}
|
|
1758
1947
|
}
|
|
1948
|
+
escapeHtml(text) {
|
|
1949
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1950
|
+
}
|
|
1759
1951
|
splitMessage(text, maxLen) {
|
|
1760
1952
|
if (text.length <= maxLen) return [text];
|
|
1761
1953
|
const chunks = [];
|
|
@@ -3110,12 +3302,19 @@ var CapabilityRegistry = class {
|
|
|
3110
3302
|
sendFileHandler;
|
|
3111
3303
|
currentChannelId = "cli";
|
|
3112
3304
|
currentChannelType = "cli";
|
|
3305
|
+
chatCommandContext;
|
|
3113
3306
|
constructor(skillLoader, scheduler, tokenBudget) {
|
|
3114
3307
|
this.permissions = new PermissionManager();
|
|
3115
3308
|
this.skillLoader = skillLoader;
|
|
3116
3309
|
this.scheduler = scheduler;
|
|
3117
3310
|
this.tokenBudget = tokenBudget;
|
|
3118
3311
|
}
|
|
3312
|
+
setChatCommandContext(ctx) {
|
|
3313
|
+
this.chatCommandContext = ctx;
|
|
3314
|
+
}
|
|
3315
|
+
getChatCommandContext() {
|
|
3316
|
+
return this.chatCommandContext;
|
|
3317
|
+
}
|
|
3119
3318
|
setChannelContext(channelId, channelType) {
|
|
3120
3319
|
this.currentChannelId = channelId;
|
|
3121
3320
|
this.currentChannelType = channelType;
|
|
@@ -3322,31 +3521,694 @@ Describe what this skill enables Mercury to do. When invoked via the use_skill t
|
|
|
3322
3521
|
}
|
|
3323
3522
|
};
|
|
3324
3523
|
|
|
3524
|
+
// src/utils/manual.ts
|
|
3525
|
+
import chalk3 from "chalk";
|
|
3526
|
+
function getManual() {
|
|
3527
|
+
const sections = [];
|
|
3528
|
+
sections.push("");
|
|
3529
|
+
sections.push(chalk3.bold.cyan(" MERCURY \u2014 Capabilities & Commands"));
|
|
3530
|
+
sections.push(chalk3.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
3531
|
+
sections.push("");
|
|
3532
|
+
sections.push(chalk3.bold.white(" Built-in Tools"));
|
|
3533
|
+
sections.push(chalk3.dim(" Tools Mercury can use during conversations."));
|
|
3534
|
+
sections.push("");
|
|
3535
|
+
const tools = [
|
|
3536
|
+
["read_file", "Read file contents", "path (required)"],
|
|
3537
|
+
["write_file", "Write to an existing file", "path, content"],
|
|
3538
|
+
["create_file", "Create a new file (+ dirs)", "path, content"],
|
|
3539
|
+
["edit_file", "Replace specific text in a file", "path, old_string, new_string"],
|
|
3540
|
+
["list_dir", "List directory contents", "path"],
|
|
3541
|
+
["delete_file", "Delete a file", "path"],
|
|
3542
|
+
["run_command", "Execute a shell command", "command"],
|
|
3543
|
+
["approve_command", "Permanently approve a command type", 'command (e.g. "curl")'],
|
|
3544
|
+
["fetch_url", "Fetch a URL and return content", "url, format? (text/markdown)"],
|
|
3545
|
+
["git_status", "Show working tree status", "path?"],
|
|
3546
|
+
["git_diff", "Show file changes", "path?, staged?"],
|
|
3547
|
+
["git_log", "Show commit history", "count?, path?"],
|
|
3548
|
+
["git_add", "Stage files for commit", "paths (array)"],
|
|
3549
|
+
["git_commit", "Create a commit", "message"],
|
|
3550
|
+
["git_push", "Push to remote (needs approval)", "remote?, branch?"],
|
|
3551
|
+
["install_skill", "Install a skill from content or URL", "content? or url?"],
|
|
3552
|
+
["list_skills", "List installed skills", "\u2014"],
|
|
3553
|
+
["use_skill", "Invoke a skill by name", "name"],
|
|
3554
|
+
["schedule_task", "Schedule a recurring or delayed task", "cron? or delay_seconds, description, prompt? or skill_name?"],
|
|
3555
|
+
["list_scheduled_tasks", "List all scheduled tasks", "\u2014"],
|
|
3556
|
+
["cancel_scheduled_task", "Cancel a scheduled task", "id"],
|
|
3557
|
+
["budget_status", "Check token budget", "\u2014"]
|
|
3558
|
+
];
|
|
3559
|
+
for (const [name, desc, params] of tools) {
|
|
3560
|
+
sections.push(` ${chalk3.cyan(name.padEnd(24))} ${desc}`);
|
|
3561
|
+
sections.push(` ${" ".repeat(24)} ${chalk3.dim(params)}`);
|
|
3562
|
+
}
|
|
3563
|
+
sections.push("");
|
|
3564
|
+
sections.push(chalk3.bold.white(" CLI Commands"));
|
|
3565
|
+
sections.push(chalk3.dim(" Run these from your terminal (no API calls consumed)."));
|
|
3566
|
+
sections.push("");
|
|
3567
|
+
const commands = [
|
|
3568
|
+
["mercury up", "Start persistently (install service + daemon)"],
|
|
3569
|
+
["mercury", "Start the agent (same as mercury start)"],
|
|
3570
|
+
["mercury start", "Start the agent in foreground"],
|
|
3571
|
+
["mercury start -d", "Start in background (daemon mode)"],
|
|
3572
|
+
["mercury restart", "Restart a background process"],
|
|
3573
|
+
["mercury stop", "Stop a background process"],
|
|
3574
|
+
["mercury logs", "Show recent daemon logs"],
|
|
3575
|
+
["mercury doctor", "Reconfigure settings (Enter keeps current)"],
|
|
3576
|
+
["mercury setup", "Re-run the setup wizard"],
|
|
3577
|
+
["mercury status", "Show config and daemon status"],
|
|
3578
|
+
["mercury help", "Show this manual"],
|
|
3579
|
+
["mercury service install", "Install as system service (auto-start)"],
|
|
3580
|
+
["mercury service uninstall", "Uninstall system service"],
|
|
3581
|
+
["mercury service status", "Show system service status"],
|
|
3582
|
+
["mercury --verbose", "Start with debug logging on stderr"]
|
|
3583
|
+
];
|
|
3584
|
+
for (const [cmd, desc] of commands) {
|
|
3585
|
+
sections.push(` ${chalk3.white(cmd.padEnd(26))} ${desc}`);
|
|
3586
|
+
}
|
|
3587
|
+
sections.push("");
|
|
3588
|
+
sections.push(chalk3.bold.white(" In-Chat Commands"));
|
|
3589
|
+
sections.push(chalk3.dim(" Type these during a conversation (no API calls)."));
|
|
3590
|
+
sections.push("");
|
|
3591
|
+
const chat = [
|
|
3592
|
+
["/help", "Show this manual"],
|
|
3593
|
+
["/status", "Show config and budget info"],
|
|
3594
|
+
["/tools", "List currently loaded tools"],
|
|
3595
|
+
["/skills", "List installed skills"],
|
|
3596
|
+
["/stream", "Toggle text streaming on/off (Telegram)"],
|
|
3597
|
+
["/stream on", "Enable streaming (live text updates)"],
|
|
3598
|
+
["/stream off", "Disable streaming (single message)"]
|
|
3599
|
+
];
|
|
3600
|
+
for (const [cmd, desc] of chat) {
|
|
3601
|
+
sections.push(` ${chalk3.white(cmd.padEnd(16))} ${desc}`);
|
|
3602
|
+
}
|
|
3603
|
+
sections.push("");
|
|
3604
|
+
sections.push(chalk3.bold.white(" Permissions"));
|
|
3605
|
+
sections.push("");
|
|
3606
|
+
const perms = [
|
|
3607
|
+
"Commands are blocked (never run), auto-approved, or need approval.",
|
|
3608
|
+
'Say "always" when prompted to permanently approve a command type.',
|
|
3609
|
+
"Edit ~/.mercury/permissions.yaml to customize manually.",
|
|
3610
|
+
"File access is scoped \u2014 new paths need approval (y/n/always)."
|
|
3611
|
+
];
|
|
3612
|
+
for (const p of perms) {
|
|
3613
|
+
sections.push(` ${chalk3.dim("\u2022")} ${p}`);
|
|
3614
|
+
}
|
|
3615
|
+
sections.push("");
|
|
3616
|
+
sections.push(chalk3.bold.white(" Skills"));
|
|
3617
|
+
sections.push("");
|
|
3618
|
+
const skillInfo = [
|
|
3619
|
+
"Skills live in ~/.mercury/skills/<name>/SKILL.md",
|
|
3620
|
+
'Install: ask Mercury to "install skill from <url>" or paste content',
|
|
3621
|
+
'Invoke: ask Mercury to "use skill <name>"',
|
|
3622
|
+
'Schedule: "remind me daily at 9am to run daily-digest skill"'
|
|
3623
|
+
];
|
|
3624
|
+
for (const s of skillInfo) {
|
|
3625
|
+
sections.push(` ${chalk3.dim("\u2022")} ${s}`);
|
|
3626
|
+
}
|
|
3627
|
+
sections.push("");
|
|
3628
|
+
sections.push(chalk3.bold.white(" Scheduling"));
|
|
3629
|
+
sections.push("");
|
|
3630
|
+
const schedInfo = [
|
|
3631
|
+
'Recurring: "every day at 9am remind me to\u2026"',
|
|
3632
|
+
'One-shot: "remind me in 15 seconds to\u2026"',
|
|
3633
|
+
"Tasks persist to ~/.mercury/schedules.yaml"
|
|
3634
|
+
];
|
|
3635
|
+
for (const s of schedInfo) {
|
|
3636
|
+
sections.push(` ${chalk3.dim("\u2022")} ${s}`);
|
|
3637
|
+
}
|
|
3638
|
+
sections.push("");
|
|
3639
|
+
sections.push(chalk3.bold.white(" Configuration"));
|
|
3640
|
+
sections.push("");
|
|
3641
|
+
const configInfo = [
|
|
3642
|
+
["~/.mercury/mercury.yaml", "Main config (providers, channels, budget)"],
|
|
3643
|
+
["~/.mercury/permissions.yaml", "Capabilities and approval rules"],
|
|
3644
|
+
["~/.mercury/soul/*.md", "Agent personality (soul, persona, taste, heartbeat)"],
|
|
3645
|
+
["~/.mercury/skills/", "Installed skills"],
|
|
3646
|
+
["~/.mercury/schedules.yaml", "Scheduled tasks"],
|
|
3647
|
+
["~/.mercury/token-usage.json", "Daily token usage tracking"],
|
|
3648
|
+
["~/.mercury/memory/", "Short-term, long-term, episodic memory"]
|
|
3649
|
+
];
|
|
3650
|
+
for (const [path3, desc] of configInfo) {
|
|
3651
|
+
sections.push(` ${chalk3.dim(path3.padEnd(36))} ${desc}`);
|
|
3652
|
+
}
|
|
3653
|
+
sections.push("");
|
|
3654
|
+
sections.push(chalk3.dim(" mercury.cosmicstack.org"));
|
|
3655
|
+
sections.push("");
|
|
3656
|
+
return sections.join("\n");
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
// src/cli/daemon.ts
|
|
3660
|
+
import { spawn } from "child_process";
|
|
3661
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, mkdirSync as mkdirSync9, openSync } from "fs";
|
|
3662
|
+
import { join as join9 } from "path";
|
|
3663
|
+
import process2 from "process";
|
|
3664
|
+
import chalk4 from "chalk";
|
|
3665
|
+
var PID_FILE = "daemon.pid";
|
|
3666
|
+
var LOG_FILE = "daemon.log";
|
|
3667
|
+
function pidPath() {
|
|
3668
|
+
return join9(getMercuryHome(), PID_FILE);
|
|
3669
|
+
}
|
|
3670
|
+
function logPath() {
|
|
3671
|
+
return join9(getMercuryHome(), LOG_FILE);
|
|
3672
|
+
}
|
|
3673
|
+
function readPid() {
|
|
3674
|
+
const path3 = pidPath();
|
|
3675
|
+
if (!existsSync14(path3)) return null;
|
|
3676
|
+
try {
|
|
3677
|
+
const pid = parseInt(readFileSync11(path3, "utf-8").trim(), 10);
|
|
3678
|
+
if (isNaN(pid)) return null;
|
|
3679
|
+
return pid;
|
|
3680
|
+
} catch {
|
|
3681
|
+
return null;
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
function isProcessRunning(pid) {
|
|
3685
|
+
try {
|
|
3686
|
+
process2.kill(pid, 0);
|
|
3687
|
+
return true;
|
|
3688
|
+
} catch {
|
|
3689
|
+
return false;
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
function getDaemonStatus() {
|
|
3693
|
+
const pid = readPid();
|
|
3694
|
+
if (!pid) return { running: false, pid: null, logPath: logPath() };
|
|
3695
|
+
return { running: isProcessRunning(pid), pid, logPath: logPath() };
|
|
3696
|
+
}
|
|
3697
|
+
function startBackground() {
|
|
3698
|
+
const status = getDaemonStatus();
|
|
3699
|
+
if (status.running && status.pid) {
|
|
3700
|
+
console.log(chalk4.yellow(` Mercury is already running (PID: ${status.pid})`));
|
|
3701
|
+
console.log(chalk4.dim(` Use \`mercury stop\` to stop it first.`));
|
|
3702
|
+
console.log("");
|
|
3703
|
+
process2.exit(1);
|
|
3704
|
+
}
|
|
3705
|
+
if (status.pid && !status.running) {
|
|
3706
|
+
try {
|
|
3707
|
+
unlinkSync3(pidPath());
|
|
3708
|
+
} catch {
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
const home = getMercuryHome();
|
|
3712
|
+
if (!existsSync14(home)) {
|
|
3713
|
+
mkdirSync9(home, { recursive: true });
|
|
3714
|
+
}
|
|
3715
|
+
const logFile = logPath();
|
|
3716
|
+
const isWin = process2.platform === "win32";
|
|
3717
|
+
const outFd = openSync(logFile, "a");
|
|
3718
|
+
const child = spawn(process2.execPath, [process2.argv[1], "start", "--daemon"], {
|
|
3719
|
+
detached: true,
|
|
3720
|
+
stdio: ["ignore", outFd, outFd],
|
|
3721
|
+
env: { ...process2.env },
|
|
3722
|
+
windowsHide: isWin
|
|
3723
|
+
});
|
|
3724
|
+
child.unref();
|
|
3725
|
+
writeFileSync11(pidPath(), String(child.pid));
|
|
3726
|
+
console.log("");
|
|
3727
|
+
console.log(chalk4.green(` Mercury started in background (PID: ${child.pid})`));
|
|
3728
|
+
console.log(chalk4.dim(` Logs: ${logFile}`));
|
|
3729
|
+
console.log(chalk4.dim(` Use \`mercury stop\` to stop.`));
|
|
3730
|
+
console.log(chalk4.dim(` Use \`mercury logs\` to view logs.`));
|
|
3731
|
+
console.log("");
|
|
3732
|
+
}
|
|
3733
|
+
function stopDaemon() {
|
|
3734
|
+
const status = getDaemonStatus();
|
|
3735
|
+
if (!status.pid) {
|
|
3736
|
+
console.log(chalk4.yellow(" Mercury is not running as a daemon."));
|
|
3737
|
+
console.log("");
|
|
3738
|
+
process2.exit(0);
|
|
3739
|
+
}
|
|
3740
|
+
if (!status.running) {
|
|
3741
|
+
console.log(chalk4.yellow(` Stale PID file found (PID: ${status.pid} is not running). Cleaning up.`));
|
|
3742
|
+
try {
|
|
3743
|
+
unlinkSync3(pidPath());
|
|
3744
|
+
} catch {
|
|
3745
|
+
}
|
|
3746
|
+
console.log("");
|
|
3747
|
+
process2.exit(0);
|
|
3748
|
+
}
|
|
3749
|
+
try {
|
|
3750
|
+
if (process2.platform === "win32") {
|
|
3751
|
+
process2.kill(status.pid);
|
|
3752
|
+
} else {
|
|
3753
|
+
process2.kill(status.pid, "SIGTERM");
|
|
3754
|
+
}
|
|
3755
|
+
console.log(chalk4.green(` Mercury stopped (PID: ${status.pid})`));
|
|
3756
|
+
} catch {
|
|
3757
|
+
console.log(chalk4.red(` Failed to stop PID ${status.pid}. You may need to kill it manually.`));
|
|
3758
|
+
}
|
|
3759
|
+
try {
|
|
3760
|
+
unlinkSync3(pidPath());
|
|
3761
|
+
} catch {
|
|
3762
|
+
}
|
|
3763
|
+
console.log("");
|
|
3764
|
+
}
|
|
3765
|
+
function restartDaemon() {
|
|
3766
|
+
const status = getDaemonStatus();
|
|
3767
|
+
if (status.running && status.pid) {
|
|
3768
|
+
console.log(chalk4.yellow(` Stopping Mercury (PID: ${status.pid})...`));
|
|
3769
|
+
try {
|
|
3770
|
+
if (process2.platform === "win32") {
|
|
3771
|
+
process2.kill(status.pid);
|
|
3772
|
+
} else {
|
|
3773
|
+
process2.kill(status.pid, "SIGTERM");
|
|
3774
|
+
}
|
|
3775
|
+
} catch {
|
|
3776
|
+
}
|
|
3777
|
+
try {
|
|
3778
|
+
unlinkSync3(pidPath());
|
|
3779
|
+
} catch {
|
|
3780
|
+
}
|
|
3781
|
+
console.log(chalk4.green(" Mercury stopped."));
|
|
3782
|
+
} else if (status.pid) {
|
|
3783
|
+
try {
|
|
3784
|
+
unlinkSync3(pidPath());
|
|
3785
|
+
} catch {
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
console.log(chalk4.yellow(" Starting Mercury..."));
|
|
3789
|
+
startBackground();
|
|
3790
|
+
}
|
|
3791
|
+
function showLogs() {
|
|
3792
|
+
const logFile = logPath();
|
|
3793
|
+
if (!existsSync14(logFile)) {
|
|
3794
|
+
console.log(chalk4.dim(" No daemon log file found."));
|
|
3795
|
+
console.log("");
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
const content = readFileSync11(logFile, "utf-8");
|
|
3799
|
+
const lines = content.split("\n").slice(-100);
|
|
3800
|
+
console.log(lines.join("\n"));
|
|
3801
|
+
}
|
|
3802
|
+
function tryAutoDaemonize() {
|
|
3803
|
+
try {
|
|
3804
|
+
const status = getDaemonStatus();
|
|
3805
|
+
if (status.running && status.pid) {
|
|
3806
|
+
return true;
|
|
3807
|
+
}
|
|
3808
|
+
if (status.pid && !status.running) {
|
|
3809
|
+
try {
|
|
3810
|
+
unlinkSync3(pidPath());
|
|
3811
|
+
} catch {
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
const home = getMercuryHome();
|
|
3815
|
+
if (!existsSync14(home)) {
|
|
3816
|
+
mkdirSync9(home, { recursive: true });
|
|
3817
|
+
}
|
|
3818
|
+
const logFile = logPath();
|
|
3819
|
+
const isWin = process2.platform === "win32";
|
|
3820
|
+
const outFd = openSync(logFile, "a");
|
|
3821
|
+
const child = spawn(process2.execPath, [process2.argv[1], "start", "--daemon"], {
|
|
3822
|
+
detached: true,
|
|
3823
|
+
stdio: ["ignore", outFd, outFd],
|
|
3824
|
+
env: { ...process2.env },
|
|
3825
|
+
windowsHide: isWin
|
|
3826
|
+
});
|
|
3827
|
+
child.unref();
|
|
3828
|
+
if (!child.pid) {
|
|
3829
|
+
return false;
|
|
3830
|
+
}
|
|
3831
|
+
writeFileSync11(pidPath(), String(child.pid));
|
|
3832
|
+
return true;
|
|
3833
|
+
} catch {
|
|
3834
|
+
return false;
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
// src/cli/service.ts
|
|
3839
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4 } from "fs";
|
|
3840
|
+
import { join as join10 } from "path";
|
|
3841
|
+
import { homedir as homedir3 } from "os";
|
|
3842
|
+
import chalk5 from "chalk";
|
|
3843
|
+
import { execSync as execSync8 } from "child_process";
|
|
3844
|
+
var SERVICE_DESC = "Mercury \u2014 Soul-Driven AI Agent";
|
|
3845
|
+
var WIN_TASK_NAME = "MercuryAgent";
|
|
3846
|
+
function isServiceInstalled() {
|
|
3847
|
+
const platform = process.platform;
|
|
3848
|
+
if (platform === "darwin") {
|
|
3849
|
+
return existsSync15(join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist"));
|
|
3850
|
+
} else if (platform === "linux") {
|
|
3851
|
+
return existsSync15(join10(homedir3(), ".config", "systemd", "user", "mercury.service"));
|
|
3852
|
+
} else if (platform === "win32") {
|
|
3853
|
+
try {
|
|
3854
|
+
execSync8(`schtasks /query /tn "${WIN_TASK_NAME}"`, { stdio: "pipe", shell: "cmd.exe" });
|
|
3855
|
+
return true;
|
|
3856
|
+
} catch {
|
|
3857
|
+
return false;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
return false;
|
|
3861
|
+
}
|
|
3862
|
+
function getNodeBinPath() {
|
|
3863
|
+
return process.execPath;
|
|
3864
|
+
}
|
|
3865
|
+
function getDistPath() {
|
|
3866
|
+
return join10(process.argv[1] || "/usr/local/bin/mercury", "..", "..", "lib", "node_modules", "@cosmicstack", "mercury-agent", "dist", "index.js");
|
|
3867
|
+
}
|
|
3868
|
+
function installService() {
|
|
3869
|
+
const platform = process.platform;
|
|
3870
|
+
if (platform === "darwin") {
|
|
3871
|
+
installMac();
|
|
3872
|
+
} else if (platform === "linux") {
|
|
3873
|
+
installLinux();
|
|
3874
|
+
} else if (platform === "win32") {
|
|
3875
|
+
installWindows();
|
|
3876
|
+
} else {
|
|
3877
|
+
console.log(chalk5.red(` Unsupported platform: ${platform}`));
|
|
3878
|
+
process.exit(1);
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
function uninstallService() {
|
|
3882
|
+
const platform = process.platform;
|
|
3883
|
+
if (platform === "darwin") {
|
|
3884
|
+
uninstallMac();
|
|
3885
|
+
} else if (platform === "linux") {
|
|
3886
|
+
uninstallLinux();
|
|
3887
|
+
} else if (platform === "win32") {
|
|
3888
|
+
uninstallWindows();
|
|
3889
|
+
} else {
|
|
3890
|
+
console.log(chalk5.red(` Unsupported platform: ${platform}`));
|
|
3891
|
+
process.exit(1);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
function showServiceStatus() {
|
|
3895
|
+
const platform = process.platform;
|
|
3896
|
+
if (platform === "darwin") {
|
|
3897
|
+
showMacStatus();
|
|
3898
|
+
} else if (platform === "linux") {
|
|
3899
|
+
showLinuxStatus();
|
|
3900
|
+
} else if (platform === "win32") {
|
|
3901
|
+
showWindowsStatus();
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
function installMac() {
|
|
3905
|
+
const plistDir = join10(homedir3(), "Library", "LaunchAgents");
|
|
3906
|
+
const plistPath = join10(plistDir, "com.cosmicstack.mercury.plist");
|
|
3907
|
+
if (!existsSync15(plistDir)) {
|
|
3908
|
+
mkdirSync10(plistDir, { recursive: true });
|
|
3909
|
+
}
|
|
3910
|
+
const nodeBin = getNodeBinPath();
|
|
3911
|
+
const scriptPath = getDistPath();
|
|
3912
|
+
const home = getMercuryHome();
|
|
3913
|
+
const logPath2 = join10(home, "daemon.log");
|
|
3914
|
+
const errPath = join10(home, "daemon-error.log");
|
|
3915
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3916
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3917
|
+
<plist version="1.0">
|
|
3918
|
+
<dict>
|
|
3919
|
+
<key>Label</key>
|
|
3920
|
+
<string>com.cosmicstack.mercury</string>
|
|
3921
|
+
<key>ProgramArguments</key>
|
|
3922
|
+
<array>
|
|
3923
|
+
<string>${nodeBin}</string>
|
|
3924
|
+
<string>${scriptPath}</string>
|
|
3925
|
+
<string>start</string>
|
|
3926
|
+
<string>--daemon</string>
|
|
3927
|
+
</array>
|
|
3928
|
+
<key>RunAtLoad</key>
|
|
3929
|
+
<true/>
|
|
3930
|
+
<key>KeepAlive</key>
|
|
3931
|
+
<dict>
|
|
3932
|
+
<key>SuccessfulExit</key>
|
|
3933
|
+
<false/>
|
|
3934
|
+
</dict>
|
|
3935
|
+
<key>StandardOutPath</key>
|
|
3936
|
+
<string>${logPath2}</string>
|
|
3937
|
+
<key>StandardErrorPath</key>
|
|
3938
|
+
<string>${errPath}</string>
|
|
3939
|
+
<key>EnvironmentVariables</key>
|
|
3940
|
+
<dict>
|
|
3941
|
+
<key>PATH</key>
|
|
3942
|
+
<string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
|
|
3943
|
+
<key>HOME</key>
|
|
3944
|
+
<string>${homedir3()}</string>
|
|
3945
|
+
</dict>
|
|
3946
|
+
<key>WorkingDirectory</key>
|
|
3947
|
+
<string>${homedir3()}</string>
|
|
3948
|
+
</dict>
|
|
3949
|
+
</plist>`;
|
|
3950
|
+
writeFileSync12(plistPath, plist, "utf-8");
|
|
3951
|
+
try {
|
|
3952
|
+
execSync8(`launchctl load ${plistPath}`, { stdio: "inherit" });
|
|
3953
|
+
} catch {
|
|
3954
|
+
console.log(chalk5.yellow(" launchctl load failed. Try running:"));
|
|
3955
|
+
console.log(chalk5.dim(` launchctl load ${plistPath}`));
|
|
3956
|
+
}
|
|
3957
|
+
console.log("");
|
|
3958
|
+
console.log(chalk5.green(" Mercury service installed (macOS LaunchAgent)"));
|
|
3959
|
+
console.log(chalk5.dim(` Plist: ${plistPath}`));
|
|
3960
|
+
console.log(chalk5.dim(` Logs: ${logPath2}`));
|
|
3961
|
+
console.log(chalk5.dim(" Auto-starts on login. Auto-restarts on crash."));
|
|
3962
|
+
console.log("");
|
|
3963
|
+
console.log(chalk5.dim(" Uninstall: mercury service uninstall"));
|
|
3964
|
+
console.log("");
|
|
3965
|
+
}
|
|
3966
|
+
function uninstallMac() {
|
|
3967
|
+
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
3968
|
+
if (!existsSync15(plistPath)) {
|
|
3969
|
+
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
3970
|
+
console.log("");
|
|
3971
|
+
process.exit(0);
|
|
3972
|
+
}
|
|
3973
|
+
try {
|
|
3974
|
+
execSync8(`launchctl unload ${plistPath}`, { stdio: "inherit" });
|
|
3975
|
+
} catch {
|
|
3976
|
+
}
|
|
3977
|
+
try {
|
|
3978
|
+
unlinkSync4(plistPath);
|
|
3979
|
+
} catch {
|
|
3980
|
+
console.log(chalk5.yellow(" Failed to remove plist file. Remove manually:"));
|
|
3981
|
+
console.log(chalk5.dim(` rm ${plistPath}`));
|
|
3982
|
+
}
|
|
3983
|
+
console.log("");
|
|
3984
|
+
console.log(chalk5.green(" Mercury service uninstalled"));
|
|
3985
|
+
console.log("");
|
|
3986
|
+
}
|
|
3987
|
+
function showMacStatus() {
|
|
3988
|
+
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
3989
|
+
if (!existsSync15(plistPath)) {
|
|
3990
|
+
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
3991
|
+
console.log(chalk5.dim(" Run `mercury service install` to set it up."));
|
|
3992
|
+
console.log("");
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3995
|
+
try {
|
|
3996
|
+
const output = execSync8("launchctl list | grep com.cosmicstack.mercury", { encoding: "utf-8" }).trim();
|
|
3997
|
+
console.log(` ${chalk5.green("Service installed and loaded")}`);
|
|
3998
|
+
console.log(chalk5.dim(` ${output}`));
|
|
3999
|
+
} catch {
|
|
4000
|
+
console.log(` ${chalk5.yellow("Service installed but not loaded")}`);
|
|
4001
|
+
console.log(chalk5.dim(` Plist: ${plistPath}`));
|
|
4002
|
+
}
|
|
4003
|
+
console.log("");
|
|
4004
|
+
}
|
|
4005
|
+
function installLinux() {
|
|
4006
|
+
const systemdDir = join10(homedir3(), ".config", "systemd", "user");
|
|
4007
|
+
if (!existsSync15(systemdDir)) {
|
|
4008
|
+
mkdirSync10(systemdDir, { recursive: true });
|
|
4009
|
+
}
|
|
4010
|
+
const servicePath = join10(systemdDir, "mercury.service");
|
|
4011
|
+
const nodeBin = getNodeBinPath();
|
|
4012
|
+
const scriptPath = getDistPath();
|
|
4013
|
+
const home = getMercuryHome();
|
|
4014
|
+
const service = `[Unit]
|
|
4015
|
+
Description=${SERVICE_DESC}
|
|
4016
|
+
After=network.target
|
|
4017
|
+
|
|
4018
|
+
[Service]
|
|
4019
|
+
Type=simple
|
|
4020
|
+
ExecStart=${nodeBin} ${scriptPath} start --daemon
|
|
4021
|
+
Restart=on-failure
|
|
4022
|
+
RestartSec=5
|
|
4023
|
+
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
4024
|
+
Environment=HOME=${homedir3()}
|
|
4025
|
+
WorkingDirectory=${homedir3()}
|
|
4026
|
+
StandardOutput=append:${join10(home, "daemon.log")}
|
|
4027
|
+
StandardError=append:${join10(home, "daemon-error.log")}
|
|
4028
|
+
|
|
4029
|
+
[Install]
|
|
4030
|
+
WantedBy=default.target`;
|
|
4031
|
+
writeFileSync12(servicePath, service, "utf-8");
|
|
4032
|
+
try {
|
|
4033
|
+
execSync8("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
4034
|
+
execSync8("systemctl --user enable mercury.service", { stdio: "inherit" });
|
|
4035
|
+
execSync8("systemctl --user start mercury.service", { stdio: "inherit" });
|
|
4036
|
+
} catch (err) {
|
|
4037
|
+
console.log(chalk5.yellow(" systemd commands failed. Try running manually:"));
|
|
4038
|
+
console.log(chalk5.dim(" systemctl --user daemon-reload"));
|
|
4039
|
+
console.log(chalk5.dim(" systemctl --user enable mercury.service"));
|
|
4040
|
+
console.log(chalk5.dim(" systemctl --user start mercury.service"));
|
|
4041
|
+
}
|
|
4042
|
+
try {
|
|
4043
|
+
execSync8(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
|
|
4044
|
+
} catch {
|
|
4045
|
+
console.log(chalk5.yellow(" Enable linger failed (needed for boot-without-login). Try:"));
|
|
4046
|
+
console.log(chalk5.dim(` sudo loginctl enable-linger ${process.env.USER || "$USER"}`));
|
|
4047
|
+
}
|
|
4048
|
+
console.log("");
|
|
4049
|
+
console.log(chalk5.green(" Mercury service installed (systemd --user)"));
|
|
4050
|
+
console.log(chalk5.dim(` Service: ${servicePath}`));
|
|
4051
|
+
console.log(chalk5.dim(` Logs: ${join10(home, "daemon.log")}`));
|
|
4052
|
+
console.log(chalk5.dim(" Auto-starts on login. Auto-restarts on crash (5s delay)."));
|
|
4053
|
+
console.log("");
|
|
4054
|
+
console.log(chalk5.dim(" Uninstall: mercury service uninstall"));
|
|
4055
|
+
console.log("");
|
|
4056
|
+
}
|
|
4057
|
+
function uninstallLinux() {
|
|
4058
|
+
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4059
|
+
if (!existsSync15(servicePath)) {
|
|
4060
|
+
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4061
|
+
console.log("");
|
|
4062
|
+
process.exit(0);
|
|
4063
|
+
}
|
|
4064
|
+
try {
|
|
4065
|
+
execSync8("systemctl --user stop mercury.service", { stdio: "inherit" });
|
|
4066
|
+
execSync8("systemctl --user disable mercury.service", { stdio: "inherit" });
|
|
4067
|
+
} catch {
|
|
4068
|
+
}
|
|
4069
|
+
try {
|
|
4070
|
+
unlinkSync4(servicePath);
|
|
4071
|
+
} catch {
|
|
4072
|
+
console.log(chalk5.yellow(" Failed to remove service file. Remove manually:"));
|
|
4073
|
+
console.log(chalk5.dim(` rm ${servicePath}`));
|
|
4074
|
+
}
|
|
4075
|
+
try {
|
|
4076
|
+
execSync8("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
4077
|
+
} catch {
|
|
4078
|
+
}
|
|
4079
|
+
console.log("");
|
|
4080
|
+
console.log(chalk5.green(" Mercury service uninstalled"));
|
|
4081
|
+
console.log("");
|
|
4082
|
+
}
|
|
4083
|
+
function showLinuxStatus() {
|
|
4084
|
+
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4085
|
+
if (!existsSync15(servicePath)) {
|
|
4086
|
+
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4087
|
+
console.log(chalk5.dim(" Run `mercury service install` to set it up."));
|
|
4088
|
+
console.log("");
|
|
4089
|
+
return;
|
|
4090
|
+
}
|
|
4091
|
+
try {
|
|
4092
|
+
const output = execSync8("systemctl --user status mercury.service", { encoding: "utf-8" }).trim();
|
|
4093
|
+
console.log(output);
|
|
4094
|
+
} catch (err) {
|
|
4095
|
+
console.log(chalk5.yellow(" Could not get service status:"));
|
|
4096
|
+
console.log(chalk5.dim(` ${err.message || err}`));
|
|
4097
|
+
}
|
|
4098
|
+
console.log("");
|
|
4099
|
+
}
|
|
4100
|
+
function installWindows() {
|
|
4101
|
+
const nodeBin = getNodeBinPath();
|
|
4102
|
+
const scriptPath = getDistPath();
|
|
4103
|
+
const home = getMercuryHome();
|
|
4104
|
+
const logPath2 = join10(home, "daemon.log");
|
|
4105
|
+
const cmd = `"${nodeBin}" "${scriptPath}" start --daemon`;
|
|
4106
|
+
try {
|
|
4107
|
+
execSync8(
|
|
4108
|
+
`schtasks /create /tn "${WIN_TASK_NAME}" /tr "${cmd}" /sc onlogon /rl limited /f`,
|
|
4109
|
+
{ stdio: "inherit", shell: "cmd.exe" }
|
|
4110
|
+
);
|
|
4111
|
+
} catch {
|
|
4112
|
+
console.log(chalk5.yellow(" schtasks create failed. Try running from an Administrator cmd:"));
|
|
4113
|
+
console.log(chalk5.dim(` schtasks /create /tn "${WIN_TASK_NAME}" /tr "${cmd}" /sc onlogon /rl limited /f`));
|
|
4114
|
+
}
|
|
4115
|
+
try {
|
|
4116
|
+
execSync8(`schtasks /run /tn "${WIN_TASK_NAME}"`, { stdio: "inherit", shell: "cmd.exe" });
|
|
4117
|
+
} catch {
|
|
4118
|
+
console.log(chalk5.yellow(" Task created but failed to start immediately. It will start on next login."));
|
|
4119
|
+
}
|
|
4120
|
+
console.log("");
|
|
4121
|
+
console.log(chalk5.green(" Mercury service installed (Windows Task Scheduler)"));
|
|
4122
|
+
console.log(chalk5.dim(` Task: ${WIN_TASK_NAME}`));
|
|
4123
|
+
console.log(chalk5.dim(` Trigger: on logon`));
|
|
4124
|
+
console.log(chalk5.dim(` Logs: ${logPath2}`));
|
|
4125
|
+
console.log(chalk5.dim(" Auto-starts on login. Use --daemon flag for crash recovery."));
|
|
4126
|
+
console.log("");
|
|
4127
|
+
console.log(chalk5.dim(" Uninstall: mercury service uninstall"));
|
|
4128
|
+
console.log("");
|
|
4129
|
+
}
|
|
4130
|
+
function uninstallWindows() {
|
|
4131
|
+
try {
|
|
4132
|
+
execSync8(`schtasks /delete /tn "${WIN_TASK_NAME}" /f`, { stdio: "inherit", shell: "cmd.exe" });
|
|
4133
|
+
console.log("");
|
|
4134
|
+
console.log(chalk5.green(" Mercury service uninstalled"));
|
|
4135
|
+
console.log("");
|
|
4136
|
+
} catch {
|
|
4137
|
+
console.log(chalk5.yellow(" Task not found or failed to delete. Remove manually:"));
|
|
4138
|
+
console.log(chalk5.dim(` schtasks /delete /tn "${WIN_TASK_NAME}" /f`));
|
|
4139
|
+
console.log("");
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
function showWindowsStatus() {
|
|
4143
|
+
try {
|
|
4144
|
+
const output = execSync8(`schtasks /query /tn "${WIN_TASK_NAME}" /fo list`, {
|
|
4145
|
+
encoding: "utf-8",
|
|
4146
|
+
shell: "cmd.exe"
|
|
4147
|
+
}).trim();
|
|
4148
|
+
console.log(output);
|
|
4149
|
+
console.log("");
|
|
4150
|
+
} catch {
|
|
4151
|
+
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4152
|
+
console.log(chalk5.dim(" Run `mercury service install` to set it up."));
|
|
4153
|
+
console.log("");
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
// src/cli/watchdog.ts
|
|
4158
|
+
var MAX_RESTARTS = 10;
|
|
4159
|
+
var RESTART_WINDOW_MS = 6e4;
|
|
4160
|
+
var BASE_DELAY_MS = 1e3;
|
|
4161
|
+
async function runWithWatchdog(agentFn) {
|
|
4162
|
+
const restarts = [];
|
|
4163
|
+
async function attempt() {
|
|
4164
|
+
try {
|
|
4165
|
+
await agentFn();
|
|
4166
|
+
} catch (err) {
|
|
4167
|
+
const now = Date.now();
|
|
4168
|
+
restarts.push(now);
|
|
4169
|
+
const recentRestarts = restarts.filter((t) => now - t < RESTART_WINDOW_MS);
|
|
4170
|
+
const restartCount = recentRestarts.length;
|
|
4171
|
+
if (restartCount >= MAX_RESTARTS) {
|
|
4172
|
+
logger.error({ restartCount }, "Max restarts exceeded within 60s. Exiting.");
|
|
4173
|
+
process.exit(1);
|
|
4174
|
+
}
|
|
4175
|
+
const delay = BASE_DELAY_MS * Math.pow(1.25, restartCount);
|
|
4176
|
+
logger.warn({ err, restartCount, delay }, "Crash detected. Restarting with backoff...");
|
|
4177
|
+
await sleep(delay);
|
|
4178
|
+
await attempt();
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
await attempt();
|
|
4182
|
+
}
|
|
4183
|
+
function sleep(ms) {
|
|
4184
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
4185
|
+
}
|
|
4186
|
+
|
|
3325
4187
|
// src/index.ts
|
|
3326
4188
|
function hr() {
|
|
3327
|
-
console.log(
|
|
4189
|
+
console.log(chalk6.dim("\u2500".repeat(50)));
|
|
3328
4190
|
}
|
|
3329
4191
|
function banner() {
|
|
3330
4192
|
console.log("");
|
|
3331
4193
|
const art = figlet.textSync("MERCURY", { font: "Slant", horizontalLayout: "default" });
|
|
3332
4194
|
for (const line of art.split("\n")) {
|
|
3333
|
-
if (line.trim()) console.log(
|
|
4195
|
+
if (line.trim()) console.log(chalk6.bold.cyan(` ${line}`));
|
|
3334
4196
|
}
|
|
3335
4197
|
console.log("");
|
|
3336
|
-
console.log(
|
|
3337
|
-
console.log(
|
|
4198
|
+
console.log(chalk6.white(" an AI agent for personal tasks"));
|
|
4199
|
+
console.log(chalk6.dim(" v0.2.0 \xB7 by Cosmic Stack \xB7 mercury.cosmicstack.org"));
|
|
3338
4200
|
console.log("");
|
|
3339
4201
|
}
|
|
3340
4202
|
function splashScreen() {
|
|
3341
4203
|
console.log("");
|
|
3342
4204
|
const art = figlet.textSync("MERCURY", { font: "Slant", horizontalLayout: "default" });
|
|
3343
4205
|
for (const line of art.split("\n")) {
|
|
3344
|
-
if (line.trim()) console.log(
|
|
4206
|
+
if (line.trim()) console.log(chalk6.bold.cyan(` ${line}`));
|
|
3345
4207
|
}
|
|
3346
4208
|
console.log("");
|
|
3347
|
-
console.log(
|
|
3348
|
-
console.log(
|
|
3349
|
-
console.log(
|
|
4209
|
+
console.log(chalk6.dim(" an AI agent for personal tasks"));
|
|
4210
|
+
console.log(chalk6.cyan(" by Cosmic Stack"));
|
|
4211
|
+
console.log(chalk6.dim(" mercury.cosmicstack.org"));
|
|
3350
4212
|
console.log("");
|
|
3351
4213
|
}
|
|
3352
4214
|
async function ask(prompt) {
|
|
@@ -3358,81 +4220,189 @@ async function ask(prompt) {
|
|
|
3358
4220
|
});
|
|
3359
4221
|
});
|
|
3360
4222
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
4223
|
+
function maskKey(key) {
|
|
4224
|
+
if (!key) return "";
|
|
4225
|
+
if (key.length <= 8) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
4226
|
+
return key.slice(0, 4) + "\u2022\u2022\u2022\u2022" + key.slice(-4);
|
|
4227
|
+
}
|
|
4228
|
+
async function configure(existingConfig) {
|
|
4229
|
+
const isReconfig = !!existingConfig;
|
|
4230
|
+
const config = existingConfig ?? loadConfig();
|
|
4231
|
+
if (isReconfig) {
|
|
4232
|
+
banner();
|
|
4233
|
+
console.log(chalk6.yellow(" Reconfiguring Mercury \u2014 press Enter to keep current value."));
|
|
4234
|
+
} else {
|
|
4235
|
+
splashScreen();
|
|
4236
|
+
console.log(chalk6.yellow(" First run detected \u2014 let's set you up."));
|
|
4237
|
+
}
|
|
3364
4238
|
hr();
|
|
3365
4239
|
console.log("");
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
if (
|
|
3369
|
-
|
|
3370
|
-
|
|
4240
|
+
console.log(chalk6.bold.white(" Identity"));
|
|
4241
|
+
console.log("");
|
|
4242
|
+
if (isReconfig) {
|
|
4243
|
+
const ownerName = await ask(chalk6.white(` Your name [${config.identity.owner}]: `));
|
|
4244
|
+
if (ownerName) config.identity.owner = ownerName;
|
|
4245
|
+
const agentName = await ask(chalk6.white(` Agent name [${config.identity.name}]: `));
|
|
4246
|
+
if (agentName) config.identity.name = agentName;
|
|
4247
|
+
} else {
|
|
4248
|
+
const ownerName = await ask(chalk6.white(" Your name: "));
|
|
4249
|
+
if (!ownerName) {
|
|
4250
|
+
console.log(chalk6.red(" Name is required."));
|
|
4251
|
+
process.exit(1);
|
|
4252
|
+
}
|
|
4253
|
+
config.identity.owner = ownerName;
|
|
4254
|
+
const agentName = await ask(chalk6.white(` Agent name [${config.identity.name}]: `));
|
|
4255
|
+
if (agentName) config.identity.name = agentName;
|
|
3371
4256
|
}
|
|
3372
|
-
config.identity.
|
|
3373
|
-
const agentName = await ask(chalk3.white(` Agent name [${config.identity.name}]: `));
|
|
3374
|
-
if (agentName) config.identity.name = agentName;
|
|
3375
|
-
config.identity.creator = "Cosmic Stack";
|
|
4257
|
+
config.identity.creator = config.identity.creator || "Cosmic Stack";
|
|
3376
4258
|
hr();
|
|
3377
4259
|
console.log("");
|
|
3378
|
-
console.log(
|
|
3379
|
-
|
|
4260
|
+
console.log(chalk6.bold.white(" LLM Providers"));
|
|
4261
|
+
if (isReconfig) {
|
|
4262
|
+
console.log(chalk6.dim(" Current keys shown masked. Enter new value to change, Enter to keep."));
|
|
4263
|
+
} else {
|
|
4264
|
+
console.log(chalk6.dim(" At least one API key is required."));
|
|
4265
|
+
}
|
|
3380
4266
|
console.log("");
|
|
3381
|
-
const
|
|
4267
|
+
const dsMask = isReconfig && config.providers.deepseek.apiKey ? ` [${maskKey(config.providers.deepseek.apiKey)}]` : "";
|
|
4268
|
+
const deepseekKey = await ask(chalk6.white(` DeepSeek API key${dsMask}: `));
|
|
3382
4269
|
if (deepseekKey) {
|
|
3383
4270
|
config.providers.deepseek.apiKey = deepseekKey;
|
|
3384
|
-
config.providers.default = "deepseek";
|
|
3385
4271
|
}
|
|
3386
|
-
const
|
|
4272
|
+
const oaiMask = isReconfig && config.providers.openai.apiKey ? ` [${maskKey(config.providers.openai.apiKey)}]` : " (Enter to skip)";
|
|
4273
|
+
const openaiKey = await ask(chalk6.white(` OpenAI API key${oaiMask}: `));
|
|
3387
4274
|
if (openaiKey) config.providers.openai.apiKey = openaiKey;
|
|
3388
|
-
const
|
|
4275
|
+
const antMask = isReconfig && config.providers.anthropic.apiKey ? ` [${maskKey(config.providers.anthropic.apiKey)}]` : " (Enter to skip)";
|
|
4276
|
+
const anthropicKey = await ask(chalk6.white(` Anthropic API key${antMask}: `));
|
|
3389
4277
|
if (anthropicKey) config.providers.anthropic.apiKey = anthropicKey;
|
|
3390
|
-
|
|
3391
|
-
|
|
4278
|
+
const hasKey = config.providers.deepseek.apiKey || config.providers.openai.apiKey || config.providers.anthropic.apiKey;
|
|
4279
|
+
if (!hasKey) {
|
|
4280
|
+
console.log(chalk6.red("\n At least one LLM API key is required."));
|
|
3392
4281
|
process.exit(1);
|
|
3393
4282
|
}
|
|
4283
|
+
const availableProviders = [];
|
|
4284
|
+
if (config.providers.deepseek.apiKey) availableProviders.push("deepseek");
|
|
4285
|
+
if (config.providers.openai.apiKey) availableProviders.push("openai");
|
|
4286
|
+
if (config.providers.anthropic.apiKey) availableProviders.push("anthropic");
|
|
4287
|
+
if (isReconfig && availableProviders.length > 1) {
|
|
4288
|
+
console.log("");
|
|
4289
|
+
console.log(chalk6.bold.white(" Default Provider"));
|
|
4290
|
+
console.log(chalk6.dim(" Select the default LLM provider (the one used first)."));
|
|
4291
|
+
console.log("");
|
|
4292
|
+
for (let i = 0; i < availableProviders.length; i++) {
|
|
4293
|
+
const marker = availableProviders[i] === config.providers.default ? " (current)" : "";
|
|
4294
|
+
console.log(chalk6.white(` ${i + 1}. ${availableProviders[i]}${marker}`));
|
|
4295
|
+
}
|
|
4296
|
+
console.log("");
|
|
4297
|
+
const choice = await ask(chalk6.white(` Choose [1-${availableProviders.length}] [Enter to keep ${config.providers.default}]: `));
|
|
4298
|
+
const num = parseInt(choice, 10);
|
|
4299
|
+
if (num >= 1 && num <= availableProviders.length) {
|
|
4300
|
+
config.providers.default = availableProviders[num - 1];
|
|
4301
|
+
}
|
|
4302
|
+
} else if (!isReconfig) {
|
|
4303
|
+
config.providers.default = availableProviders[0];
|
|
4304
|
+
console.log(chalk6.dim(` Default provider set to ${config.providers.default}`));
|
|
4305
|
+
}
|
|
3394
4306
|
hr();
|
|
3395
4307
|
console.log("");
|
|
3396
|
-
console.log(
|
|
3397
|
-
|
|
4308
|
+
console.log(chalk6.bold.white(" Telegram (optional)"));
|
|
4309
|
+
if (isReconfig) {
|
|
4310
|
+
console.log(chalk6.dim(' Leave empty to keep current value. Enter "none" to disable.'));
|
|
4311
|
+
} else {
|
|
4312
|
+
console.log(chalk6.dim(" Leave empty to skip. You can add it later."));
|
|
4313
|
+
}
|
|
3398
4314
|
console.log("");
|
|
3399
|
-
const
|
|
3400
|
-
|
|
4315
|
+
const tgMask = isReconfig && config.channels.telegram.botToken ? ` [${maskKey(config.channels.telegram.botToken)}]` : "";
|
|
4316
|
+
const telegramToken = await ask(chalk6.white(` Telegram Bot Token${tgMask}: `));
|
|
4317
|
+
if (isReconfig && telegramToken.toLowerCase() === "none") {
|
|
4318
|
+
config.channels.telegram.enabled = false;
|
|
4319
|
+
config.channels.telegram.botToken = "";
|
|
4320
|
+
} else if (telegramToken) {
|
|
3401
4321
|
config.channels.telegram.botToken = telegramToken;
|
|
3402
4322
|
config.channels.telegram.enabled = true;
|
|
3403
4323
|
}
|
|
3404
4324
|
hr();
|
|
4325
|
+
console.log("");
|
|
4326
|
+
console.log(chalk6.bold.white(" Token Budget"));
|
|
4327
|
+
console.log("");
|
|
4328
|
+
const budgetPrompt = isReconfig ? chalk6.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `) : chalk6.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `);
|
|
4329
|
+
const budgetStr = await ask(budgetPrompt);
|
|
4330
|
+
if (budgetStr) {
|
|
4331
|
+
const budget = parseInt(budgetStr.replace(/,/g, ""), 10);
|
|
4332
|
+
if (!isNaN(budget) && budget > 0) {
|
|
4333
|
+
config.tokens.dailyBudget = budget;
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
hr();
|
|
3405
4337
|
saveConfig(config);
|
|
3406
4338
|
const home = getMercuryHome();
|
|
3407
4339
|
console.log("");
|
|
3408
|
-
console.log(
|
|
3409
|
-
console.log(
|
|
3410
|
-
console.log(
|
|
3411
|
-
console.log(
|
|
3412
|
-
console.log(
|
|
4340
|
+
console.log(chalk6.green(` \u2713 Config saved to ${home}/mercury.yaml`));
|
|
4341
|
+
console.log(chalk6.green(` \u2713 Soul files seeded in ${home}/soul/`));
|
|
4342
|
+
console.log(chalk6.green(` \u2713 Memory stored in ${home}/memory/`));
|
|
4343
|
+
console.log(chalk6.green(` \u2713 Permissions seeded in ${home}/permissions.yaml`));
|
|
4344
|
+
console.log(chalk6.green(` \u2713 Skills directory ready in ${home}/skills/`));
|
|
3413
4345
|
console.log("");
|
|
3414
|
-
console.log(
|
|
3415
|
-
console.log(
|
|
4346
|
+
console.log(chalk6.cyan(` ${config.identity.name} is ready. Run \`mercury start\` to chat.`));
|
|
4347
|
+
console.log(chalk6.dim(" mercury.cosmicstack.org"));
|
|
3416
4348
|
console.log("");
|
|
3417
4349
|
}
|
|
3418
|
-
|
|
4350
|
+
function autoDaemonize() {
|
|
4351
|
+
const daemon = getDaemonStatus();
|
|
4352
|
+
if (daemon.running) {
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4355
|
+
console.log(chalk6.dim(" Setting up background mode..."));
|
|
4356
|
+
try {
|
|
4357
|
+
if (!isServiceInstalled()) {
|
|
4358
|
+
installService();
|
|
4359
|
+
}
|
|
4360
|
+
} catch {
|
|
4361
|
+
console.log(chalk6.dim(" Service install skipped (can run `mercury service install` later)."));
|
|
4362
|
+
}
|
|
4363
|
+
const ok = tryAutoDaemonize();
|
|
4364
|
+
if (ok) {
|
|
4365
|
+
const status = getDaemonStatus();
|
|
4366
|
+
console.log(chalk6.green(` \u2713 Mercury is running in background (PID: ${status.pid})`));
|
|
4367
|
+
console.log(chalk6.green(" \u2713 Auto-starts on login. Auto-restarts on crash."));
|
|
4368
|
+
console.log(chalk6.dim(" Use `mercury stop` to stop. `mercury restart` to restart."));
|
|
4369
|
+
} else {
|
|
4370
|
+
console.log(chalk6.dim(" Background mode not available. Run `mercury up` to set it up."));
|
|
4371
|
+
}
|
|
4372
|
+
console.log("");
|
|
4373
|
+
}
|
|
4374
|
+
async function runAgent(isDaemon = false) {
|
|
3419
4375
|
let config = loadConfig();
|
|
3420
4376
|
config = ensureCreatorField(config);
|
|
3421
4377
|
const name = config.identity.name;
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
4378
|
+
if (!isDaemon) {
|
|
4379
|
+
banner();
|
|
4380
|
+
console.log(chalk6.white(` ${name} is waking up...`));
|
|
4381
|
+
console.log("");
|
|
4382
|
+
} else {
|
|
4383
|
+
logger.info(`${name} is waking up (daemon mode)...`);
|
|
4384
|
+
}
|
|
3425
4385
|
const tokenBudget = new TokenBudget(config);
|
|
3426
4386
|
const providers = new ProviderRegistry(config);
|
|
3427
4387
|
if (!providers.hasProviders()) {
|
|
3428
|
-
|
|
4388
|
+
if (isDaemon) {
|
|
4389
|
+
logger.error("No LLM providers available. Run `mercury doctor` to configure API keys.");
|
|
4390
|
+
return;
|
|
4391
|
+
}
|
|
4392
|
+
console.log(chalk6.red(" No LLM providers available. Run `mercury doctor` to configure API keys."));
|
|
3429
4393
|
process.exit(1);
|
|
3430
4394
|
}
|
|
3431
4395
|
const available = providers.listAvailable();
|
|
3432
|
-
|
|
4396
|
+
if (!isDaemon) {
|
|
4397
|
+
console.log(chalk6.dim(` Providers: ${available.join(", ")}`));
|
|
4398
|
+
} else {
|
|
4399
|
+
logger.info({ providers: available }, "Providers loaded");
|
|
4400
|
+
}
|
|
3433
4401
|
const skillLoader = new SkillLoader();
|
|
3434
4402
|
const skills = skillLoader.discover();
|
|
3435
|
-
|
|
4403
|
+
if (!isDaemon) {
|
|
4404
|
+
console.log(chalk6.dim(` Skills: ${skills.length > 0 ? skills.map((s) => s.name).join(", ") : "none installed"}`));
|
|
4405
|
+
}
|
|
3436
4406
|
const scheduler = new Scheduler(config);
|
|
3437
4407
|
const identity = new Identity();
|
|
3438
4408
|
const shortTerm = new ShortTermMemory(config);
|
|
@@ -3440,6 +4410,13 @@ async function runAgent() {
|
|
|
3440
4410
|
const episodic = new EpisodicMemory(config);
|
|
3441
4411
|
const channels = new ChannelRegistry(config);
|
|
3442
4412
|
const capabilities = new CapabilityRegistry(skillLoader, scheduler, tokenBudget);
|
|
4413
|
+
capabilities.setChatCommandContext({
|
|
4414
|
+
toolNames: () => capabilities.getToolNames(),
|
|
4415
|
+
skillNames: () => skills.map((s) => s.name),
|
|
4416
|
+
config: () => config,
|
|
4417
|
+
tokenBudget: () => tokenBudget,
|
|
4418
|
+
manual: () => getManual()
|
|
4419
|
+
});
|
|
3443
4420
|
capabilities.setSendFileHandler(async (filePath) => {
|
|
3444
4421
|
const msg = channels.getActiveChannels().includes("telegram") ? channels.get("telegram") : channels.get("cli");
|
|
3445
4422
|
if (msg) {
|
|
@@ -3469,21 +4446,29 @@ async function runAgent() {
|
|
|
3469
4446
|
}
|
|
3470
4447
|
const activeCh = channels.getActiveChannels();
|
|
3471
4448
|
const toolNames = capabilities.getToolNames();
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
4449
|
+
if (!isDaemon) {
|
|
4450
|
+
console.log(chalk6.dim(` Channels: ${activeCh.join(", ")}`));
|
|
4451
|
+
console.log(chalk6.dim(` Tools: ${toolNames.join(", ")}`));
|
|
4452
|
+
console.log(chalk6.dim(` Permissions: ${getMercuryHome()}/permissions.yaml`));
|
|
4453
|
+
console.log(chalk6.dim(` Schedules: ${getMercuryHome()}/schedules.yaml`));
|
|
4454
|
+
if (config.identity.creator) {
|
|
4455
|
+
console.log(chalk6.dim(` Creator: ${config.identity.creator}`));
|
|
4456
|
+
}
|
|
4457
|
+
hr();
|
|
4458
|
+
console.log("");
|
|
4459
|
+
console.log(chalk6.green(` ${name} is live. Type a message and press Enter.`));
|
|
4460
|
+
console.log(chalk6.dim(" Ctrl+C to exit \xB7 /help for commands"));
|
|
4461
|
+
console.log("");
|
|
4462
|
+
} else {
|
|
4463
|
+
logger.info({ channels: activeCh, tools: toolNames }, "Mercury is live (daemon mode)");
|
|
3478
4464
|
}
|
|
3479
|
-
hr();
|
|
3480
|
-
console.log("");
|
|
3481
|
-
console.log(chalk3.green(` ${name} is live. Type a message and press Enter.`));
|
|
3482
|
-
console.log(chalk3.dim(" Ctrl+C to exit."));
|
|
3483
|
-
console.log("");
|
|
3484
4465
|
const shutdown = async () => {
|
|
3485
|
-
|
|
3486
|
-
|
|
4466
|
+
if (!isDaemon) {
|
|
4467
|
+
console.log("");
|
|
4468
|
+
console.log(chalk6.dim(` ${name} is shutting down...`));
|
|
4469
|
+
} else {
|
|
4470
|
+
logger.info("Mercury is shutting down (daemon mode)");
|
|
4471
|
+
}
|
|
3487
4472
|
await agent.shutdown();
|
|
3488
4473
|
process.exit(0);
|
|
3489
4474
|
};
|
|
@@ -3491,41 +4476,105 @@ async function runAgent() {
|
|
|
3491
4476
|
process.on("SIGTERM", shutdown);
|
|
3492
4477
|
}
|
|
3493
4478
|
var program = new Command();
|
|
3494
|
-
program.name("mercury").description("Mercury \u2014
|
|
4479
|
+
program.name("mercury").description("Mercury \u2014 Soul-driven AI agent with permission-hardened tools, token budgets, and multi-channel access.").version("0.2.0").option("-v, --verbose", "Show debug logs").action(async () => {
|
|
3495
4480
|
if (!isSetupComplete()) {
|
|
3496
|
-
await
|
|
4481
|
+
await configure();
|
|
4482
|
+
autoDaemonize();
|
|
3497
4483
|
return;
|
|
3498
4484
|
}
|
|
3499
4485
|
await runAgent();
|
|
3500
4486
|
});
|
|
3501
|
-
program.command("start").description("Start Mercury agent").option("-v, --verbose", "Show debug logs").action(async () => {
|
|
4487
|
+
program.command("start").description("Start Mercury agent").option("-v, --verbose", "Show debug logs").option("-d, --detached", "Run in background (daemon mode)").option("--daemon", "Internal flag for daemon child process").action(async (opts) => {
|
|
4488
|
+
if (opts.daemon) {
|
|
4489
|
+
await runWithWatchdog(() => runAgent(true));
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
if (opts.detached) {
|
|
4493
|
+
startBackground();
|
|
4494
|
+
return;
|
|
4495
|
+
}
|
|
3502
4496
|
if (!isSetupComplete()) {
|
|
3503
|
-
await
|
|
4497
|
+
await configure();
|
|
3504
4498
|
return;
|
|
3505
4499
|
}
|
|
3506
4500
|
await runAgent();
|
|
3507
4501
|
});
|
|
3508
|
-
program.command("
|
|
3509
|
-
|
|
4502
|
+
program.command("stop").description("Stop a background Mercury process").action(() => {
|
|
4503
|
+
stopDaemon();
|
|
4504
|
+
});
|
|
4505
|
+
program.command("restart").description("Restart a background Mercury process").action(() => {
|
|
4506
|
+
restartDaemon();
|
|
4507
|
+
});
|
|
4508
|
+
program.command("up").description("Ensure Mercury is running persistently \u2014 installs service if needed, starts daemon").action(async () => {
|
|
4509
|
+
if (!isSetupComplete()) {
|
|
4510
|
+
await configure();
|
|
4511
|
+
}
|
|
4512
|
+
const daemon = getDaemonStatus();
|
|
4513
|
+
if (daemon.running && daemon.pid) {
|
|
4514
|
+
console.log("");
|
|
4515
|
+
console.log(chalk6.green(` Mercury is already running (PID: ${daemon.pid})`));
|
|
4516
|
+
console.log(chalk6.dim(` Logs: ${daemon.logPath}`));
|
|
4517
|
+
console.log("");
|
|
4518
|
+
return;
|
|
4519
|
+
}
|
|
4520
|
+
if (!isServiceInstalled()) {
|
|
4521
|
+
console.log("");
|
|
4522
|
+
console.log(chalk6.cyan(" Installing Mercury as a system service..."));
|
|
4523
|
+
installService();
|
|
4524
|
+
}
|
|
4525
|
+
console.log(chalk6.cyan(" Starting Mercury in background..."));
|
|
4526
|
+
startBackground();
|
|
4527
|
+
});
|
|
4528
|
+
program.command("logs").description("Show recent daemon logs").action(() => {
|
|
4529
|
+
showLogs();
|
|
4530
|
+
});
|
|
4531
|
+
program.command("setup").description("Re-run the setup wizard (reconfigure)").action(async () => {
|
|
4532
|
+
if (isSetupComplete()) {
|
|
4533
|
+
await configure(loadConfig());
|
|
4534
|
+
} else {
|
|
4535
|
+
await configure();
|
|
4536
|
+
}
|
|
4537
|
+
});
|
|
4538
|
+
program.command("doctor").description("Reconfigure Mercury \u2014 change keys, name, settings (Enter to keep current)").action(async () => {
|
|
4539
|
+
if (isSetupComplete()) {
|
|
4540
|
+
await configure(loadConfig());
|
|
4541
|
+
} else {
|
|
4542
|
+
await configure();
|
|
4543
|
+
}
|
|
3510
4544
|
});
|
|
3511
|
-
program.command("status").description("Show current configuration").action(() => {
|
|
4545
|
+
program.command("status").description("Show current configuration and daemon status").action(() => {
|
|
3512
4546
|
const config = loadConfig();
|
|
3513
4547
|
const home = getMercuryHome();
|
|
3514
4548
|
const skillLoader = new SkillLoader();
|
|
3515
4549
|
const skills = skillLoader.discover();
|
|
4550
|
+
const daemon = getDaemonStatus();
|
|
3516
4551
|
banner();
|
|
3517
|
-
console.log(` Name: ${
|
|
3518
|
-
console.log(` Owner: ${
|
|
4552
|
+
console.log(` Name: ${chalk6.cyan(config.identity.name)}`);
|
|
4553
|
+
console.log(` Owner: ${chalk6.white(config.identity.owner || "(not set)")}`);
|
|
3519
4554
|
if (config.identity.creator) {
|
|
3520
|
-
console.log(` Creator: ${
|
|
3521
|
-
}
|
|
3522
|
-
console.log(` Provider: ${
|
|
3523
|
-
console.log(` Telegram: ${config.channels.telegram.enabled ?
|
|
3524
|
-
console.log(` Skills: ${skills.length > 0 ?
|
|
3525
|
-
console.log(` Budget: ${
|
|
3526
|
-
console.log(` Setup: ${isSetupComplete() ?
|
|
3527
|
-
console.log(`
|
|
4555
|
+
console.log(` Creator: ${chalk6.white(config.identity.creator)}`);
|
|
4556
|
+
}
|
|
4557
|
+
console.log(` Provider: ${chalk6.white(config.providers.default)}`);
|
|
4558
|
+
console.log(` Telegram: ${config.channels.telegram.enabled ? chalk6.green("enabled") : chalk6.dim("disabled")}`);
|
|
4559
|
+
console.log(` Skills: ${skills.length > 0 ? chalk6.green(skills.map((s) => s.name).join(", ")) : chalk6.dim("none")}`);
|
|
4560
|
+
console.log(` Budget: ${chalk6.white(config.tokens.dailyBudget.toLocaleString())} tokens/day`);
|
|
4561
|
+
console.log(` Setup: ${isSetupComplete() ? chalk6.green("complete") : chalk6.red("not done")}`);
|
|
4562
|
+
console.log(` Daemon: ${daemon.running ? chalk6.green(`running (PID: ${daemon.pid})`) : chalk6.dim("not running")}`);
|
|
4563
|
+
console.log(` Home: ${chalk6.dim(home)}`);
|
|
3528
4564
|
console.log("");
|
|
3529
4565
|
});
|
|
4566
|
+
program.command("help").description("Show capabilities and commands manual").action(() => {
|
|
4567
|
+
console.log(getManual());
|
|
4568
|
+
});
|
|
4569
|
+
var serviceCmd = program.command("service").description("Manage Mercury as a system service (auto-start, crash recovery)");
|
|
4570
|
+
serviceCmd.command("install").description("Install Mercury as a system service (auto-start on boot)").action(() => {
|
|
4571
|
+
installService();
|
|
4572
|
+
});
|
|
4573
|
+
serviceCmd.command("uninstall").description("Uninstall the system service").action(() => {
|
|
4574
|
+
uninstallService();
|
|
4575
|
+
});
|
|
4576
|
+
serviceCmd.command("status").description("Show system service status").action(() => {
|
|
4577
|
+
showServiceStatus();
|
|
4578
|
+
});
|
|
3530
4579
|
program.parse();
|
|
3531
4580
|
//# sourceMappingURL=index.js.map
|