@cosmicstack/mercury-agent 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -11
- package/dist/index.js +1145 -94
- 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 = [];
|
|
@@ -1973,7 +2165,7 @@ var TokenBudget = class {
|
|
|
1973
2165
|
|
|
1974
2166
|
// src/capabilities/permissions.ts
|
|
1975
2167
|
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
|
|
1976
|
-
import { join as join6, resolve } from "path";
|
|
2168
|
+
import { join as join6, resolve, sep } from "path";
|
|
1977
2169
|
import { homedir as homedir2 } from "os";
|
|
1978
2170
|
import { parse as parseYaml3, stringify as stringifyYaml3 } from "yaml";
|
|
1979
2171
|
var DEFAULT_MANIFEST = {
|
|
@@ -2229,7 +2421,7 @@ var PermissionManager = class {
|
|
|
2229
2421
|
const scopes = this.manifest.capabilities.filesystem.scopes;
|
|
2230
2422
|
for (const scope of scopes) {
|
|
2231
2423
|
const scopeResolved = resolve(scope.path.replace(/^~/, homedir2()));
|
|
2232
|
-
if (resolvedPath === scopeResolved || resolvedPath.startsWith(scopeResolved +
|
|
2424
|
+
if (resolvedPath === scopeResolved || resolvedPath.startsWith(scopeResolved + sep)) {
|
|
2233
2425
|
return scope;
|
|
2234
2426
|
}
|
|
2235
2427
|
}
|
|
@@ -2263,7 +2455,9 @@ var PermissionManager = class {
|
|
|
2263
2455
|
const pathPatterns = [
|
|
2264
2456
|
/(?:^|\s)(\/[^\s]+)/,
|
|
2265
2457
|
/(?:^|\s)(~\/[^\s]+)/,
|
|
2266
|
-
/(?:^|\s)\.\.\/([^\s]+)
|
|
2458
|
+
/(?:^|\s)\.\.\/([^\s]+)/,
|
|
2459
|
+
/(?:^|\s)([A-Za-z]:\\[^\s]+)/,
|
|
2460
|
+
/(?:^|\s)(\\\\[^\s]+)/
|
|
2267
2461
|
];
|
|
2268
2462
|
for (const p of pathPatterns) {
|
|
2269
2463
|
const match = command.match(p);
|
|
@@ -3110,12 +3304,19 @@ var CapabilityRegistry = class {
|
|
|
3110
3304
|
sendFileHandler;
|
|
3111
3305
|
currentChannelId = "cli";
|
|
3112
3306
|
currentChannelType = "cli";
|
|
3307
|
+
chatCommandContext;
|
|
3113
3308
|
constructor(skillLoader, scheduler, tokenBudget) {
|
|
3114
3309
|
this.permissions = new PermissionManager();
|
|
3115
3310
|
this.skillLoader = skillLoader;
|
|
3116
3311
|
this.scheduler = scheduler;
|
|
3117
3312
|
this.tokenBudget = tokenBudget;
|
|
3118
3313
|
}
|
|
3314
|
+
setChatCommandContext(ctx) {
|
|
3315
|
+
this.chatCommandContext = ctx;
|
|
3316
|
+
}
|
|
3317
|
+
getChatCommandContext() {
|
|
3318
|
+
return this.chatCommandContext;
|
|
3319
|
+
}
|
|
3119
3320
|
setChannelContext(channelId, channelType) {
|
|
3120
3321
|
this.currentChannelId = channelId;
|
|
3121
3322
|
this.currentChannelType = channelType;
|
|
@@ -3322,31 +3523,694 @@ Describe what this skill enables Mercury to do. When invoked via the use_skill t
|
|
|
3322
3523
|
}
|
|
3323
3524
|
};
|
|
3324
3525
|
|
|
3526
|
+
// src/utils/manual.ts
|
|
3527
|
+
import chalk3 from "chalk";
|
|
3528
|
+
function getManual() {
|
|
3529
|
+
const sections = [];
|
|
3530
|
+
sections.push("");
|
|
3531
|
+
sections.push(chalk3.bold.cyan(" MERCURY \u2014 Capabilities & Commands"));
|
|
3532
|
+
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"));
|
|
3533
|
+
sections.push("");
|
|
3534
|
+
sections.push(chalk3.bold.white(" Built-in Tools"));
|
|
3535
|
+
sections.push(chalk3.dim(" Tools Mercury can use during conversations."));
|
|
3536
|
+
sections.push("");
|
|
3537
|
+
const tools = [
|
|
3538
|
+
["read_file", "Read file contents", "path (required)"],
|
|
3539
|
+
["write_file", "Write to an existing file", "path, content"],
|
|
3540
|
+
["create_file", "Create a new file (+ dirs)", "path, content"],
|
|
3541
|
+
["edit_file", "Replace specific text in a file", "path, old_string, new_string"],
|
|
3542
|
+
["list_dir", "List directory contents", "path"],
|
|
3543
|
+
["delete_file", "Delete a file", "path"],
|
|
3544
|
+
["run_command", "Execute a shell command", "command"],
|
|
3545
|
+
["approve_command", "Permanently approve a command type", 'command (e.g. "curl")'],
|
|
3546
|
+
["fetch_url", "Fetch a URL and return content", "url, format? (text/markdown)"],
|
|
3547
|
+
["git_status", "Show working tree status", "path?"],
|
|
3548
|
+
["git_diff", "Show file changes", "path?, staged?"],
|
|
3549
|
+
["git_log", "Show commit history", "count?, path?"],
|
|
3550
|
+
["git_add", "Stage files for commit", "paths (array)"],
|
|
3551
|
+
["git_commit", "Create a commit", "message"],
|
|
3552
|
+
["git_push", "Push to remote (needs approval)", "remote?, branch?"],
|
|
3553
|
+
["install_skill", "Install a skill from content or URL", "content? or url?"],
|
|
3554
|
+
["list_skills", "List installed skills", "\u2014"],
|
|
3555
|
+
["use_skill", "Invoke a skill by name", "name"],
|
|
3556
|
+
["schedule_task", "Schedule a recurring or delayed task", "cron? or delay_seconds, description, prompt? or skill_name?"],
|
|
3557
|
+
["list_scheduled_tasks", "List all scheduled tasks", "\u2014"],
|
|
3558
|
+
["cancel_scheduled_task", "Cancel a scheduled task", "id"],
|
|
3559
|
+
["budget_status", "Check token budget", "\u2014"]
|
|
3560
|
+
];
|
|
3561
|
+
for (const [name, desc, params] of tools) {
|
|
3562
|
+
sections.push(` ${chalk3.cyan(name.padEnd(24))} ${desc}`);
|
|
3563
|
+
sections.push(` ${" ".repeat(24)} ${chalk3.dim(params)}`);
|
|
3564
|
+
}
|
|
3565
|
+
sections.push("");
|
|
3566
|
+
sections.push(chalk3.bold.white(" CLI Commands"));
|
|
3567
|
+
sections.push(chalk3.dim(" Run these from your terminal (no API calls consumed)."));
|
|
3568
|
+
sections.push("");
|
|
3569
|
+
const commands = [
|
|
3570
|
+
["mercury up", "Start persistently (install service + daemon)"],
|
|
3571
|
+
["mercury", "Start the agent (same as mercury start)"],
|
|
3572
|
+
["mercury start", "Start the agent in foreground"],
|
|
3573
|
+
["mercury start -d", "Start in background (daemon mode)"],
|
|
3574
|
+
["mercury restart", "Restart a background process"],
|
|
3575
|
+
["mercury stop", "Stop a background process"],
|
|
3576
|
+
["mercury logs", "Show recent daemon logs"],
|
|
3577
|
+
["mercury doctor", "Reconfigure settings (Enter keeps current)"],
|
|
3578
|
+
["mercury setup", "Re-run the setup wizard"],
|
|
3579
|
+
["mercury status", "Show config and daemon status"],
|
|
3580
|
+
["mercury help", "Show this manual"],
|
|
3581
|
+
["mercury service install", "Install as system service (auto-start)"],
|
|
3582
|
+
["mercury service uninstall", "Uninstall system service"],
|
|
3583
|
+
["mercury service status", "Show system service status"],
|
|
3584
|
+
["mercury --verbose", "Start with debug logging on stderr"]
|
|
3585
|
+
];
|
|
3586
|
+
for (const [cmd, desc] of commands) {
|
|
3587
|
+
sections.push(` ${chalk3.white(cmd.padEnd(26))} ${desc}`);
|
|
3588
|
+
}
|
|
3589
|
+
sections.push("");
|
|
3590
|
+
sections.push(chalk3.bold.white(" In-Chat Commands"));
|
|
3591
|
+
sections.push(chalk3.dim(" Type these during a conversation (no API calls)."));
|
|
3592
|
+
sections.push("");
|
|
3593
|
+
const chat = [
|
|
3594
|
+
["/help", "Show this manual"],
|
|
3595
|
+
["/status", "Show config and budget info"],
|
|
3596
|
+
["/tools", "List currently loaded tools"],
|
|
3597
|
+
["/skills", "List installed skills"],
|
|
3598
|
+
["/stream", "Toggle text streaming on/off (Telegram)"],
|
|
3599
|
+
["/stream on", "Enable streaming (live text updates)"],
|
|
3600
|
+
["/stream off", "Disable streaming (single message)"]
|
|
3601
|
+
];
|
|
3602
|
+
for (const [cmd, desc] of chat) {
|
|
3603
|
+
sections.push(` ${chalk3.white(cmd.padEnd(16))} ${desc}`);
|
|
3604
|
+
}
|
|
3605
|
+
sections.push("");
|
|
3606
|
+
sections.push(chalk3.bold.white(" Permissions"));
|
|
3607
|
+
sections.push("");
|
|
3608
|
+
const perms = [
|
|
3609
|
+
"Commands are blocked (never run), auto-approved, or need approval.",
|
|
3610
|
+
'Say "always" when prompted to permanently approve a command type.',
|
|
3611
|
+
"Edit ~/.mercury/permissions.yaml to customize manually.",
|
|
3612
|
+
"File access is scoped \u2014 new paths need approval (y/n/always)."
|
|
3613
|
+
];
|
|
3614
|
+
for (const p of perms) {
|
|
3615
|
+
sections.push(` ${chalk3.dim("\u2022")} ${p}`);
|
|
3616
|
+
}
|
|
3617
|
+
sections.push("");
|
|
3618
|
+
sections.push(chalk3.bold.white(" Skills"));
|
|
3619
|
+
sections.push("");
|
|
3620
|
+
const skillInfo = [
|
|
3621
|
+
"Skills live in ~/.mercury/skills/<name>/SKILL.md",
|
|
3622
|
+
'Install: ask Mercury to "install skill from <url>" or paste content',
|
|
3623
|
+
'Invoke: ask Mercury to "use skill <name>"',
|
|
3624
|
+
'Schedule: "remind me daily at 9am to run daily-digest skill"'
|
|
3625
|
+
];
|
|
3626
|
+
for (const s of skillInfo) {
|
|
3627
|
+
sections.push(` ${chalk3.dim("\u2022")} ${s}`);
|
|
3628
|
+
}
|
|
3629
|
+
sections.push("");
|
|
3630
|
+
sections.push(chalk3.bold.white(" Scheduling"));
|
|
3631
|
+
sections.push("");
|
|
3632
|
+
const schedInfo = [
|
|
3633
|
+
'Recurring: "every day at 9am remind me to\u2026"',
|
|
3634
|
+
'One-shot: "remind me in 15 seconds to\u2026"',
|
|
3635
|
+
"Tasks persist to ~/.mercury/schedules.yaml"
|
|
3636
|
+
];
|
|
3637
|
+
for (const s of schedInfo) {
|
|
3638
|
+
sections.push(` ${chalk3.dim("\u2022")} ${s}`);
|
|
3639
|
+
}
|
|
3640
|
+
sections.push("");
|
|
3641
|
+
sections.push(chalk3.bold.white(" Configuration"));
|
|
3642
|
+
sections.push("");
|
|
3643
|
+
const configInfo = [
|
|
3644
|
+
["~/.mercury/mercury.yaml", "Main config (providers, channels, budget)"],
|
|
3645
|
+
["~/.mercury/permissions.yaml", "Capabilities and approval rules"],
|
|
3646
|
+
["~/.mercury/soul/*.md", "Agent personality (soul, persona, taste, heartbeat)"],
|
|
3647
|
+
["~/.mercury/skills/", "Installed skills"],
|
|
3648
|
+
["~/.mercury/schedules.yaml", "Scheduled tasks"],
|
|
3649
|
+
["~/.mercury/token-usage.json", "Daily token usage tracking"],
|
|
3650
|
+
["~/.mercury/memory/", "Short-term, long-term, episodic memory"]
|
|
3651
|
+
];
|
|
3652
|
+
for (const [path3, desc] of configInfo) {
|
|
3653
|
+
sections.push(` ${chalk3.dim(path3.padEnd(36))} ${desc}`);
|
|
3654
|
+
}
|
|
3655
|
+
sections.push("");
|
|
3656
|
+
sections.push(chalk3.dim(" mercury.cosmicstack.org"));
|
|
3657
|
+
sections.push("");
|
|
3658
|
+
return sections.join("\n");
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
// src/cli/daemon.ts
|
|
3662
|
+
import { spawn } from "child_process";
|
|
3663
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, mkdirSync as mkdirSync9, openSync } from "fs";
|
|
3664
|
+
import { join as join9 } from "path";
|
|
3665
|
+
import process2 from "process";
|
|
3666
|
+
import chalk4 from "chalk";
|
|
3667
|
+
var PID_FILE = "daemon.pid";
|
|
3668
|
+
var LOG_FILE = "daemon.log";
|
|
3669
|
+
function pidPath() {
|
|
3670
|
+
return join9(getMercuryHome(), PID_FILE);
|
|
3671
|
+
}
|
|
3672
|
+
function logPath() {
|
|
3673
|
+
return join9(getMercuryHome(), LOG_FILE);
|
|
3674
|
+
}
|
|
3675
|
+
function readPid() {
|
|
3676
|
+
const path3 = pidPath();
|
|
3677
|
+
if (!existsSync14(path3)) return null;
|
|
3678
|
+
try {
|
|
3679
|
+
const pid = parseInt(readFileSync11(path3, "utf-8").trim(), 10);
|
|
3680
|
+
if (isNaN(pid)) return null;
|
|
3681
|
+
return pid;
|
|
3682
|
+
} catch {
|
|
3683
|
+
return null;
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
function isProcessRunning(pid) {
|
|
3687
|
+
try {
|
|
3688
|
+
process2.kill(pid, 0);
|
|
3689
|
+
return true;
|
|
3690
|
+
} catch {
|
|
3691
|
+
return false;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
function getDaemonStatus() {
|
|
3695
|
+
const pid = readPid();
|
|
3696
|
+
if (!pid) return { running: false, pid: null, logPath: logPath() };
|
|
3697
|
+
return { running: isProcessRunning(pid), pid, logPath: logPath() };
|
|
3698
|
+
}
|
|
3699
|
+
function startBackground() {
|
|
3700
|
+
const status = getDaemonStatus();
|
|
3701
|
+
if (status.running && status.pid) {
|
|
3702
|
+
console.log(chalk4.yellow(` Mercury is already running (PID: ${status.pid})`));
|
|
3703
|
+
console.log(chalk4.dim(` Use \`mercury stop\` to stop it first.`));
|
|
3704
|
+
console.log("");
|
|
3705
|
+
process2.exit(1);
|
|
3706
|
+
}
|
|
3707
|
+
if (status.pid && !status.running) {
|
|
3708
|
+
try {
|
|
3709
|
+
unlinkSync3(pidPath());
|
|
3710
|
+
} catch {
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
const home = getMercuryHome();
|
|
3714
|
+
if (!existsSync14(home)) {
|
|
3715
|
+
mkdirSync9(home, { recursive: true });
|
|
3716
|
+
}
|
|
3717
|
+
const logFile = logPath();
|
|
3718
|
+
const isWin = process2.platform === "win32";
|
|
3719
|
+
const outFd = openSync(logFile, "a");
|
|
3720
|
+
const child = spawn(process2.execPath, [process2.argv[1], "start", "--daemon"], {
|
|
3721
|
+
detached: true,
|
|
3722
|
+
stdio: ["ignore", outFd, outFd],
|
|
3723
|
+
env: { ...process2.env },
|
|
3724
|
+
windowsHide: isWin
|
|
3725
|
+
});
|
|
3726
|
+
child.unref();
|
|
3727
|
+
writeFileSync11(pidPath(), String(child.pid));
|
|
3728
|
+
console.log("");
|
|
3729
|
+
console.log(chalk4.green(` Mercury started in background (PID: ${child.pid})`));
|
|
3730
|
+
console.log(chalk4.dim(` Logs: ${logFile}`));
|
|
3731
|
+
console.log(chalk4.dim(` Use \`mercury stop\` to stop.`));
|
|
3732
|
+
console.log(chalk4.dim(` Use \`mercury logs\` to view logs.`));
|
|
3733
|
+
console.log("");
|
|
3734
|
+
}
|
|
3735
|
+
function stopDaemon() {
|
|
3736
|
+
const status = getDaemonStatus();
|
|
3737
|
+
if (!status.pid) {
|
|
3738
|
+
console.log(chalk4.yellow(" Mercury is not running as a daemon."));
|
|
3739
|
+
console.log("");
|
|
3740
|
+
process2.exit(0);
|
|
3741
|
+
}
|
|
3742
|
+
if (!status.running) {
|
|
3743
|
+
console.log(chalk4.yellow(` Stale PID file found (PID: ${status.pid} is not running). Cleaning up.`));
|
|
3744
|
+
try {
|
|
3745
|
+
unlinkSync3(pidPath());
|
|
3746
|
+
} catch {
|
|
3747
|
+
}
|
|
3748
|
+
console.log("");
|
|
3749
|
+
process2.exit(0);
|
|
3750
|
+
}
|
|
3751
|
+
try {
|
|
3752
|
+
if (process2.platform === "win32") {
|
|
3753
|
+
process2.kill(status.pid);
|
|
3754
|
+
} else {
|
|
3755
|
+
process2.kill(status.pid, "SIGTERM");
|
|
3756
|
+
}
|
|
3757
|
+
console.log(chalk4.green(` Mercury stopped (PID: ${status.pid})`));
|
|
3758
|
+
} catch {
|
|
3759
|
+
console.log(chalk4.red(` Failed to stop PID ${status.pid}. You may need to kill it manually.`));
|
|
3760
|
+
}
|
|
3761
|
+
try {
|
|
3762
|
+
unlinkSync3(pidPath());
|
|
3763
|
+
} catch {
|
|
3764
|
+
}
|
|
3765
|
+
console.log("");
|
|
3766
|
+
}
|
|
3767
|
+
function restartDaemon() {
|
|
3768
|
+
const status = getDaemonStatus();
|
|
3769
|
+
if (status.running && status.pid) {
|
|
3770
|
+
console.log(chalk4.yellow(` Stopping Mercury (PID: ${status.pid})...`));
|
|
3771
|
+
try {
|
|
3772
|
+
if (process2.platform === "win32") {
|
|
3773
|
+
process2.kill(status.pid);
|
|
3774
|
+
} else {
|
|
3775
|
+
process2.kill(status.pid, "SIGTERM");
|
|
3776
|
+
}
|
|
3777
|
+
} catch {
|
|
3778
|
+
}
|
|
3779
|
+
try {
|
|
3780
|
+
unlinkSync3(pidPath());
|
|
3781
|
+
} catch {
|
|
3782
|
+
}
|
|
3783
|
+
console.log(chalk4.green(" Mercury stopped."));
|
|
3784
|
+
} else if (status.pid) {
|
|
3785
|
+
try {
|
|
3786
|
+
unlinkSync3(pidPath());
|
|
3787
|
+
} catch {
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
console.log(chalk4.yellow(" Starting Mercury..."));
|
|
3791
|
+
startBackground();
|
|
3792
|
+
}
|
|
3793
|
+
function showLogs() {
|
|
3794
|
+
const logFile = logPath();
|
|
3795
|
+
if (!existsSync14(logFile)) {
|
|
3796
|
+
console.log(chalk4.dim(" No daemon log file found."));
|
|
3797
|
+
console.log("");
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
const content = readFileSync11(logFile, "utf-8");
|
|
3801
|
+
const lines = content.split("\n").slice(-100);
|
|
3802
|
+
console.log(lines.join("\n"));
|
|
3803
|
+
}
|
|
3804
|
+
function tryAutoDaemonize() {
|
|
3805
|
+
try {
|
|
3806
|
+
const status = getDaemonStatus();
|
|
3807
|
+
if (status.running && status.pid) {
|
|
3808
|
+
return true;
|
|
3809
|
+
}
|
|
3810
|
+
if (status.pid && !status.running) {
|
|
3811
|
+
try {
|
|
3812
|
+
unlinkSync3(pidPath());
|
|
3813
|
+
} catch {
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
const home = getMercuryHome();
|
|
3817
|
+
if (!existsSync14(home)) {
|
|
3818
|
+
mkdirSync9(home, { recursive: true });
|
|
3819
|
+
}
|
|
3820
|
+
const logFile = logPath();
|
|
3821
|
+
const isWin = process2.platform === "win32";
|
|
3822
|
+
const outFd = openSync(logFile, "a");
|
|
3823
|
+
const child = spawn(process2.execPath, [process2.argv[1], "start", "--daemon"], {
|
|
3824
|
+
detached: true,
|
|
3825
|
+
stdio: ["ignore", outFd, outFd],
|
|
3826
|
+
env: { ...process2.env },
|
|
3827
|
+
windowsHide: isWin
|
|
3828
|
+
});
|
|
3829
|
+
child.unref();
|
|
3830
|
+
if (!child.pid) {
|
|
3831
|
+
return false;
|
|
3832
|
+
}
|
|
3833
|
+
writeFileSync11(pidPath(), String(child.pid));
|
|
3834
|
+
return true;
|
|
3835
|
+
} catch {
|
|
3836
|
+
return false;
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
// src/cli/service.ts
|
|
3841
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4 } from "fs";
|
|
3842
|
+
import { join as join10 } from "path";
|
|
3843
|
+
import { homedir as homedir3 } from "os";
|
|
3844
|
+
import chalk5 from "chalk";
|
|
3845
|
+
import { execSync as execSync8 } from "child_process";
|
|
3846
|
+
var SERVICE_DESC = "Mercury \u2014 Soul-Driven AI Agent";
|
|
3847
|
+
var WIN_TASK_NAME = "MercuryAgent";
|
|
3848
|
+
function isServiceInstalled() {
|
|
3849
|
+
const platform = process.platform;
|
|
3850
|
+
if (platform === "darwin") {
|
|
3851
|
+
return existsSync15(join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist"));
|
|
3852
|
+
} else if (platform === "linux") {
|
|
3853
|
+
return existsSync15(join10(homedir3(), ".config", "systemd", "user", "mercury.service"));
|
|
3854
|
+
} else if (platform === "win32") {
|
|
3855
|
+
try {
|
|
3856
|
+
execSync8(`schtasks /query /tn "${WIN_TASK_NAME}"`, { stdio: "pipe", shell: "cmd.exe" });
|
|
3857
|
+
return true;
|
|
3858
|
+
} catch {
|
|
3859
|
+
return false;
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
return false;
|
|
3863
|
+
}
|
|
3864
|
+
function getNodeBinPath() {
|
|
3865
|
+
return process.execPath;
|
|
3866
|
+
}
|
|
3867
|
+
function getDistPath() {
|
|
3868
|
+
return join10(process.argv[1] || "/usr/local/bin/mercury", "..", "..", "lib", "node_modules", "@cosmicstack", "mercury-agent", "dist", "index.js");
|
|
3869
|
+
}
|
|
3870
|
+
function installService() {
|
|
3871
|
+
const platform = process.platform;
|
|
3872
|
+
if (platform === "darwin") {
|
|
3873
|
+
installMac();
|
|
3874
|
+
} else if (platform === "linux") {
|
|
3875
|
+
installLinux();
|
|
3876
|
+
} else if (platform === "win32") {
|
|
3877
|
+
installWindows();
|
|
3878
|
+
} else {
|
|
3879
|
+
console.log(chalk5.red(` Unsupported platform: ${platform}`));
|
|
3880
|
+
process.exit(1);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
function uninstallService() {
|
|
3884
|
+
const platform = process.platform;
|
|
3885
|
+
if (platform === "darwin") {
|
|
3886
|
+
uninstallMac();
|
|
3887
|
+
} else if (platform === "linux") {
|
|
3888
|
+
uninstallLinux();
|
|
3889
|
+
} else if (platform === "win32") {
|
|
3890
|
+
uninstallWindows();
|
|
3891
|
+
} else {
|
|
3892
|
+
console.log(chalk5.red(` Unsupported platform: ${platform}`));
|
|
3893
|
+
process.exit(1);
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
function showServiceStatus() {
|
|
3897
|
+
const platform = process.platform;
|
|
3898
|
+
if (platform === "darwin") {
|
|
3899
|
+
showMacStatus();
|
|
3900
|
+
} else if (platform === "linux") {
|
|
3901
|
+
showLinuxStatus();
|
|
3902
|
+
} else if (platform === "win32") {
|
|
3903
|
+
showWindowsStatus();
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
function installMac() {
|
|
3907
|
+
const plistDir = join10(homedir3(), "Library", "LaunchAgents");
|
|
3908
|
+
const plistPath = join10(plistDir, "com.cosmicstack.mercury.plist");
|
|
3909
|
+
if (!existsSync15(plistDir)) {
|
|
3910
|
+
mkdirSync10(plistDir, { recursive: true });
|
|
3911
|
+
}
|
|
3912
|
+
const nodeBin = getNodeBinPath();
|
|
3913
|
+
const scriptPath = getDistPath();
|
|
3914
|
+
const home = getMercuryHome();
|
|
3915
|
+
const logPath2 = join10(home, "daemon.log");
|
|
3916
|
+
const errPath = join10(home, "daemon-error.log");
|
|
3917
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3918
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3919
|
+
<plist version="1.0">
|
|
3920
|
+
<dict>
|
|
3921
|
+
<key>Label</key>
|
|
3922
|
+
<string>com.cosmicstack.mercury</string>
|
|
3923
|
+
<key>ProgramArguments</key>
|
|
3924
|
+
<array>
|
|
3925
|
+
<string>${nodeBin}</string>
|
|
3926
|
+
<string>${scriptPath}</string>
|
|
3927
|
+
<string>start</string>
|
|
3928
|
+
<string>--daemon</string>
|
|
3929
|
+
</array>
|
|
3930
|
+
<key>RunAtLoad</key>
|
|
3931
|
+
<true/>
|
|
3932
|
+
<key>KeepAlive</key>
|
|
3933
|
+
<dict>
|
|
3934
|
+
<key>SuccessfulExit</key>
|
|
3935
|
+
<false/>
|
|
3936
|
+
</dict>
|
|
3937
|
+
<key>StandardOutPath</key>
|
|
3938
|
+
<string>${logPath2}</string>
|
|
3939
|
+
<key>StandardErrorPath</key>
|
|
3940
|
+
<string>${errPath}</string>
|
|
3941
|
+
<key>EnvironmentVariables</key>
|
|
3942
|
+
<dict>
|
|
3943
|
+
<key>PATH</key>
|
|
3944
|
+
<string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
|
|
3945
|
+
<key>HOME</key>
|
|
3946
|
+
<string>${homedir3()}</string>
|
|
3947
|
+
</dict>
|
|
3948
|
+
<key>WorkingDirectory</key>
|
|
3949
|
+
<string>${homedir3()}</string>
|
|
3950
|
+
</dict>
|
|
3951
|
+
</plist>`;
|
|
3952
|
+
writeFileSync12(plistPath, plist, "utf-8");
|
|
3953
|
+
try {
|
|
3954
|
+
execSync8(`launchctl load ${plistPath}`, { stdio: "inherit" });
|
|
3955
|
+
} catch {
|
|
3956
|
+
console.log(chalk5.yellow(" launchctl load failed. Try running:"));
|
|
3957
|
+
console.log(chalk5.dim(` launchctl load ${plistPath}`));
|
|
3958
|
+
}
|
|
3959
|
+
console.log("");
|
|
3960
|
+
console.log(chalk5.green(" Mercury service installed (macOS LaunchAgent)"));
|
|
3961
|
+
console.log(chalk5.dim(` Plist: ${plistPath}`));
|
|
3962
|
+
console.log(chalk5.dim(` Logs: ${logPath2}`));
|
|
3963
|
+
console.log(chalk5.dim(" Auto-starts on login. Auto-restarts on crash."));
|
|
3964
|
+
console.log("");
|
|
3965
|
+
console.log(chalk5.dim(" Uninstall: mercury service uninstall"));
|
|
3966
|
+
console.log("");
|
|
3967
|
+
}
|
|
3968
|
+
function uninstallMac() {
|
|
3969
|
+
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
3970
|
+
if (!existsSync15(plistPath)) {
|
|
3971
|
+
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
3972
|
+
console.log("");
|
|
3973
|
+
process.exit(0);
|
|
3974
|
+
}
|
|
3975
|
+
try {
|
|
3976
|
+
execSync8(`launchctl unload ${plistPath}`, { stdio: "inherit" });
|
|
3977
|
+
} catch {
|
|
3978
|
+
}
|
|
3979
|
+
try {
|
|
3980
|
+
unlinkSync4(plistPath);
|
|
3981
|
+
} catch {
|
|
3982
|
+
console.log(chalk5.yellow(" Failed to remove plist file. Remove manually:"));
|
|
3983
|
+
console.log(chalk5.dim(` rm ${plistPath}`));
|
|
3984
|
+
}
|
|
3985
|
+
console.log("");
|
|
3986
|
+
console.log(chalk5.green(" Mercury service uninstalled"));
|
|
3987
|
+
console.log("");
|
|
3988
|
+
}
|
|
3989
|
+
function showMacStatus() {
|
|
3990
|
+
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
3991
|
+
if (!existsSync15(plistPath)) {
|
|
3992
|
+
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
3993
|
+
console.log(chalk5.dim(" Run `mercury service install` to set it up."));
|
|
3994
|
+
console.log("");
|
|
3995
|
+
return;
|
|
3996
|
+
}
|
|
3997
|
+
try {
|
|
3998
|
+
const output = execSync8("launchctl list | grep com.cosmicstack.mercury", { encoding: "utf-8" }).trim();
|
|
3999
|
+
console.log(` ${chalk5.green("Service installed and loaded")}`);
|
|
4000
|
+
console.log(chalk5.dim(` ${output}`));
|
|
4001
|
+
} catch {
|
|
4002
|
+
console.log(` ${chalk5.yellow("Service installed but not loaded")}`);
|
|
4003
|
+
console.log(chalk5.dim(` Plist: ${plistPath}`));
|
|
4004
|
+
}
|
|
4005
|
+
console.log("");
|
|
4006
|
+
}
|
|
4007
|
+
function installLinux() {
|
|
4008
|
+
const systemdDir = join10(homedir3(), ".config", "systemd", "user");
|
|
4009
|
+
if (!existsSync15(systemdDir)) {
|
|
4010
|
+
mkdirSync10(systemdDir, { recursive: true });
|
|
4011
|
+
}
|
|
4012
|
+
const servicePath = join10(systemdDir, "mercury.service");
|
|
4013
|
+
const nodeBin = getNodeBinPath();
|
|
4014
|
+
const scriptPath = getDistPath();
|
|
4015
|
+
const home = getMercuryHome();
|
|
4016
|
+
const service = `[Unit]
|
|
4017
|
+
Description=${SERVICE_DESC}
|
|
4018
|
+
After=network.target
|
|
4019
|
+
|
|
4020
|
+
[Service]
|
|
4021
|
+
Type=simple
|
|
4022
|
+
ExecStart=${nodeBin} ${scriptPath} start --daemon
|
|
4023
|
+
Restart=on-failure
|
|
4024
|
+
RestartSec=5
|
|
4025
|
+
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
4026
|
+
Environment=HOME=${homedir3()}
|
|
4027
|
+
WorkingDirectory=${homedir3()}
|
|
4028
|
+
StandardOutput=append:${join10(home, "daemon.log")}
|
|
4029
|
+
StandardError=append:${join10(home, "daemon-error.log")}
|
|
4030
|
+
|
|
4031
|
+
[Install]
|
|
4032
|
+
WantedBy=default.target`;
|
|
4033
|
+
writeFileSync12(servicePath, service, "utf-8");
|
|
4034
|
+
try {
|
|
4035
|
+
execSync8("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
4036
|
+
execSync8("systemctl --user enable mercury.service", { stdio: "inherit" });
|
|
4037
|
+
execSync8("systemctl --user start mercury.service", { stdio: "inherit" });
|
|
4038
|
+
} catch (err) {
|
|
4039
|
+
console.log(chalk5.yellow(" systemd commands failed. Try running manually:"));
|
|
4040
|
+
console.log(chalk5.dim(" systemctl --user daemon-reload"));
|
|
4041
|
+
console.log(chalk5.dim(" systemctl --user enable mercury.service"));
|
|
4042
|
+
console.log(chalk5.dim(" systemctl --user start mercury.service"));
|
|
4043
|
+
}
|
|
4044
|
+
try {
|
|
4045
|
+
execSync8(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
|
|
4046
|
+
} catch {
|
|
4047
|
+
console.log(chalk5.yellow(" Enable linger failed (needed for boot-without-login). Try:"));
|
|
4048
|
+
console.log(chalk5.dim(` sudo loginctl enable-linger ${process.env.USER || "$USER"}`));
|
|
4049
|
+
}
|
|
4050
|
+
console.log("");
|
|
4051
|
+
console.log(chalk5.green(" Mercury service installed (systemd --user)"));
|
|
4052
|
+
console.log(chalk5.dim(` Service: ${servicePath}`));
|
|
4053
|
+
console.log(chalk5.dim(` Logs: ${join10(home, "daemon.log")}`));
|
|
4054
|
+
console.log(chalk5.dim(" Auto-starts on login. Auto-restarts on crash (5s delay)."));
|
|
4055
|
+
console.log("");
|
|
4056
|
+
console.log(chalk5.dim(" Uninstall: mercury service uninstall"));
|
|
4057
|
+
console.log("");
|
|
4058
|
+
}
|
|
4059
|
+
function uninstallLinux() {
|
|
4060
|
+
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4061
|
+
if (!existsSync15(servicePath)) {
|
|
4062
|
+
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4063
|
+
console.log("");
|
|
4064
|
+
process.exit(0);
|
|
4065
|
+
}
|
|
4066
|
+
try {
|
|
4067
|
+
execSync8("systemctl --user stop mercury.service", { stdio: "inherit" });
|
|
4068
|
+
execSync8("systemctl --user disable mercury.service", { stdio: "inherit" });
|
|
4069
|
+
} catch {
|
|
4070
|
+
}
|
|
4071
|
+
try {
|
|
4072
|
+
unlinkSync4(servicePath);
|
|
4073
|
+
} catch {
|
|
4074
|
+
console.log(chalk5.yellow(" Failed to remove service file. Remove manually:"));
|
|
4075
|
+
console.log(chalk5.dim(` rm ${servicePath}`));
|
|
4076
|
+
}
|
|
4077
|
+
try {
|
|
4078
|
+
execSync8("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
4079
|
+
} catch {
|
|
4080
|
+
}
|
|
4081
|
+
console.log("");
|
|
4082
|
+
console.log(chalk5.green(" Mercury service uninstalled"));
|
|
4083
|
+
console.log("");
|
|
4084
|
+
}
|
|
4085
|
+
function showLinuxStatus() {
|
|
4086
|
+
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4087
|
+
if (!existsSync15(servicePath)) {
|
|
4088
|
+
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4089
|
+
console.log(chalk5.dim(" Run `mercury service install` to set it up."));
|
|
4090
|
+
console.log("");
|
|
4091
|
+
return;
|
|
4092
|
+
}
|
|
4093
|
+
try {
|
|
4094
|
+
const output = execSync8("systemctl --user status mercury.service", { encoding: "utf-8" }).trim();
|
|
4095
|
+
console.log(output);
|
|
4096
|
+
} catch (err) {
|
|
4097
|
+
console.log(chalk5.yellow(" Could not get service status:"));
|
|
4098
|
+
console.log(chalk5.dim(` ${err.message || err}`));
|
|
4099
|
+
}
|
|
4100
|
+
console.log("");
|
|
4101
|
+
}
|
|
4102
|
+
function installWindows() {
|
|
4103
|
+
const nodeBin = getNodeBinPath();
|
|
4104
|
+
const scriptPath = getDistPath();
|
|
4105
|
+
const home = getMercuryHome();
|
|
4106
|
+
const logPath2 = join10(home, "daemon.log");
|
|
4107
|
+
const cmd = `"${nodeBin}" "${scriptPath}" start --daemon`;
|
|
4108
|
+
try {
|
|
4109
|
+
execSync8(
|
|
4110
|
+
`schtasks /create /tn "${WIN_TASK_NAME}" /tr "${cmd}" /sc onlogon /rl limited /f`,
|
|
4111
|
+
{ stdio: "inherit", shell: "cmd.exe" }
|
|
4112
|
+
);
|
|
4113
|
+
} catch {
|
|
4114
|
+
console.log(chalk5.yellow(" schtasks create failed. Try running from an Administrator cmd:"));
|
|
4115
|
+
console.log(chalk5.dim(` schtasks /create /tn "${WIN_TASK_NAME}" /tr "${cmd}" /sc onlogon /rl limited /f`));
|
|
4116
|
+
}
|
|
4117
|
+
try {
|
|
4118
|
+
execSync8(`schtasks /run /tn "${WIN_TASK_NAME}"`, { stdio: "inherit", shell: "cmd.exe" });
|
|
4119
|
+
} catch {
|
|
4120
|
+
console.log(chalk5.yellow(" Task created but failed to start immediately. It will start on next login."));
|
|
4121
|
+
}
|
|
4122
|
+
console.log("");
|
|
4123
|
+
console.log(chalk5.green(" Mercury service installed (Windows Task Scheduler)"));
|
|
4124
|
+
console.log(chalk5.dim(` Task: ${WIN_TASK_NAME}`));
|
|
4125
|
+
console.log(chalk5.dim(` Trigger: on logon`));
|
|
4126
|
+
console.log(chalk5.dim(` Logs: ${logPath2}`));
|
|
4127
|
+
console.log(chalk5.dim(" Auto-starts on login. Use --daemon flag for crash recovery."));
|
|
4128
|
+
console.log("");
|
|
4129
|
+
console.log(chalk5.dim(" Uninstall: mercury service uninstall"));
|
|
4130
|
+
console.log("");
|
|
4131
|
+
}
|
|
4132
|
+
function uninstallWindows() {
|
|
4133
|
+
try {
|
|
4134
|
+
execSync8(`schtasks /delete /tn "${WIN_TASK_NAME}" /f`, { stdio: "inherit", shell: "cmd.exe" });
|
|
4135
|
+
console.log("");
|
|
4136
|
+
console.log(chalk5.green(" Mercury service uninstalled"));
|
|
4137
|
+
console.log("");
|
|
4138
|
+
} catch {
|
|
4139
|
+
console.log(chalk5.yellow(" Task not found or failed to delete. Remove manually:"));
|
|
4140
|
+
console.log(chalk5.dim(` schtasks /delete /tn "${WIN_TASK_NAME}" /f`));
|
|
4141
|
+
console.log("");
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
function showWindowsStatus() {
|
|
4145
|
+
try {
|
|
4146
|
+
const output = execSync8(`schtasks /query /tn "${WIN_TASK_NAME}" /fo list`, {
|
|
4147
|
+
encoding: "utf-8",
|
|
4148
|
+
shell: "cmd.exe"
|
|
4149
|
+
}).trim();
|
|
4150
|
+
console.log(output);
|
|
4151
|
+
console.log("");
|
|
4152
|
+
} catch {
|
|
4153
|
+
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4154
|
+
console.log(chalk5.dim(" Run `mercury service install` to set it up."));
|
|
4155
|
+
console.log("");
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
// src/cli/watchdog.ts
|
|
4160
|
+
var MAX_RESTARTS = 10;
|
|
4161
|
+
var RESTART_WINDOW_MS = 6e4;
|
|
4162
|
+
var BASE_DELAY_MS = 1e3;
|
|
4163
|
+
async function runWithWatchdog(agentFn) {
|
|
4164
|
+
const restarts = [];
|
|
4165
|
+
async function attempt() {
|
|
4166
|
+
try {
|
|
4167
|
+
await agentFn();
|
|
4168
|
+
} catch (err) {
|
|
4169
|
+
const now = Date.now();
|
|
4170
|
+
restarts.push(now);
|
|
4171
|
+
const recentRestarts = restarts.filter((t) => now - t < RESTART_WINDOW_MS);
|
|
4172
|
+
const restartCount = recentRestarts.length;
|
|
4173
|
+
if (restartCount >= MAX_RESTARTS) {
|
|
4174
|
+
logger.error({ restartCount }, "Max restarts exceeded within 60s. Exiting.");
|
|
4175
|
+
process.exit(1);
|
|
4176
|
+
}
|
|
4177
|
+
const delay = BASE_DELAY_MS * Math.pow(1.25, restartCount);
|
|
4178
|
+
logger.warn({ err, restartCount, delay }, "Crash detected. Restarting with backoff...");
|
|
4179
|
+
await sleep(delay);
|
|
4180
|
+
await attempt();
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
await attempt();
|
|
4184
|
+
}
|
|
4185
|
+
function sleep(ms) {
|
|
4186
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
4187
|
+
}
|
|
4188
|
+
|
|
3325
4189
|
// src/index.ts
|
|
3326
4190
|
function hr() {
|
|
3327
|
-
console.log(
|
|
4191
|
+
console.log(chalk6.dim("\u2500".repeat(50)));
|
|
3328
4192
|
}
|
|
3329
4193
|
function banner() {
|
|
3330
4194
|
console.log("");
|
|
3331
4195
|
const art = figlet.textSync("MERCURY", { font: "Slant", horizontalLayout: "default" });
|
|
3332
4196
|
for (const line of art.split("\n")) {
|
|
3333
|
-
if (line.trim()) console.log(
|
|
4197
|
+
if (line.trim()) console.log(chalk6.bold.cyan(` ${line}`));
|
|
3334
4198
|
}
|
|
3335
4199
|
console.log("");
|
|
3336
|
-
console.log(
|
|
3337
|
-
console.log(
|
|
4200
|
+
console.log(chalk6.white(" an AI agent for personal tasks"));
|
|
4201
|
+
console.log(chalk6.dim(" v0.2.1 \xB7 by Cosmic Stack \xB7 mercury.cosmicstack.org"));
|
|
3338
4202
|
console.log("");
|
|
3339
4203
|
}
|
|
3340
4204
|
function splashScreen() {
|
|
3341
4205
|
console.log("");
|
|
3342
4206
|
const art = figlet.textSync("MERCURY", { font: "Slant", horizontalLayout: "default" });
|
|
3343
4207
|
for (const line of art.split("\n")) {
|
|
3344
|
-
if (line.trim()) console.log(
|
|
4208
|
+
if (line.trim()) console.log(chalk6.bold.cyan(` ${line}`));
|
|
3345
4209
|
}
|
|
3346
4210
|
console.log("");
|
|
3347
|
-
console.log(
|
|
3348
|
-
console.log(
|
|
3349
|
-
console.log(
|
|
4211
|
+
console.log(chalk6.dim(" an AI agent for personal tasks"));
|
|
4212
|
+
console.log(chalk6.cyan(" by Cosmic Stack"));
|
|
4213
|
+
console.log(chalk6.dim(" mercury.cosmicstack.org"));
|
|
3350
4214
|
console.log("");
|
|
3351
4215
|
}
|
|
3352
4216
|
async function ask(prompt) {
|
|
@@ -3358,81 +4222,189 @@ async function ask(prompt) {
|
|
|
3358
4222
|
});
|
|
3359
4223
|
});
|
|
3360
4224
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
4225
|
+
function maskKey(key) {
|
|
4226
|
+
if (!key) return "";
|
|
4227
|
+
if (key.length <= 8) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
4228
|
+
return key.slice(0, 4) + "\u2022\u2022\u2022\u2022" + key.slice(-4);
|
|
4229
|
+
}
|
|
4230
|
+
async function configure(existingConfig) {
|
|
4231
|
+
const isReconfig = !!existingConfig;
|
|
4232
|
+
const config = existingConfig ?? loadConfig();
|
|
4233
|
+
if (isReconfig) {
|
|
4234
|
+
banner();
|
|
4235
|
+
console.log(chalk6.yellow(" Reconfiguring Mercury \u2014 press Enter to keep current value."));
|
|
4236
|
+
} else {
|
|
4237
|
+
splashScreen();
|
|
4238
|
+
console.log(chalk6.yellow(" First run detected \u2014 let's set you up."));
|
|
4239
|
+
}
|
|
3364
4240
|
hr();
|
|
3365
4241
|
console.log("");
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
if (
|
|
3369
|
-
|
|
3370
|
-
|
|
4242
|
+
console.log(chalk6.bold.white(" Identity"));
|
|
4243
|
+
console.log("");
|
|
4244
|
+
if (isReconfig) {
|
|
4245
|
+
const ownerName = await ask(chalk6.white(` Your name [${config.identity.owner}]: `));
|
|
4246
|
+
if (ownerName) config.identity.owner = ownerName;
|
|
4247
|
+
const agentName = await ask(chalk6.white(` Agent name [${config.identity.name}]: `));
|
|
4248
|
+
if (agentName) config.identity.name = agentName;
|
|
4249
|
+
} else {
|
|
4250
|
+
const ownerName = await ask(chalk6.white(" Your name: "));
|
|
4251
|
+
if (!ownerName) {
|
|
4252
|
+
console.log(chalk6.red(" Name is required."));
|
|
4253
|
+
process.exit(1);
|
|
4254
|
+
}
|
|
4255
|
+
config.identity.owner = ownerName;
|
|
4256
|
+
const agentName = await ask(chalk6.white(` Agent name [${config.identity.name}]: `));
|
|
4257
|
+
if (agentName) config.identity.name = agentName;
|
|
3371
4258
|
}
|
|
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";
|
|
4259
|
+
config.identity.creator = config.identity.creator || "Cosmic Stack";
|
|
3376
4260
|
hr();
|
|
3377
4261
|
console.log("");
|
|
3378
|
-
console.log(
|
|
3379
|
-
|
|
4262
|
+
console.log(chalk6.bold.white(" LLM Providers"));
|
|
4263
|
+
if (isReconfig) {
|
|
4264
|
+
console.log(chalk6.dim(" Current keys shown masked. Enter new value to change, Enter to keep."));
|
|
4265
|
+
} else {
|
|
4266
|
+
console.log(chalk6.dim(" At least one API key is required."));
|
|
4267
|
+
}
|
|
3380
4268
|
console.log("");
|
|
3381
|
-
const
|
|
4269
|
+
const dsMask = isReconfig && config.providers.deepseek.apiKey ? ` [${maskKey(config.providers.deepseek.apiKey)}]` : "";
|
|
4270
|
+
const deepseekKey = await ask(chalk6.white(` DeepSeek API key${dsMask}: `));
|
|
3382
4271
|
if (deepseekKey) {
|
|
3383
4272
|
config.providers.deepseek.apiKey = deepseekKey;
|
|
3384
|
-
config.providers.default = "deepseek";
|
|
3385
4273
|
}
|
|
3386
|
-
const
|
|
4274
|
+
const oaiMask = isReconfig && config.providers.openai.apiKey ? ` [${maskKey(config.providers.openai.apiKey)}]` : " (Enter to skip)";
|
|
4275
|
+
const openaiKey = await ask(chalk6.white(` OpenAI API key${oaiMask}: `));
|
|
3387
4276
|
if (openaiKey) config.providers.openai.apiKey = openaiKey;
|
|
3388
|
-
const
|
|
4277
|
+
const antMask = isReconfig && config.providers.anthropic.apiKey ? ` [${maskKey(config.providers.anthropic.apiKey)}]` : " (Enter to skip)";
|
|
4278
|
+
const anthropicKey = await ask(chalk6.white(` Anthropic API key${antMask}: `));
|
|
3389
4279
|
if (anthropicKey) config.providers.anthropic.apiKey = anthropicKey;
|
|
3390
|
-
|
|
3391
|
-
|
|
4280
|
+
const hasKey = config.providers.deepseek.apiKey || config.providers.openai.apiKey || config.providers.anthropic.apiKey;
|
|
4281
|
+
if (!hasKey) {
|
|
4282
|
+
console.log(chalk6.red("\n At least one LLM API key is required."));
|
|
3392
4283
|
process.exit(1);
|
|
3393
4284
|
}
|
|
4285
|
+
const availableProviders = [];
|
|
4286
|
+
if (config.providers.deepseek.apiKey) availableProviders.push("deepseek");
|
|
4287
|
+
if (config.providers.openai.apiKey) availableProviders.push("openai");
|
|
4288
|
+
if (config.providers.anthropic.apiKey) availableProviders.push("anthropic");
|
|
4289
|
+
if (isReconfig && availableProviders.length > 1) {
|
|
4290
|
+
console.log("");
|
|
4291
|
+
console.log(chalk6.bold.white(" Default Provider"));
|
|
4292
|
+
console.log(chalk6.dim(" Select the default LLM provider (the one used first)."));
|
|
4293
|
+
console.log("");
|
|
4294
|
+
for (let i = 0; i < availableProviders.length; i++) {
|
|
4295
|
+
const marker = availableProviders[i] === config.providers.default ? " (current)" : "";
|
|
4296
|
+
console.log(chalk6.white(` ${i + 1}. ${availableProviders[i]}${marker}`));
|
|
4297
|
+
}
|
|
4298
|
+
console.log("");
|
|
4299
|
+
const choice = await ask(chalk6.white(` Choose [1-${availableProviders.length}] [Enter to keep ${config.providers.default}]: `));
|
|
4300
|
+
const num = parseInt(choice, 10);
|
|
4301
|
+
if (num >= 1 && num <= availableProviders.length) {
|
|
4302
|
+
config.providers.default = availableProviders[num - 1];
|
|
4303
|
+
}
|
|
4304
|
+
} else if (!isReconfig) {
|
|
4305
|
+
config.providers.default = availableProviders[0];
|
|
4306
|
+
console.log(chalk6.dim(` Default provider set to ${config.providers.default}`));
|
|
4307
|
+
}
|
|
3394
4308
|
hr();
|
|
3395
4309
|
console.log("");
|
|
3396
|
-
console.log(
|
|
3397
|
-
|
|
4310
|
+
console.log(chalk6.bold.white(" Telegram (optional)"));
|
|
4311
|
+
if (isReconfig) {
|
|
4312
|
+
console.log(chalk6.dim(' Leave empty to keep current value. Enter "none" to disable.'));
|
|
4313
|
+
} else {
|
|
4314
|
+
console.log(chalk6.dim(" Leave empty to skip. You can add it later."));
|
|
4315
|
+
}
|
|
3398
4316
|
console.log("");
|
|
3399
|
-
const
|
|
3400
|
-
|
|
4317
|
+
const tgMask = isReconfig && config.channels.telegram.botToken ? ` [${maskKey(config.channels.telegram.botToken)}]` : "";
|
|
4318
|
+
const telegramToken = await ask(chalk6.white(` Telegram Bot Token${tgMask}: `));
|
|
4319
|
+
if (isReconfig && telegramToken.toLowerCase() === "none") {
|
|
4320
|
+
config.channels.telegram.enabled = false;
|
|
4321
|
+
config.channels.telegram.botToken = "";
|
|
4322
|
+
} else if (telegramToken) {
|
|
3401
4323
|
config.channels.telegram.botToken = telegramToken;
|
|
3402
4324
|
config.channels.telegram.enabled = true;
|
|
3403
4325
|
}
|
|
3404
4326
|
hr();
|
|
4327
|
+
console.log("");
|
|
4328
|
+
console.log(chalk6.bold.white(" Token Budget"));
|
|
4329
|
+
console.log("");
|
|
4330
|
+
const budgetPrompt = isReconfig ? chalk6.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `) : chalk6.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `);
|
|
4331
|
+
const budgetStr = await ask(budgetPrompt);
|
|
4332
|
+
if (budgetStr) {
|
|
4333
|
+
const budget = parseInt(budgetStr.replace(/,/g, ""), 10);
|
|
4334
|
+
if (!isNaN(budget) && budget > 0) {
|
|
4335
|
+
config.tokens.dailyBudget = budget;
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
hr();
|
|
3405
4339
|
saveConfig(config);
|
|
3406
4340
|
const home = getMercuryHome();
|
|
3407
4341
|
console.log("");
|
|
3408
|
-
console.log(
|
|
3409
|
-
console.log(
|
|
3410
|
-
console.log(
|
|
3411
|
-
console.log(
|
|
3412
|
-
console.log(
|
|
4342
|
+
console.log(chalk6.green(` \u2713 Config saved to ${home}/mercury.yaml`));
|
|
4343
|
+
console.log(chalk6.green(` \u2713 Soul files seeded in ${home}/soul/`));
|
|
4344
|
+
console.log(chalk6.green(` \u2713 Memory stored in ${home}/memory/`));
|
|
4345
|
+
console.log(chalk6.green(` \u2713 Permissions seeded in ${home}/permissions.yaml`));
|
|
4346
|
+
console.log(chalk6.green(` \u2713 Skills directory ready in ${home}/skills/`));
|
|
3413
4347
|
console.log("");
|
|
3414
|
-
console.log(
|
|
3415
|
-
console.log(
|
|
4348
|
+
console.log(chalk6.cyan(` ${config.identity.name} is ready. Run \`mercury start\` to chat.`));
|
|
4349
|
+
console.log(chalk6.dim(" mercury.cosmicstack.org"));
|
|
3416
4350
|
console.log("");
|
|
3417
4351
|
}
|
|
3418
|
-
|
|
4352
|
+
function autoDaemonize() {
|
|
4353
|
+
const daemon = getDaemonStatus();
|
|
4354
|
+
if (daemon.running) {
|
|
4355
|
+
return;
|
|
4356
|
+
}
|
|
4357
|
+
console.log(chalk6.dim(" Setting up background mode..."));
|
|
4358
|
+
try {
|
|
4359
|
+
if (!isServiceInstalled()) {
|
|
4360
|
+
installService();
|
|
4361
|
+
}
|
|
4362
|
+
} catch {
|
|
4363
|
+
console.log(chalk6.dim(" Service install skipped (can run `mercury service install` later)."));
|
|
4364
|
+
}
|
|
4365
|
+
const ok = tryAutoDaemonize();
|
|
4366
|
+
if (ok) {
|
|
4367
|
+
const status = getDaemonStatus();
|
|
4368
|
+
console.log(chalk6.green(` \u2713 Mercury is running in background (PID: ${status.pid})`));
|
|
4369
|
+
console.log(chalk6.green(" \u2713 Auto-starts on login. Auto-restarts on crash."));
|
|
4370
|
+
console.log(chalk6.dim(" Use `mercury stop` to stop. `mercury restart` to restart."));
|
|
4371
|
+
} else {
|
|
4372
|
+
console.log(chalk6.dim(" Background mode not available. Run `mercury up` to set it up."));
|
|
4373
|
+
}
|
|
4374
|
+
console.log("");
|
|
4375
|
+
}
|
|
4376
|
+
async function runAgent(isDaemon = false) {
|
|
3419
4377
|
let config = loadConfig();
|
|
3420
4378
|
config = ensureCreatorField(config);
|
|
3421
4379
|
const name = config.identity.name;
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
4380
|
+
if (!isDaemon) {
|
|
4381
|
+
banner();
|
|
4382
|
+
console.log(chalk6.white(` ${name} is waking up...`));
|
|
4383
|
+
console.log("");
|
|
4384
|
+
} else {
|
|
4385
|
+
logger.info(`${name} is waking up (daemon mode)...`);
|
|
4386
|
+
}
|
|
3425
4387
|
const tokenBudget = new TokenBudget(config);
|
|
3426
4388
|
const providers = new ProviderRegistry(config);
|
|
3427
4389
|
if (!providers.hasProviders()) {
|
|
3428
|
-
|
|
4390
|
+
if (isDaemon) {
|
|
4391
|
+
logger.error("No LLM providers available. Run `mercury doctor` to configure API keys.");
|
|
4392
|
+
return;
|
|
4393
|
+
}
|
|
4394
|
+
console.log(chalk6.red(" No LLM providers available. Run `mercury doctor` to configure API keys."));
|
|
3429
4395
|
process.exit(1);
|
|
3430
4396
|
}
|
|
3431
4397
|
const available = providers.listAvailable();
|
|
3432
|
-
|
|
4398
|
+
if (!isDaemon) {
|
|
4399
|
+
console.log(chalk6.dim(` Providers: ${available.join(", ")}`));
|
|
4400
|
+
} else {
|
|
4401
|
+
logger.info({ providers: available }, "Providers loaded");
|
|
4402
|
+
}
|
|
3433
4403
|
const skillLoader = new SkillLoader();
|
|
3434
4404
|
const skills = skillLoader.discover();
|
|
3435
|
-
|
|
4405
|
+
if (!isDaemon) {
|
|
4406
|
+
console.log(chalk6.dim(` Skills: ${skills.length > 0 ? skills.map((s) => s.name).join(", ") : "none installed"}`));
|
|
4407
|
+
}
|
|
3436
4408
|
const scheduler = new Scheduler(config);
|
|
3437
4409
|
const identity = new Identity();
|
|
3438
4410
|
const shortTerm = new ShortTermMemory(config);
|
|
@@ -3440,6 +4412,13 @@ async function runAgent() {
|
|
|
3440
4412
|
const episodic = new EpisodicMemory(config);
|
|
3441
4413
|
const channels = new ChannelRegistry(config);
|
|
3442
4414
|
const capabilities = new CapabilityRegistry(skillLoader, scheduler, tokenBudget);
|
|
4415
|
+
capabilities.setChatCommandContext({
|
|
4416
|
+
toolNames: () => capabilities.getToolNames(),
|
|
4417
|
+
skillNames: () => skills.map((s) => s.name),
|
|
4418
|
+
config: () => config,
|
|
4419
|
+
tokenBudget: () => tokenBudget,
|
|
4420
|
+
manual: () => getManual()
|
|
4421
|
+
});
|
|
3443
4422
|
capabilities.setSendFileHandler(async (filePath) => {
|
|
3444
4423
|
const msg = channels.getActiveChannels().includes("telegram") ? channels.get("telegram") : channels.get("cli");
|
|
3445
4424
|
if (msg) {
|
|
@@ -3469,21 +4448,29 @@ async function runAgent() {
|
|
|
3469
4448
|
}
|
|
3470
4449
|
const activeCh = channels.getActiveChannels();
|
|
3471
4450
|
const toolNames = capabilities.getToolNames();
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
4451
|
+
if (!isDaemon) {
|
|
4452
|
+
console.log(chalk6.dim(` Channels: ${activeCh.join(", ")}`));
|
|
4453
|
+
console.log(chalk6.dim(` Tools: ${toolNames.join(", ")}`));
|
|
4454
|
+
console.log(chalk6.dim(` Permissions: ${getMercuryHome()}/permissions.yaml`));
|
|
4455
|
+
console.log(chalk6.dim(` Schedules: ${getMercuryHome()}/schedules.yaml`));
|
|
4456
|
+
if (config.identity.creator) {
|
|
4457
|
+
console.log(chalk6.dim(` Creator: ${config.identity.creator}`));
|
|
4458
|
+
}
|
|
4459
|
+
hr();
|
|
4460
|
+
console.log("");
|
|
4461
|
+
console.log(chalk6.green(` ${name} is live. Type a message and press Enter.`));
|
|
4462
|
+
console.log(chalk6.dim(" Ctrl+C to exit \xB7 /help for commands"));
|
|
4463
|
+
console.log("");
|
|
4464
|
+
} else {
|
|
4465
|
+
logger.info({ channels: activeCh, tools: toolNames }, "Mercury is live (daemon mode)");
|
|
3478
4466
|
}
|
|
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
4467
|
const shutdown = async () => {
|
|
3485
|
-
|
|
3486
|
-
|
|
4468
|
+
if (!isDaemon) {
|
|
4469
|
+
console.log("");
|
|
4470
|
+
console.log(chalk6.dim(` ${name} is shutting down...`));
|
|
4471
|
+
} else {
|
|
4472
|
+
logger.info("Mercury is shutting down (daemon mode)");
|
|
4473
|
+
}
|
|
3487
4474
|
await agent.shutdown();
|
|
3488
4475
|
process.exit(0);
|
|
3489
4476
|
};
|
|
@@ -3491,41 +4478,105 @@ async function runAgent() {
|
|
|
3491
4478
|
process.on("SIGTERM", shutdown);
|
|
3492
4479
|
}
|
|
3493
4480
|
var program = new Command();
|
|
3494
|
-
program.name("mercury").description("Mercury \u2014
|
|
4481
|
+
program.name("mercury").description("Mercury \u2014 Soul-driven AI agent with permission-hardened tools, token budgets, and multi-channel access.").version("0.2.1").option("-v, --verbose", "Show debug logs").action(async () => {
|
|
3495
4482
|
if (!isSetupComplete()) {
|
|
3496
|
-
await
|
|
4483
|
+
await configure();
|
|
4484
|
+
autoDaemonize();
|
|
3497
4485
|
return;
|
|
3498
4486
|
}
|
|
3499
4487
|
await runAgent();
|
|
3500
4488
|
});
|
|
3501
|
-
program.command("start").description("Start Mercury agent").option("-v, --verbose", "Show debug logs").action(async () => {
|
|
4489
|
+
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) => {
|
|
4490
|
+
if (opts.daemon) {
|
|
4491
|
+
await runWithWatchdog(() => runAgent(true));
|
|
4492
|
+
return;
|
|
4493
|
+
}
|
|
4494
|
+
if (opts.detached) {
|
|
4495
|
+
startBackground();
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
3502
4498
|
if (!isSetupComplete()) {
|
|
3503
|
-
await
|
|
4499
|
+
await configure();
|
|
3504
4500
|
return;
|
|
3505
4501
|
}
|
|
3506
4502
|
await runAgent();
|
|
3507
4503
|
});
|
|
3508
|
-
program.command("
|
|
3509
|
-
|
|
4504
|
+
program.command("stop").description("Stop a background Mercury process").action(() => {
|
|
4505
|
+
stopDaemon();
|
|
4506
|
+
});
|
|
4507
|
+
program.command("restart").description("Restart a background Mercury process").action(() => {
|
|
4508
|
+
restartDaemon();
|
|
4509
|
+
});
|
|
4510
|
+
program.command("up").description("Ensure Mercury is running persistently \u2014 installs service if needed, starts daemon").action(async () => {
|
|
4511
|
+
if (!isSetupComplete()) {
|
|
4512
|
+
await configure();
|
|
4513
|
+
}
|
|
4514
|
+
const daemon = getDaemonStatus();
|
|
4515
|
+
if (daemon.running && daemon.pid) {
|
|
4516
|
+
console.log("");
|
|
4517
|
+
console.log(chalk6.green(` Mercury is already running (PID: ${daemon.pid})`));
|
|
4518
|
+
console.log(chalk6.dim(` Logs: ${daemon.logPath}`));
|
|
4519
|
+
console.log("");
|
|
4520
|
+
return;
|
|
4521
|
+
}
|
|
4522
|
+
if (!isServiceInstalled()) {
|
|
4523
|
+
console.log("");
|
|
4524
|
+
console.log(chalk6.cyan(" Installing Mercury as a system service..."));
|
|
4525
|
+
installService();
|
|
4526
|
+
}
|
|
4527
|
+
console.log(chalk6.cyan(" Starting Mercury in background..."));
|
|
4528
|
+
startBackground();
|
|
4529
|
+
});
|
|
4530
|
+
program.command("logs").description("Show recent daemon logs").action(() => {
|
|
4531
|
+
showLogs();
|
|
4532
|
+
});
|
|
4533
|
+
program.command("setup").description("Re-run the setup wizard (reconfigure)").action(async () => {
|
|
4534
|
+
if (isSetupComplete()) {
|
|
4535
|
+
await configure(loadConfig());
|
|
4536
|
+
} else {
|
|
4537
|
+
await configure();
|
|
4538
|
+
}
|
|
4539
|
+
});
|
|
4540
|
+
program.command("doctor").description("Reconfigure Mercury \u2014 change keys, name, settings (Enter to keep current)").action(async () => {
|
|
4541
|
+
if (isSetupComplete()) {
|
|
4542
|
+
await configure(loadConfig());
|
|
4543
|
+
} else {
|
|
4544
|
+
await configure();
|
|
4545
|
+
}
|
|
3510
4546
|
});
|
|
3511
|
-
program.command("status").description("Show current configuration").action(() => {
|
|
4547
|
+
program.command("status").description("Show current configuration and daemon status").action(() => {
|
|
3512
4548
|
const config = loadConfig();
|
|
3513
4549
|
const home = getMercuryHome();
|
|
3514
4550
|
const skillLoader = new SkillLoader();
|
|
3515
4551
|
const skills = skillLoader.discover();
|
|
4552
|
+
const daemon = getDaemonStatus();
|
|
3516
4553
|
banner();
|
|
3517
|
-
console.log(` Name: ${
|
|
3518
|
-
console.log(` Owner: ${
|
|
4554
|
+
console.log(` Name: ${chalk6.cyan(config.identity.name)}`);
|
|
4555
|
+
console.log(` Owner: ${chalk6.white(config.identity.owner || "(not set)")}`);
|
|
3519
4556
|
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(`
|
|
4557
|
+
console.log(` Creator: ${chalk6.white(config.identity.creator)}`);
|
|
4558
|
+
}
|
|
4559
|
+
console.log(` Provider: ${chalk6.white(config.providers.default)}`);
|
|
4560
|
+
console.log(` Telegram: ${config.channels.telegram.enabled ? chalk6.green("enabled") : chalk6.dim("disabled")}`);
|
|
4561
|
+
console.log(` Skills: ${skills.length > 0 ? chalk6.green(skills.map((s) => s.name).join(", ")) : chalk6.dim("none")}`);
|
|
4562
|
+
console.log(` Budget: ${chalk6.white(config.tokens.dailyBudget.toLocaleString())} tokens/day`);
|
|
4563
|
+
console.log(` Setup: ${isSetupComplete() ? chalk6.green("complete") : chalk6.red("not done")}`);
|
|
4564
|
+
console.log(` Daemon: ${daemon.running ? chalk6.green(`running (PID: ${daemon.pid})`) : chalk6.dim("not running")}`);
|
|
4565
|
+
console.log(` Home: ${chalk6.dim(home)}`);
|
|
3528
4566
|
console.log("");
|
|
3529
4567
|
});
|
|
4568
|
+
program.command("help").description("Show capabilities and commands manual").action(() => {
|
|
4569
|
+
console.log(getManual());
|
|
4570
|
+
});
|
|
4571
|
+
var serviceCmd = program.command("service").description("Manage Mercury as a system service (auto-start, crash recovery)");
|
|
4572
|
+
serviceCmd.command("install").description("Install Mercury as a system service (auto-start on boot)").action(() => {
|
|
4573
|
+
installService();
|
|
4574
|
+
});
|
|
4575
|
+
serviceCmd.command("uninstall").description("Uninstall the system service").action(() => {
|
|
4576
|
+
uninstallService();
|
|
4577
|
+
});
|
|
4578
|
+
serviceCmd.command("status").description("Show system service status").action(() => {
|
|
4579
|
+
showServiceStatus();
|
|
4580
|
+
});
|
|
3530
4581
|
program.parse();
|
|
3531
4582
|
//# sourceMappingURL=index.js.map
|