@cosmicstack/mercury-agent 0.3.4 → 0.4.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 +3 -0
- package/dist/index.js +783 -284
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as readFileSync12, writeFileSync as writeFileSync13, existsSync as
|
|
4
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync13, existsSync as existsSync18 } from "fs";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { dirname as dirname3, join as join11 } from "path";
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -45,7 +45,7 @@ function getDefaultConfig() {
|
|
|
45
45
|
creator: getEnv("MERCURY_CREATOR", "")
|
|
46
46
|
},
|
|
47
47
|
providers: {
|
|
48
|
-
default: getEnv("DEFAULT_PROVIDER", "
|
|
48
|
+
default: getEnv("DEFAULT_PROVIDER", "deepseek"),
|
|
49
49
|
openai: {
|
|
50
50
|
name: "openai",
|
|
51
51
|
apiKey: getEnv("OPENAI_API_KEY", ""),
|
|
@@ -66,6 +66,27 @@ function getDefaultConfig() {
|
|
|
66
66
|
baseUrl: getEnv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1"),
|
|
67
67
|
model: getEnv("DEEPSEEK_MODEL", "deepseek-chat"),
|
|
68
68
|
enabled: getEnvBool("DEEPSEEK_ENABLED", true)
|
|
69
|
+
},
|
|
70
|
+
grok: {
|
|
71
|
+
name: "grok",
|
|
72
|
+
apiKey: getEnv("GROK_API_KEY", ""),
|
|
73
|
+
baseUrl: getEnv("GROK_BASE_URL", "https://api.x.ai/v1"),
|
|
74
|
+
model: getEnv("GROK_MODEL", "grok-4"),
|
|
75
|
+
enabled: getEnvBool("GROK_ENABLED", true)
|
|
76
|
+
},
|
|
77
|
+
ollamaCloud: {
|
|
78
|
+
name: "ollamaCloud",
|
|
79
|
+
apiKey: getEnv("OLLAMA_CLOUD_API_KEY", ""),
|
|
80
|
+
baseUrl: getEnv("OLLAMA_CLOUD_BASE_URL", "https://ollama.com/api"),
|
|
81
|
+
model: getEnv("OLLAMA_CLOUD_MODEL", "gpt-oss:120b"),
|
|
82
|
+
enabled: getEnvBool("OLLAMA_CLOUD_ENABLED", true)
|
|
83
|
+
},
|
|
84
|
+
ollamaLocal: {
|
|
85
|
+
name: "ollamaLocal",
|
|
86
|
+
apiKey: "",
|
|
87
|
+
baseUrl: getEnv("OLLAMA_LOCAL_BASE_URL", "http://127.0.0.1:11434/api"),
|
|
88
|
+
model: getEnv("OLLAMA_LOCAL_MODEL", "gpt-oss:20b"),
|
|
89
|
+
enabled: getEnvBool("OLLAMA_LOCAL_ENABLED", false)
|
|
69
90
|
}
|
|
70
91
|
},
|
|
71
92
|
channels: {
|
|
@@ -140,6 +161,25 @@ function deepMerge(target, source) {
|
|
|
140
161
|
}
|
|
141
162
|
return result;
|
|
142
163
|
}
|
|
164
|
+
function isProviderConfigured(provider) {
|
|
165
|
+
if (!provider.enabled) return false;
|
|
166
|
+
if (provider.name === "ollamaLocal") {
|
|
167
|
+
return provider.baseUrl.length > 0 && provider.model.length > 0;
|
|
168
|
+
}
|
|
169
|
+
return provider.apiKey.length > 0;
|
|
170
|
+
}
|
|
171
|
+
function setTelegramPairing(config, userId, chatId, username) {
|
|
172
|
+
config.channels.telegram.pairedUserId = userId;
|
|
173
|
+
config.channels.telegram.pairedChatId = chatId;
|
|
174
|
+
config.channels.telegram.pairedUsername = username || void 0;
|
|
175
|
+
return config;
|
|
176
|
+
}
|
|
177
|
+
function clearTelegramPairing(config) {
|
|
178
|
+
delete config.channels.telegram.pairedUserId;
|
|
179
|
+
delete config.channels.telegram.pairedChatId;
|
|
180
|
+
delete config.channels.telegram.pairedUsername;
|
|
181
|
+
return config;
|
|
182
|
+
}
|
|
143
183
|
|
|
144
184
|
// src/utils/logger.ts
|
|
145
185
|
import pino from "pino";
|
|
@@ -573,6 +613,42 @@ var AnthropicProvider = class extends BaseProvider {
|
|
|
573
613
|
}
|
|
574
614
|
};
|
|
575
615
|
|
|
616
|
+
// src/providers/ollama.ts
|
|
617
|
+
import { createOllama } from "ollama-ai-provider";
|
|
618
|
+
var OllamaProvider = class extends BaseProvider {
|
|
619
|
+
name;
|
|
620
|
+
model;
|
|
621
|
+
client;
|
|
622
|
+
modelInstance;
|
|
623
|
+
constructor(config) {
|
|
624
|
+
super(config);
|
|
625
|
+
this.name = config.name;
|
|
626
|
+
this.model = config.model;
|
|
627
|
+
const headers = config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : void 0;
|
|
628
|
+
this.client = createOllama({
|
|
629
|
+
baseURL: config.baseUrl,
|
|
630
|
+
headers
|
|
631
|
+
});
|
|
632
|
+
this.modelInstance = this.client(config.model);
|
|
633
|
+
}
|
|
634
|
+
async generateText(_prompt, _systemPrompt) {
|
|
635
|
+
throw new Error("Use getModelInstance() with the AI SDK agent loop");
|
|
636
|
+
}
|
|
637
|
+
async *streamText(_prompt, _systemPrompt) {
|
|
638
|
+
throw new Error("Use getModelInstance() with the AI SDK agent loop");
|
|
639
|
+
}
|
|
640
|
+
isAvailable() {
|
|
641
|
+
if (!this.config.enabled) return false;
|
|
642
|
+
if (this.name === "ollamaLocal") {
|
|
643
|
+
return this.config.baseUrl.length > 0 && this.config.model.length > 0;
|
|
644
|
+
}
|
|
645
|
+
return this.config.apiKey.length > 0;
|
|
646
|
+
}
|
|
647
|
+
getModelInstance() {
|
|
648
|
+
return this.modelInstance;
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
576
652
|
// src/providers/registry.ts
|
|
577
653
|
var ProviderRegistry = class {
|
|
578
654
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -581,14 +657,24 @@ var ProviderRegistry = class {
|
|
|
581
657
|
constructor(config) {
|
|
582
658
|
this.defaultName = config.providers.default;
|
|
583
659
|
const entries = [
|
|
660
|
+
config.providers.deepseek,
|
|
584
661
|
config.providers.openai,
|
|
585
662
|
config.providers.anthropic,
|
|
586
|
-
config.providers.
|
|
663
|
+
config.providers.grok,
|
|
664
|
+
config.providers.ollamaCloud,
|
|
665
|
+
config.providers.ollamaLocal
|
|
587
666
|
];
|
|
588
667
|
for (const pc of entries) {
|
|
589
|
-
if (!pc
|
|
668
|
+
if (!isProviderConfigured(pc)) continue;
|
|
590
669
|
try {
|
|
591
|
-
|
|
670
|
+
let provider;
|
|
671
|
+
if (pc.name === "anthropic") {
|
|
672
|
+
provider = new AnthropicProvider(pc);
|
|
673
|
+
} else if (pc.name === "ollamaCloud" || pc.name === "ollamaLocal") {
|
|
674
|
+
provider = new OllamaProvider(pc);
|
|
675
|
+
} else {
|
|
676
|
+
provider = new OpenAICompatProvider(pc);
|
|
677
|
+
}
|
|
592
678
|
this.providers.set(pc.name, provider);
|
|
593
679
|
logger.info({ provider: pc.name, model: pc.model }, "Provider registered");
|
|
594
680
|
} catch (err) {
|
|
@@ -1190,11 +1276,13 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
|
|
|
1190
1276
|
if (cmd === "/status") {
|
|
1191
1277
|
const config = ctx.config();
|
|
1192
1278
|
const budget = ctx.tokenBudget();
|
|
1279
|
+
const telegramPairing = config.channels.telegram.pairedUserId != null ? `paired to user ${config.channels.telegram.pairedUserId}${config.channels.telegram.pairedUsername ? ` (@${config.channels.telegram.pairedUsername})` : ""}` : "unpaired";
|
|
1193
1280
|
const lines = [
|
|
1194
1281
|
`**${config.identity.name}** \u2014 Status`,
|
|
1195
1282
|
`Owner: ${config.identity.owner || "(not set)"}`,
|
|
1196
1283
|
`Provider: ${config.providers.default}`,
|
|
1197
1284
|
`Telegram: ${config.channels.telegram.enabled ? "enabled" : "disabled"}`,
|
|
1285
|
+
`Telegram pairing: ${telegramPairing}`,
|
|
1198
1286
|
`Budget: ${budget.getStatusText()}`,
|
|
1199
1287
|
`Skills: ${ctx.skillNames().length > 0 ? ctx.skillNames().join(", ") : "none"}`
|
|
1200
1288
|
];
|
|
@@ -1727,16 +1815,16 @@ var CLIChannel = class extends BaseChannel {
|
|
|
1727
1815
|
}
|
|
1728
1816
|
}
|
|
1729
1817
|
async prompt(question) {
|
|
1730
|
-
return new Promise((
|
|
1731
|
-
this.rl?.question(question, (answer) =>
|
|
1818
|
+
return new Promise((resolve13) => {
|
|
1819
|
+
this.rl?.question(question, (answer) => resolve13(answer.trim()));
|
|
1732
1820
|
});
|
|
1733
1821
|
}
|
|
1734
1822
|
async askPermission(prompt) {
|
|
1735
|
-
return new Promise((
|
|
1823
|
+
return new Promise((resolve13) => {
|
|
1736
1824
|
console.log("");
|
|
1737
1825
|
console.log(chalk2.yellow(` \u26A0 ${prompt}`));
|
|
1738
1826
|
this.rl?.question(chalk2.yellow(" > "), (answer) => {
|
|
1739
|
-
|
|
1827
|
+
resolve13(answer.trim());
|
|
1740
1828
|
});
|
|
1741
1829
|
});
|
|
1742
1830
|
}
|
|
@@ -1752,6 +1840,7 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1752
1840
|
constructor(config) {
|
|
1753
1841
|
super();
|
|
1754
1842
|
this.config = config;
|
|
1843
|
+
this.ownerChatId = config.channels.telegram.pairedChatId ?? null;
|
|
1755
1844
|
}
|
|
1756
1845
|
config;
|
|
1757
1846
|
type = "telegram";
|
|
@@ -1773,9 +1862,33 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1773
1862
|
bot.api.config.use(autoRetry());
|
|
1774
1863
|
bot.on("message:text", async (ctx) => {
|
|
1775
1864
|
const chatId = ctx.chat.id;
|
|
1776
|
-
|
|
1865
|
+
const userId = ctx.from?.id;
|
|
1866
|
+
const text = ctx.message.text?.trim() || "";
|
|
1867
|
+
if (!userId) return;
|
|
1868
|
+
if (ctx.chat.type !== "private") {
|
|
1869
|
+
await this.sendDirectMessage(chatId, "This bot is only available in private one-to-one chats.");
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (!this.isPaired()) {
|
|
1873
|
+
await this.handleUnpairedMessage(userId, chatId, text, ctx.from?.username);
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (!this.isAuthorizedUser(userId)) {
|
|
1877
|
+
await this.sendDirectMessage(chatId, "This bot is not available to you.");
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1777
1880
|
this.ownerChatId = chatId;
|
|
1778
1881
|
logger.info({ chatId, text: ctx.message.text?.slice(0, 50) }, "Telegram message received");
|
|
1882
|
+
const command = text.toLowerCase();
|
|
1883
|
+
if (command === "/start" || command === "/pair") {
|
|
1884
|
+
await this.sendDirectMessage(chatId, this.getPairingStatusMessage());
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
if (command === "/unpair") {
|
|
1888
|
+
this.unpair();
|
|
1889
|
+
await this.sendDirectMessage(chatId, "Telegram pairing removed. Send /start to pair this Mercury instance again.");
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1779
1892
|
const msg = {
|
|
1780
1893
|
id: ctx.message.message_id.toString(),
|
|
1781
1894
|
channelId: `telegram:${chatId}`,
|
|
@@ -1815,6 +1928,8 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1815
1928
|
async registerCommands() {
|
|
1816
1929
|
if (!this.bot) return;
|
|
1817
1930
|
const commands = [
|
|
1931
|
+
{ command: "start", description: "Pair this Telegram account to Mercury" },
|
|
1932
|
+
{ command: "pair", description: "Pair this Telegram account to Mercury" },
|
|
1818
1933
|
{ command: "help", description: "Show capabilities and commands manual" },
|
|
1819
1934
|
{ command: "status", description: "Show agent config, budget, and uptime" },
|
|
1820
1935
|
{ command: "tools", description: "List all loaded tools" },
|
|
@@ -1823,7 +1938,8 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1823
1938
|
{ command: "budget_override", description: "Override budget for one request" },
|
|
1824
1939
|
{ command: "budget_reset", description: "Reset token usage to zero" },
|
|
1825
1940
|
{ command: "budget_set", description: "Set new daily token budget" },
|
|
1826
|
-
{ command: "stream", description: "Toggle text streaming on/off" }
|
|
1941
|
+
{ command: "stream", description: "Toggle text streaming on/off" },
|
|
1942
|
+
{ command: "unpair", description: "Remove Telegram pairing for this Mercury instance" }
|
|
1827
1943
|
];
|
|
1828
1944
|
try {
|
|
1829
1945
|
await this.bot.api.setMyCommands(commands);
|
|
@@ -1999,15 +2115,15 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1999
2115
|
reply_markup: keyboard
|
|
2000
2116
|
});
|
|
2001
2117
|
}
|
|
2002
|
-
return new Promise((
|
|
2003
|
-
this.pendingApprovals.set(`${id}:yes`, () =>
|
|
2004
|
-
this.pendingApprovals.set(`${id}:always`, () =>
|
|
2005
|
-
this.pendingApprovals.set(`${id}:no`, () =>
|
|
2118
|
+
return new Promise((resolve13) => {
|
|
2119
|
+
this.pendingApprovals.set(`${id}:yes`, () => resolve13("yes"));
|
|
2120
|
+
this.pendingApprovals.set(`${id}:always`, () => resolve13("always"));
|
|
2121
|
+
this.pendingApprovals.set(`${id}:no`, () => resolve13("no"));
|
|
2006
2122
|
setTimeout(() => {
|
|
2007
2123
|
this.pendingApprovals.delete(`${id}:yes`);
|
|
2008
2124
|
this.pendingApprovals.delete(`${id}:always`);
|
|
2009
2125
|
this.pendingApprovals.delete(`${id}:no`);
|
|
2010
|
-
|
|
2126
|
+
resolve13("no");
|
|
2011
2127
|
}, 12e4);
|
|
2012
2128
|
});
|
|
2013
2129
|
}
|
|
@@ -2044,19 +2160,57 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2044
2160
|
return [".mp4", ".mov", ".avi", ".mkv", ".webm"].includes(ext);
|
|
2045
2161
|
}
|
|
2046
2162
|
parseChatId(targetId) {
|
|
2047
|
-
if (!targetId) return this.ownerChatId;
|
|
2163
|
+
if (!targetId) return this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null;
|
|
2048
2164
|
if (targetId.startsWith("telegram:")) {
|
|
2049
2165
|
const raw = Number(targetId.split(":")[1]);
|
|
2050
|
-
return isNaN(raw) ? this.ownerChatId : raw;
|
|
2166
|
+
return isNaN(raw) ? this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null : raw;
|
|
2051
2167
|
}
|
|
2052
|
-
if (targetId === "notification") return this.ownerChatId;
|
|
2168
|
+
if (targetId === "notification") return this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null;
|
|
2053
2169
|
const num = Number(targetId);
|
|
2054
|
-
return isNaN(num) ? this.ownerChatId : num;
|
|
2170
|
+
return isNaN(num) ? this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null : num;
|
|
2055
2171
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2172
|
+
isPaired() {
|
|
2173
|
+
return typeof this.config.channels.telegram.pairedUserId === "number";
|
|
2174
|
+
}
|
|
2175
|
+
isAuthorizedUser(userId) {
|
|
2176
|
+
return this.config.channels.telegram.pairedUserId === userId;
|
|
2177
|
+
}
|
|
2178
|
+
async handleUnpairedMessage(userId, chatId, text, username) {
|
|
2179
|
+
const command = text.toLowerCase();
|
|
2180
|
+
if (command === "/start" || command === "/pair") {
|
|
2181
|
+
setTelegramPairing(this.config, userId, chatId, username);
|
|
2182
|
+
saveConfig(this.config);
|
|
2183
|
+
this.ownerChatId = chatId;
|
|
2184
|
+
logger.info({ chatId, userId, username }, "Telegram paired to owner");
|
|
2185
|
+
await this.sendDirectMessage(chatId, this.getPairingStatusMessage(true));
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
await this.sendDirectMessage(
|
|
2189
|
+
chatId,
|
|
2190
|
+
"This Mercury instance is not paired yet. Send /start to pair this bot to your Telegram account."
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
getPairingStatusMessage(newlyPaired = false) {
|
|
2194
|
+
const username = this.config.channels.telegram.pairedUsername ? ` (@${this.config.channels.telegram.pairedUsername})` : "";
|
|
2195
|
+
const prefix = newlyPaired ? "Telegram paired successfully." : "This Telegram account is already paired.";
|
|
2196
|
+
return `${prefix}
|
|
2197
|
+
|
|
2198
|
+
Owner user ID: ${this.config.channels.telegram.pairedUserId}${username}`;
|
|
2199
|
+
}
|
|
2200
|
+
unpair() {
|
|
2201
|
+
clearTelegramPairing(this.config);
|
|
2202
|
+
saveConfig(this.config);
|
|
2203
|
+
this.ownerChatId = null;
|
|
2204
|
+
logger.info("Telegram pairing cleared");
|
|
2205
|
+
}
|
|
2206
|
+
async sendDirectMessage(chatId, content) {
|
|
2207
|
+
if (!this.bot) return;
|
|
2208
|
+
try {
|
|
2209
|
+
await this.bot.api.sendMessage(chatId, mdToTelegram(content), { parse_mode: "HTML" });
|
|
2210
|
+
} catch {
|
|
2211
|
+
await this.bot.api.sendMessage(chatId, content).catch(() => {
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2060
2214
|
}
|
|
2061
2215
|
};
|
|
2062
2216
|
|
|
@@ -2356,8 +2510,8 @@ var PermissionManager = class {
|
|
|
2356
2510
|
clearElevation() {
|
|
2357
2511
|
this.elevatedCommands.clear();
|
|
2358
2512
|
}
|
|
2359
|
-
isElevated(
|
|
2360
|
-
if (this.elevatedCommands.has(
|
|
2513
|
+
isElevated(tool32) {
|
|
2514
|
+
if (this.elevatedCommands.has(tool32)) return true;
|
|
2361
2515
|
return false;
|
|
2362
2516
|
}
|
|
2363
2517
|
isShellElevated() {
|
|
@@ -2617,15 +2771,15 @@ Allow access?`;
|
|
|
2617
2771
|
import { tool } from "ai";
|
|
2618
2772
|
import { z } from "zod";
|
|
2619
2773
|
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
2620
|
-
import { resolve as resolve3 } from "path";
|
|
2621
|
-
function createReadFileTool(permissions) {
|
|
2774
|
+
import { resolve as resolve3, isAbsolute } from "path";
|
|
2775
|
+
function createReadFileTool(permissions, getCwd) {
|
|
2622
2776
|
return tool({
|
|
2623
2777
|
description: "Read the contents of a file. The path must be within an allowed scope.",
|
|
2624
2778
|
parameters: z.object({
|
|
2625
2779
|
path: z.string().describe("Absolute or relative path to the file")
|
|
2626
2780
|
}),
|
|
2627
2781
|
execute: async ({ path: path3 }) => {
|
|
2628
|
-
const resolved = resolve3(path3);
|
|
2782
|
+
const resolved = isAbsolute(path3) ? resolve3(path3) : resolve3(getCwd(), path3);
|
|
2629
2783
|
const check = await permissions.checkFsAccess(resolved, "read");
|
|
2630
2784
|
if (!check.allowed) {
|
|
2631
2785
|
const parentDir = resolve3(resolved, "..");
|
|
@@ -2654,8 +2808,8 @@ function createReadFileTool(permissions) {
|
|
|
2654
2808
|
import { tool as tool2 } from "ai";
|
|
2655
2809
|
import { z as z2 } from "zod";
|
|
2656
2810
|
import { existsSync as existsSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
2657
|
-
import { resolve as resolve4 } from "path";
|
|
2658
|
-
function createWriteFileTool(permissions) {
|
|
2811
|
+
import { resolve as resolve4, isAbsolute as isAbsolute2 } from "path";
|
|
2812
|
+
function createWriteFileTool(permissions, getCwd) {
|
|
2659
2813
|
return tool2({
|
|
2660
2814
|
description: "Write content to an existing file. The path must be within a writable scope.",
|
|
2661
2815
|
parameters: z2.object({
|
|
@@ -2663,7 +2817,7 @@ function createWriteFileTool(permissions) {
|
|
|
2663
2817
|
content: z2.string().describe("The content to write to the file")
|
|
2664
2818
|
}),
|
|
2665
2819
|
execute: async ({ path: path3, content }) => {
|
|
2666
|
-
const resolved = resolve4(path3);
|
|
2820
|
+
const resolved = isAbsolute2(path3) ? resolve4(path3) : resolve4(getCwd(), path3);
|
|
2667
2821
|
const check = await permissions.checkFsAccess(resolved, "write");
|
|
2668
2822
|
if (!check.allowed) {
|
|
2669
2823
|
const parentDir = resolve4(resolved, "..");
|
|
@@ -2686,8 +2840,8 @@ function createWriteFileTool(permissions) {
|
|
|
2686
2840
|
import { tool as tool3 } from "ai";
|
|
2687
2841
|
import { z as z3 } from "zod";
|
|
2688
2842
|
import { existsSync as existsSync9, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
2689
|
-
import { resolve as resolve5, dirname as dirname2 } from "path";
|
|
2690
|
-
function createCreateFileTool(permissions) {
|
|
2843
|
+
import { resolve as resolve5, dirname as dirname2, isAbsolute as isAbsolute3 } from "path";
|
|
2844
|
+
function createCreateFileTool(permissions, getCwd) {
|
|
2691
2845
|
return tool3({
|
|
2692
2846
|
description: "Create a new file with the given content. Also creates parent directories if needed. The path must be within a writable scope.",
|
|
2693
2847
|
parameters: z3.object({
|
|
@@ -2695,7 +2849,7 @@ function createCreateFileTool(permissions) {
|
|
|
2695
2849
|
content: z3.string().describe("The content of the new file")
|
|
2696
2850
|
}),
|
|
2697
2851
|
execute: async ({ path: path3, content }) => {
|
|
2698
|
-
const resolved = resolve5(path3);
|
|
2852
|
+
const resolved = isAbsolute3(path3) ? resolve5(path3) : resolve5(getCwd(), path3);
|
|
2699
2853
|
const check = await permissions.checkFsAccess(resolved, "write");
|
|
2700
2854
|
if (!check.allowed) {
|
|
2701
2855
|
const parentDir = resolve5(resolved, "..");
|
|
@@ -2722,15 +2876,15 @@ function createCreateFileTool(permissions) {
|
|
|
2722
2876
|
import { tool as tool4 } from "ai";
|
|
2723
2877
|
import { z as z4 } from "zod";
|
|
2724
2878
|
import { existsSync as existsSync10, readdirSync as readdirSync2, statSync } from "fs";
|
|
2725
|
-
import { resolve as resolve6 } from "path";
|
|
2726
|
-
function createListDirTool(permissions) {
|
|
2879
|
+
import { resolve as resolve6, isAbsolute as isAbsolute4 } from "path";
|
|
2880
|
+
function createListDirTool(permissions, getCwd) {
|
|
2727
2881
|
return tool4({
|
|
2728
2882
|
description: "List the contents of a directory. Shows file names, types, and sizes.",
|
|
2729
2883
|
parameters: z4.object({
|
|
2730
2884
|
path: z4.string().describe("Absolute or relative path to the directory")
|
|
2731
2885
|
}),
|
|
2732
2886
|
execute: async ({ path: path3 }) => {
|
|
2733
|
-
const resolved = resolve6(path3);
|
|
2887
|
+
const resolved = isAbsolute4(path3) ? resolve6(path3) : resolve6(getCwd(), path3);
|
|
2734
2888
|
const check = await permissions.checkFsAccess(resolved, "read");
|
|
2735
2889
|
if (!check.allowed) {
|
|
2736
2890
|
return `Error: Permission denied for read access to ${resolved}. Use the approve_scope tool with path="${resolved}" and mode="read" to request access from the user.`;
|
|
@@ -2780,15 +2934,15 @@ function formatSize(bytes) {
|
|
|
2780
2934
|
import { tool as tool5 } from "ai";
|
|
2781
2935
|
import { z as z5 } from "zod";
|
|
2782
2936
|
import { existsSync as existsSync11, unlinkSync as unlinkSync2 } from "fs";
|
|
2783
|
-
import { resolve as resolve7 } from "path";
|
|
2784
|
-
function createDeleteFileTool(permissions) {
|
|
2937
|
+
import { resolve as resolve7, isAbsolute as isAbsolute5 } from "path";
|
|
2938
|
+
function createDeleteFileTool(permissions, getCwd) {
|
|
2785
2939
|
return tool5({
|
|
2786
2940
|
description: "Delete a file. This action cannot be undone. The path must be within a writable scope. Always asks for confirmation.",
|
|
2787
2941
|
parameters: z5.object({
|
|
2788
2942
|
path: z5.string().describe("Absolute or relative path to the file to delete")
|
|
2789
2943
|
}),
|
|
2790
2944
|
execute: async ({ path: path3 }) => {
|
|
2791
|
-
const resolved = resolve7(path3);
|
|
2945
|
+
const resolved = isAbsolute5(path3) ? resolve7(path3) : resolve7(getCwd(), path3);
|
|
2792
2946
|
const check = await permissions.checkFsAccess(resolved, "write");
|
|
2793
2947
|
if (!check.allowed) {
|
|
2794
2948
|
const parentDir = resolve7(resolved, "..");
|
|
@@ -2815,8 +2969,8 @@ function createDeleteFileTool(permissions) {
|
|
|
2815
2969
|
import { tool as tool6 } from "ai";
|
|
2816
2970
|
import { z as z6 } from "zod";
|
|
2817
2971
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
2818
|
-
import { resolve as resolve8 } from "path";
|
|
2819
|
-
function createEditFileTool(permissions) {
|
|
2972
|
+
import { resolve as resolve8, isAbsolute as isAbsolute6 } from "path";
|
|
2973
|
+
function createEditFileTool(permissions, getCwd) {
|
|
2820
2974
|
return tool6({
|
|
2821
2975
|
description: "Edit a file by replacing an exact string match with new content. Use this instead of write_file when you only need to change part of a file. The old_string must match exactly (including whitespace and indentation). Fails if old_string is not found or found multiple times.",
|
|
2822
2976
|
parameters: z6.object({
|
|
@@ -2825,7 +2979,7 @@ function createEditFileTool(permissions) {
|
|
|
2825
2979
|
new_string: z6.string().describe("The text to replace it with")
|
|
2826
2980
|
}),
|
|
2827
2981
|
execute: async ({ path: path3, old_string, new_string }) => {
|
|
2828
|
-
const resolved = resolve8(path3);
|
|
2982
|
+
const resolved = isAbsolute6(path3) ? resolve8(path3) : resolve8(getCwd(), path3);
|
|
2829
2983
|
const fsCheck = await permissions.checkFsAccess(resolved, "write");
|
|
2830
2984
|
if (!fsCheck.allowed) {
|
|
2831
2985
|
const parentDir = resolve8(resolved, "..");
|
|
@@ -2859,15 +3013,15 @@ function createEditFileTool(permissions) {
|
|
|
2859
3013
|
import { tool as tool7 } from "ai";
|
|
2860
3014
|
import { z as z7 } from "zod";
|
|
2861
3015
|
import { existsSync as existsSync12, statSync as statSync2 } from "fs";
|
|
2862
|
-
import { resolve as resolve9, basename } from "path";
|
|
2863
|
-
function createSendFileTool(permissions, sendFile) {
|
|
3016
|
+
import { resolve as resolve9, basename, isAbsolute as isAbsolute7 } from "path";
|
|
3017
|
+
function createSendFileTool(permissions, getCwd, sendFile) {
|
|
2864
3018
|
return tool7({
|
|
2865
3019
|
description: "Send a file to the user. On Telegram the file is uploaded as an attachment. On CLI the file path and size are displayed. The path must be within an allowed read scope.",
|
|
2866
3020
|
parameters: z7.object({
|
|
2867
3021
|
path: z7.string().describe("Absolute or relative path to the file to send")
|
|
2868
3022
|
}),
|
|
2869
3023
|
execute: async ({ path: path3 }) => {
|
|
2870
|
-
const resolved = resolve9(path3);
|
|
3024
|
+
const resolved = isAbsolute7(path3) ? resolve9(path3) : resolve9(getCwd(), path3);
|
|
2871
3025
|
const check = await permissions.checkFsAccess(resolved, "read");
|
|
2872
3026
|
if (!check.allowed) {
|
|
2873
3027
|
const parentDir = resolve9(resolved, "..");
|
|
@@ -2895,19 +3049,43 @@ function createSendFileTool(permissions, sendFile) {
|
|
|
2895
3049
|
});
|
|
2896
3050
|
}
|
|
2897
3051
|
|
|
2898
|
-
// src/capabilities/
|
|
3052
|
+
// src/capabilities/messaging/send-message.ts
|
|
2899
3053
|
import { tool as tool8 } from "ai";
|
|
2900
3054
|
import { z as z8 } from "zod";
|
|
2901
|
-
|
|
2902
|
-
function createApproveScopeTool(permissions) {
|
|
3055
|
+
function createSendMessageTool(sendMessage) {
|
|
2903
3056
|
return tool8({
|
|
2904
|
-
description:
|
|
3057
|
+
description: "Send a message to the paired user through the configured outbound channel. Currently this sends only to the paired Telegram owner. Use this only when the user explicitly asks you to send something to Telegram or asks for scheduled results to be sent there.",
|
|
2905
3058
|
parameters: z8.object({
|
|
2906
|
-
|
|
2907
|
-
|
|
3059
|
+
content: z8.string().describe("The message content to send to the paired Telegram owner")
|
|
3060
|
+
}),
|
|
3061
|
+
execute: async ({ content }) => {
|
|
3062
|
+
const trimmed = content.trim();
|
|
3063
|
+
if (!trimmed) {
|
|
3064
|
+
return "Error: Message content cannot be empty.";
|
|
3065
|
+
}
|
|
3066
|
+
try {
|
|
3067
|
+
await sendMessage(trimmed);
|
|
3068
|
+
return "Message sent to the paired Telegram owner.";
|
|
3069
|
+
} catch (err) {
|
|
3070
|
+
return `Error sending message: ${err.message}`;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// src/capabilities/filesystem/approve-scope.ts
|
|
3077
|
+
import { tool as tool9 } from "ai";
|
|
3078
|
+
import { z as z9 } from "zod";
|
|
3079
|
+
import { resolve as resolve10, isAbsolute as isAbsolute8 } from "path";
|
|
3080
|
+
function createApproveScopeTool(permissions, getCwd) {
|
|
3081
|
+
return tool9({
|
|
3082
|
+
description: 'Request user approval to access a directory outside current scopes. Use this when a file tool returns a permission denied error. The user gets an approval prompt (Allow/Always/Deny buttons on Telegram, yes/always/no on CLI). "Allow" grants session-only access. "Always" persists to disk. After approval, retry the original file operation.',
|
|
3083
|
+
parameters: z9.object({
|
|
3084
|
+
path: z9.string().describe("The directory path to request access to"),
|
|
3085
|
+
mode: z9.enum(["read", "write"]).describe("The access mode needed")
|
|
2908
3086
|
}),
|
|
2909
3087
|
execute: async ({ path: path3, mode }) => {
|
|
2910
|
-
const resolved = resolve10(path3);
|
|
3088
|
+
const resolved = isAbsolute8(path3) ? resolve10(path3) : resolve10(getCwd(), path3);
|
|
2911
3089
|
const result = await permissions.requestScopeExternal(resolved, mode);
|
|
2912
3090
|
if (result.allowed) {
|
|
2913
3091
|
return `Access approved for ${mode} access to ${resolved}. You can now retry the file operation.`;
|
|
@@ -2918,17 +3096,19 @@ function createApproveScopeTool(permissions) {
|
|
|
2918
3096
|
}
|
|
2919
3097
|
|
|
2920
3098
|
// src/capabilities/shell/run-command.ts
|
|
2921
|
-
import { tool as
|
|
2922
|
-
import { z as
|
|
3099
|
+
import { tool as tool10 } from "ai";
|
|
3100
|
+
import { z as z10 } from "zod";
|
|
2923
3101
|
import { execSync } from "child_process";
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3102
|
+
import { resolve as resolve11, isAbsolute as isAbsolute9 } from "path";
|
|
3103
|
+
import { existsSync as existsSync13 } from "fs";
|
|
3104
|
+
function createRunCommandTool(permissions, getCwd, setCwd) {
|
|
3105
|
+
return tool10({
|
|
3106
|
+
description: `Run a shell command in the current working directory. Use the cd tool to change directories first \u2014 cd commands within this tool only affect chained commands (e.g., "cd /path && ls"), not subsequent calls.
|
|
2927
3107
|
Blocked commands (sudo, rm -rf /, etc.) are never executed.
|
|
2928
3108
|
Auto-approved commands (ls, cat, git status, curl, etc.) run without asking.
|
|
2929
|
-
Other commands require user approval
|
|
2930
|
-
parameters:
|
|
2931
|
-
command:
|
|
3109
|
+
Other commands require user approval.`,
|
|
3110
|
+
parameters: z10.object({
|
|
3111
|
+
command: z10.string().describe("The shell command to execute")
|
|
2932
3112
|
}),
|
|
2933
3113
|
execute: async ({ command }) => {
|
|
2934
3114
|
const check = await permissions.checkShellCommand(command);
|
|
@@ -2942,20 +3122,25 @@ Tell the user what this command does and ask for permission. If they approve, tr
|
|
|
2942
3122
|
}
|
|
2943
3123
|
return `Error: ${check.reason}`;
|
|
2944
3124
|
}
|
|
3125
|
+
const cwd = getCwd();
|
|
2945
3126
|
try {
|
|
2946
|
-
logger.info({ cmd: command }, "Executing shell command");
|
|
3127
|
+
logger.info({ cmd: command, cwd }, "Executing shell command");
|
|
2947
3128
|
const result = execSync(command, {
|
|
2948
|
-
cwd
|
|
3129
|
+
cwd,
|
|
2949
3130
|
timeout: 3e4,
|
|
2950
3131
|
maxBuffer: 1024 * 1024,
|
|
2951
3132
|
encoding: "utf-8",
|
|
2952
3133
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2953
3134
|
});
|
|
2954
|
-
const
|
|
2955
|
-
|
|
3135
|
+
const trimmedOutput = result?.trim() || "(no output)";
|
|
3136
|
+
detectCd(command, cwd, setCwd);
|
|
3137
|
+
return trimmedOutput;
|
|
2956
3138
|
} catch (err) {
|
|
2957
3139
|
const stderr = err.stderr?.trim();
|
|
2958
3140
|
const stdout = err.stdout?.trim();
|
|
3141
|
+
if (stdout || stderr) {
|
|
3142
|
+
detectCd(command, cwd, setCwd);
|
|
3143
|
+
}
|
|
2959
3144
|
let msg = `Command exited with code ${err.status || "unknown"}`;
|
|
2960
3145
|
if (stdout) msg += `
|
|
2961
3146
|
Output: ${stdout}`;
|
|
@@ -2966,15 +3151,66 @@ Error: ${stderr}`;
|
|
|
2966
3151
|
}
|
|
2967
3152
|
});
|
|
2968
3153
|
}
|
|
3154
|
+
function detectCd(command, currentCwd, setCwd) {
|
|
3155
|
+
const trimmed = command.trim();
|
|
3156
|
+
const cdOnly = trimmed.match(/^cd\s+(.+)$/);
|
|
3157
|
+
if (cdOnly) {
|
|
3158
|
+
const target = cdOnly[1].replace(/^["']|["']$/g, "").replace(/~/, process.env.HOME || "");
|
|
3159
|
+
const resolved = isAbsolute9(target) ? target : resolve11(currentCwd, target);
|
|
3160
|
+
if (existsSync13(resolved)) {
|
|
3161
|
+
setCwd(resolved);
|
|
3162
|
+
}
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
const cdChain = trimmed.match(/cd\s+(.+?)\s*&&/);
|
|
3166
|
+
if (cdChain) {
|
|
3167
|
+
const target = cdChain[1].replace(/^["']|["']$/g, "").replace(/~/, process.env.HOME || "");
|
|
3168
|
+
const resolved = isAbsolute9(target) ? target : resolve11(currentCwd, target);
|
|
3169
|
+
if (existsSync13(resolved)) {
|
|
3170
|
+
setCwd(resolved);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
// src/capabilities/shell/cd.ts
|
|
3176
|
+
import { tool as tool11 } from "ai";
|
|
3177
|
+
import { z as z11 } from "zod";
|
|
3178
|
+
import { resolve as resolve12, isAbsolute as isAbsolute10 } from "path";
|
|
3179
|
+
import { existsSync as existsSync14, statSync as statSync3 } from "fs";
|
|
3180
|
+
function createCdTool(getCwd, setCwd) {
|
|
3181
|
+
return tool11({
|
|
3182
|
+
description: "Change the current working directory. All subsequent file operations, shell commands, and git operations will use this directory. Use this before running commands in a specific project folder.",
|
|
3183
|
+
parameters: z11.object({
|
|
3184
|
+
path: z11.string().describe("The directory to change to. Can be absolute or relative to the current directory.")
|
|
3185
|
+
}),
|
|
3186
|
+
execute: async ({ path: path3 }) => {
|
|
3187
|
+
const cwd = getCwd();
|
|
3188
|
+
const resolved = isAbsolute10(path3) ? resolve12(path3) : resolve12(cwd, path3);
|
|
3189
|
+
if (!existsSync14(resolved)) {
|
|
3190
|
+
return `Error: Directory not found: ${resolved}`;
|
|
3191
|
+
}
|
|
3192
|
+
try {
|
|
3193
|
+
const stat = statSync3(resolved);
|
|
3194
|
+
if (!stat.isDirectory()) {
|
|
3195
|
+
return `Error: Not a directory: ${resolved}`;
|
|
3196
|
+
}
|
|
3197
|
+
} catch {
|
|
3198
|
+
return `Error: Cannot access: ${resolved}`;
|
|
3199
|
+
}
|
|
3200
|
+
setCwd(resolved);
|
|
3201
|
+
return `Changed directory to ${resolved}`;
|
|
3202
|
+
}
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
2969
3205
|
|
|
2970
3206
|
// src/capabilities/shell/approve-command.ts
|
|
2971
|
-
import { tool as
|
|
2972
|
-
import { z as
|
|
3207
|
+
import { tool as tool12 } from "ai";
|
|
3208
|
+
import { z as z12 } from "zod";
|
|
2973
3209
|
function createApproveCommandTool(permissions) {
|
|
2974
|
-
return
|
|
3210
|
+
return tool12({
|
|
2975
3211
|
description: 'Permanently approve a command type so it runs without asking in the future. Use this when the user says "always" or "always approve" for a command. For example, if the user says "always approve curl", call this with command="curl".',
|
|
2976
|
-
parameters:
|
|
2977
|
-
command:
|
|
3212
|
+
parameters: z12.object({
|
|
3213
|
+
command: z12.string().describe('The base command to permanently approve (e.g. "curl", "docker", "npm")')
|
|
2978
3214
|
}),
|
|
2979
3215
|
execute: async ({ command }) => {
|
|
2980
3216
|
const baseCmd = command.trim().split(/\s+/)[0];
|
|
@@ -2985,15 +3221,15 @@ function createApproveCommandTool(permissions) {
|
|
|
2985
3221
|
}
|
|
2986
3222
|
|
|
2987
3223
|
// src/capabilities/skills/install-skill.ts
|
|
2988
|
-
import { tool as
|
|
2989
|
-
import { z as
|
|
3224
|
+
import { tool as tool13 } from "ai";
|
|
3225
|
+
import { z as z13 } from "zod";
|
|
2990
3226
|
import { parse as parseYaml4 } from "yaml";
|
|
2991
3227
|
function createInstallSkillTool(skillLoader) {
|
|
2992
|
-
return
|
|
3228
|
+
return tool13({
|
|
2993
3229
|
description: "Install a new skill by providing SKILL.md markdown content or a URL. The content must have YAML frontmatter (---) with at least name and description fields.",
|
|
2994
|
-
parameters:
|
|
2995
|
-
content:
|
|
2996
|
-
url:
|
|
3230
|
+
parameters: z13.object({
|
|
3231
|
+
content: z13.string().optional().describe("Raw SKILL.md markdown content with YAML frontmatter"),
|
|
3232
|
+
url: z13.string().optional().describe("URL to fetch a SKILL.md from")
|
|
2997
3233
|
}),
|
|
2998
3234
|
execute: async ({ content, url }) => {
|
|
2999
3235
|
let skillContent;
|
|
@@ -3033,12 +3269,12 @@ function createInstallSkillTool(skillLoader) {
|
|
|
3033
3269
|
}
|
|
3034
3270
|
|
|
3035
3271
|
// src/capabilities/skills/list-skills.ts
|
|
3036
|
-
import { tool as
|
|
3037
|
-
import { z as
|
|
3272
|
+
import { tool as tool14 } from "ai";
|
|
3273
|
+
import { z as z14 } from "zod";
|
|
3038
3274
|
function createListSkillsTool(skillLoader) {
|
|
3039
|
-
return
|
|
3275
|
+
return tool14({
|
|
3040
3276
|
description: "List all installed skills with their names and descriptions.",
|
|
3041
|
-
parameters:
|
|
3277
|
+
parameters: z14.object({}),
|
|
3042
3278
|
execute: async () => {
|
|
3043
3279
|
const skills = skillLoader.getDiscovered();
|
|
3044
3280
|
if (skills.length === 0) {
|
|
@@ -3050,13 +3286,13 @@ function createListSkillsTool(skillLoader) {
|
|
|
3050
3286
|
}
|
|
3051
3287
|
|
|
3052
3288
|
// src/capabilities/skills/use-skill.ts
|
|
3053
|
-
import { tool as
|
|
3054
|
-
import { z as
|
|
3289
|
+
import { tool as tool15 } from "ai";
|
|
3290
|
+
import { z as z15 } from "zod";
|
|
3055
3291
|
function createUseSkillTool(skillLoader, permissions) {
|
|
3056
|
-
return
|
|
3292
|
+
return tool15({
|
|
3057
3293
|
description: "Load and invoke a skill by name. Returns the skill's full instructions which should be followed as guidance for the current task.",
|
|
3058
|
-
parameters:
|
|
3059
|
-
name:
|
|
3294
|
+
parameters: z15.object({
|
|
3295
|
+
name: z15.string().describe("Name of the skill to invoke")
|
|
3060
3296
|
}),
|
|
3061
3297
|
execute: async ({ name }) => {
|
|
3062
3298
|
const skill = skillLoader.load(name);
|
|
@@ -3083,18 +3319,18 @@ Allowed tools: ${skill["allowed-tools"].join(", ")}`;
|
|
|
3083
3319
|
}
|
|
3084
3320
|
|
|
3085
3321
|
// src/capabilities/scheduler/schedule-task.ts
|
|
3086
|
-
import { tool as
|
|
3087
|
-
import { z as
|
|
3322
|
+
import { tool as tool16 } from "ai";
|
|
3323
|
+
import { z as z16 } from "zod";
|
|
3088
3324
|
import cron2 from "node-cron";
|
|
3089
3325
|
function createScheduleTaskTool(scheduler, getContext) {
|
|
3090
|
-
return
|
|
3326
|
+
return tool16({
|
|
3091
3327
|
description: 'Schedule a task. Use "cron" for recurring tasks (e.g. "0 9 * * *" for daily at 9am) or "delay_seconds" for one-shot delayed tasks (e.g. 15 for "remind me in 15 seconds"). Provide exactly one of cron or delay_seconds.',
|
|
3092
|
-
parameters:
|
|
3093
|
-
cron:
|
|
3094
|
-
delay_seconds:
|
|
3095
|
-
description:
|
|
3096
|
-
prompt:
|
|
3097
|
-
skill_name:
|
|
3328
|
+
parameters: z16.object({
|
|
3329
|
+
cron: z16.string().optional().describe('Cron expression for recurring tasks (e.g. "0 9 * * *" for daily at 9am)'),
|
|
3330
|
+
delay_seconds: z16.number().optional().describe('Delay in seconds for one-shot tasks (e.g. 15 for "remind me in 15 seconds")'),
|
|
3331
|
+
description: z16.string().describe("Human-readable description of what this task does"),
|
|
3332
|
+
prompt: z16.string().optional().describe("Prompt to send to the agent when the task fires"),
|
|
3333
|
+
skill_name: z16.string().optional().describe("Name of a skill to invoke when the task fires")
|
|
3098
3334
|
}),
|
|
3099
3335
|
execute: async ({ cron: cronExpr, delay_seconds, description, prompt, skill_name }) => {
|
|
3100
3336
|
if (!cronExpr && !delay_seconds) {
|
|
@@ -3147,12 +3383,12 @@ function createScheduleTaskTool(scheduler, getContext) {
|
|
|
3147
3383
|
}
|
|
3148
3384
|
|
|
3149
3385
|
// src/capabilities/scheduler/list-tasks.ts
|
|
3150
|
-
import { tool as
|
|
3151
|
-
import { z as
|
|
3386
|
+
import { tool as tool17 } from "ai";
|
|
3387
|
+
import { z as z17 } from "zod";
|
|
3152
3388
|
function createListTasksTool(scheduler) {
|
|
3153
|
-
return
|
|
3389
|
+
return tool17({
|
|
3154
3390
|
description: "List all scheduled tasks with their cron expressions and descriptions.",
|
|
3155
|
-
parameters:
|
|
3391
|
+
parameters: z17.object({}),
|
|
3156
3392
|
execute: async () => {
|
|
3157
3393
|
const manifests = scheduler.getManifests();
|
|
3158
3394
|
if (manifests.length === 0) {
|
|
@@ -3167,13 +3403,13 @@ function createListTasksTool(scheduler) {
|
|
|
3167
3403
|
}
|
|
3168
3404
|
|
|
3169
3405
|
// src/capabilities/scheduler/cancel-task.ts
|
|
3170
|
-
import { tool as
|
|
3171
|
-
import { z as
|
|
3406
|
+
import { tool as tool18 } from "ai";
|
|
3407
|
+
import { z as z18 } from "zod";
|
|
3172
3408
|
function createCancelTaskTool(scheduler) {
|
|
3173
|
-
return
|
|
3409
|
+
return tool18({
|
|
3174
3410
|
description: "Cancel and remove a scheduled task by its ID.",
|
|
3175
|
-
parameters:
|
|
3176
|
-
id:
|
|
3411
|
+
parameters: z18.object({
|
|
3412
|
+
id: z18.string().describe("ID of the scheduled task to cancel")
|
|
3177
3413
|
}),
|
|
3178
3414
|
execute: async ({ id }) => {
|
|
3179
3415
|
const manifests = scheduler.getManifests();
|
|
@@ -3189,12 +3425,12 @@ function createCancelTaskTool(scheduler) {
|
|
|
3189
3425
|
}
|
|
3190
3426
|
|
|
3191
3427
|
// src/capabilities/system/budget-status.ts
|
|
3192
|
-
import { tool as
|
|
3193
|
-
import { z as
|
|
3428
|
+
import { tool as tool19 } from "ai";
|
|
3429
|
+
import { z as z19 } from "zod";
|
|
3194
3430
|
function createBudgetStatusTool(tokenBudget) {
|
|
3195
|
-
return
|
|
3431
|
+
return tool19({
|
|
3196
3432
|
description: "Check the current token budget status \u2014 how many tokens have been used today, how many remain, and what percentage is consumed.",
|
|
3197
|
-
parameters:
|
|
3433
|
+
parameters: z19.object({}),
|
|
3198
3434
|
execute: async () => {
|
|
3199
3435
|
return tokenBudget.getStatusText();
|
|
3200
3436
|
}
|
|
@@ -3202,19 +3438,19 @@ function createBudgetStatusTool(tokenBudget) {
|
|
|
3202
3438
|
}
|
|
3203
3439
|
|
|
3204
3440
|
// src/capabilities/git/git-status.ts
|
|
3205
|
-
import { tool as
|
|
3206
|
-
import { z as
|
|
3441
|
+
import { tool as tool20 } from "ai";
|
|
3442
|
+
import { z as z20 } from "zod";
|
|
3207
3443
|
import { execSync as execSync2 } from "child_process";
|
|
3208
|
-
function createGitStatusTool() {
|
|
3209
|
-
return
|
|
3444
|
+
function createGitStatusTool(getCwd) {
|
|
3445
|
+
return tool20({
|
|
3210
3446
|
description: "Show the working tree status. Returns staged, unstaged, and untracked files.",
|
|
3211
|
-
parameters:
|
|
3212
|
-
path:
|
|
3447
|
+
parameters: z20.object({
|
|
3448
|
+
path: z20.string().optional().describe("Path to check (defaults to current directory)")
|
|
3213
3449
|
}),
|
|
3214
3450
|
execute: async ({ path: path3 }) => {
|
|
3215
3451
|
try {
|
|
3216
3452
|
const cmd = path3 ? `git -C "${path3}" status --porcelain` : "git status --porcelain";
|
|
3217
|
-
const result = execSync2(cmd, { encoding: "utf-8", timeout: 1e4 });
|
|
3453
|
+
const result = execSync2(cmd, { encoding: "utf-8", timeout: 1e4, cwd: getCwd() });
|
|
3218
3454
|
if (!result.trim()) return "Working tree clean \u2014 no changes.";
|
|
3219
3455
|
return result.trim();
|
|
3220
3456
|
} catch (err) {
|
|
@@ -3225,22 +3461,22 @@ function createGitStatusTool() {
|
|
|
3225
3461
|
}
|
|
3226
3462
|
|
|
3227
3463
|
// src/capabilities/git/git-diff.ts
|
|
3228
|
-
import { tool as
|
|
3229
|
-
import { z as
|
|
3464
|
+
import { tool as tool21 } from "ai";
|
|
3465
|
+
import { z as z21 } from "zod";
|
|
3230
3466
|
import { execSync as execSync3 } from "child_process";
|
|
3231
|
-
function createGitDiffTool() {
|
|
3232
|
-
return
|
|
3467
|
+
function createGitDiffTool(getCwd) {
|
|
3468
|
+
return tool21({
|
|
3233
3469
|
description: "Show changes between commits, commit and working tree, etc. Shows what has been modified.",
|
|
3234
|
-
parameters:
|
|
3235
|
-
path:
|
|
3236
|
-
staged:
|
|
3470
|
+
parameters: z21.object({
|
|
3471
|
+
path: z21.string().optional().describe("File or directory to diff"),
|
|
3472
|
+
staged: z21.boolean().optional().describe("Show staged changes (cached) instead of unstaged")
|
|
3237
3473
|
}),
|
|
3238
3474
|
execute: async ({ path: path3, staged }) => {
|
|
3239
3475
|
try {
|
|
3240
3476
|
let cmd = "git diff";
|
|
3241
3477
|
if (staged) cmd += " --cached";
|
|
3242
3478
|
if (path3) cmd += ` -- "${path3}"`;
|
|
3243
|
-
const result = execSync3(cmd, { encoding: "utf-8", timeout: 15e3 });
|
|
3479
|
+
const result = execSync3(cmd, { encoding: "utf-8", timeout: 15e3, cwd: getCwd() });
|
|
3244
3480
|
if (!result.trim()) return "No differences found.";
|
|
3245
3481
|
const truncated = result.length > 15e3 ? result.slice(0, 15e3) + "\n... (truncated)" : result;
|
|
3246
3482
|
return truncated;
|
|
@@ -3252,22 +3488,22 @@ function createGitDiffTool() {
|
|
|
3252
3488
|
}
|
|
3253
3489
|
|
|
3254
3490
|
// src/capabilities/git/git-log.ts
|
|
3255
|
-
import { tool as
|
|
3256
|
-
import { z as
|
|
3491
|
+
import { tool as tool22 } from "ai";
|
|
3492
|
+
import { z as z22 } from "zod";
|
|
3257
3493
|
import { execSync as execSync4 } from "child_process";
|
|
3258
|
-
function createGitLogTool() {
|
|
3259
|
-
return
|
|
3494
|
+
function createGitLogTool(getCwd) {
|
|
3495
|
+
return tool22({
|
|
3260
3496
|
description: "Show commit logs. Returns recent commit history with hash, author, date, and message.",
|
|
3261
|
-
parameters:
|
|
3262
|
-
count:
|
|
3263
|
-
path:
|
|
3497
|
+
parameters: z22.object({
|
|
3498
|
+
count: z22.number().optional().describe("Number of commits to show (default 10)"),
|
|
3499
|
+
path: z22.string().optional().describe("File or directory to show log for")
|
|
3264
3500
|
}),
|
|
3265
3501
|
execute: async ({ count, path: path3 }) => {
|
|
3266
3502
|
try {
|
|
3267
3503
|
const n = count ?? 10;
|
|
3268
3504
|
let cmd = `git log --oneline --decorate -${n}`;
|
|
3269
3505
|
if (path3) cmd += ` -- "${path3}"`;
|
|
3270
|
-
const result = execSync4(cmd, { encoding: "utf-8", timeout: 1e4 });
|
|
3506
|
+
const result = execSync4(cmd, { encoding: "utf-8", timeout: 1e4, cwd: getCwd() });
|
|
3271
3507
|
if (!result.trim()) return "No commits found.";
|
|
3272
3508
|
return result.trim();
|
|
3273
3509
|
} catch (err) {
|
|
@@ -3278,19 +3514,19 @@ function createGitLogTool() {
|
|
|
3278
3514
|
}
|
|
3279
3515
|
|
|
3280
3516
|
// src/capabilities/git/git-add.ts
|
|
3281
|
-
import { tool as
|
|
3282
|
-
import { z as
|
|
3517
|
+
import { tool as tool23 } from "ai";
|
|
3518
|
+
import { z as z23 } from "zod";
|
|
3283
3519
|
import { execSync as execSync5 } from "child_process";
|
|
3284
|
-
function createGitAddTool() {
|
|
3285
|
-
return
|
|
3520
|
+
function createGitAddTool(getCwd) {
|
|
3521
|
+
return tool23({
|
|
3286
3522
|
description: "Add file contents to the index (staging area). Prepares files for commit.",
|
|
3287
|
-
parameters:
|
|
3288
|
-
paths:
|
|
3523
|
+
parameters: z23.object({
|
|
3524
|
+
paths: z23.array(z23.string()).describe("File paths to stage")
|
|
3289
3525
|
}),
|
|
3290
3526
|
execute: async ({ paths }) => {
|
|
3291
3527
|
try {
|
|
3292
3528
|
const fileArgs = paths.map((p) => `"${p}"`).join(" ");
|
|
3293
|
-
const result = execSync5(`git add ${fileArgs}`, { encoding: "utf-8", timeout: 1e4 });
|
|
3529
|
+
const result = execSync5(`git add ${fileArgs}`, { encoding: "utf-8", timeout: 1e4, cwd: getCwd() });
|
|
3294
3530
|
return `Staged ${paths.length} file(s): ${paths.join(", ")}`;
|
|
3295
3531
|
} catch (err) {
|
|
3296
3532
|
return `Error: ${err.stderr?.trim() || err.message}`;
|
|
@@ -3300,15 +3536,15 @@ function createGitAddTool() {
|
|
|
3300
3536
|
}
|
|
3301
3537
|
|
|
3302
3538
|
// src/capabilities/git/git-commit.ts
|
|
3303
|
-
import { tool as
|
|
3304
|
-
import { z as
|
|
3539
|
+
import { tool as tool24 } from "ai";
|
|
3540
|
+
import { z as z24 } from "zod";
|
|
3305
3541
|
import { execSync as execSync6 } from "child_process";
|
|
3306
3542
|
var CO_AUTHOR = "Mercury <mercury@cosmicstack.org>";
|
|
3307
|
-
function createGitCommitTool() {
|
|
3308
|
-
return
|
|
3543
|
+
function createGitCommitTool(getCwd) {
|
|
3544
|
+
return tool24({
|
|
3309
3545
|
description: "Record changes to the repository. Creates a new commit with staged changes. Automatically includes a Co-authored-by trailer for attribution.",
|
|
3310
|
-
parameters:
|
|
3311
|
-
message:
|
|
3546
|
+
parameters: z24.object({
|
|
3547
|
+
message: z24.string().describe("Commit message")
|
|
3312
3548
|
}),
|
|
3313
3549
|
execute: async ({ message }) => {
|
|
3314
3550
|
try {
|
|
@@ -3316,7 +3552,7 @@ function createGitCommitTool() {
|
|
|
3316
3552
|
|
|
3317
3553
|
Co-authored-by: ${CO_AUTHOR}`;
|
|
3318
3554
|
const escapedMsg = fullMessage.replace(/"/g, '\\"');
|
|
3319
|
-
const result = execSync6(`git commit -m "${escapedMsg}"`, { encoding: "utf-8", timeout: 1e4 });
|
|
3555
|
+
const result = execSync6(`git commit -m "${escapedMsg}"`, { encoding: "utf-8", timeout: 1e4, cwd: getCwd() });
|
|
3320
3556
|
return result.trim() || "Committed successfully.";
|
|
3321
3557
|
} catch (err) {
|
|
3322
3558
|
const stderr = err.stderr?.trim() || "";
|
|
@@ -3330,15 +3566,15 @@ Co-authored-by: ${CO_AUTHOR}`;
|
|
|
3330
3566
|
}
|
|
3331
3567
|
|
|
3332
3568
|
// src/capabilities/git/git-push.ts
|
|
3333
|
-
import { tool as
|
|
3334
|
-
import { z as
|
|
3569
|
+
import { tool as tool25 } from "ai";
|
|
3570
|
+
import { z as z25 } from "zod";
|
|
3335
3571
|
import { execSync as execSync7 } from "child_process";
|
|
3336
|
-
function createGitPushTool(permissions) {
|
|
3337
|
-
return
|
|
3572
|
+
function createGitPushTool(permissions, getCwd) {
|
|
3573
|
+
return tool25({
|
|
3338
3574
|
description: "Push commits to a remote repository. This modifies a remote and requires approval.",
|
|
3339
|
-
parameters:
|
|
3340
|
-
remote:
|
|
3341
|
-
branch:
|
|
3575
|
+
parameters: z25.object({
|
|
3576
|
+
remote: z25.string().optional().describe("Remote name (default: origin)"),
|
|
3577
|
+
branch: z25.string().optional().describe("Branch name (default: current branch)")
|
|
3342
3578
|
}),
|
|
3343
3579
|
execute: async ({ remote, branch }) => {
|
|
3344
3580
|
const cmd = `git push ${remote || "origin"} ${branch || ""}`.trim();
|
|
@@ -3353,7 +3589,7 @@ Ask the user for permission. If they approve, try again. If they say "always", u
|
|
|
3353
3589
|
return `Error: ${check.reason}`;
|
|
3354
3590
|
}
|
|
3355
3591
|
try {
|
|
3356
|
-
const result = execSync7(cmd, { encoding: "utf-8", timeout: 3e4 });
|
|
3592
|
+
const result = execSync7(cmd, { encoding: "utf-8", timeout: 3e4, cwd: getCwd() });
|
|
3357
3593
|
return result.trim() || "Pushed successfully.";
|
|
3358
3594
|
} catch (err) {
|
|
3359
3595
|
return `Error: ${err.stderr?.trim() || err.message}`;
|
|
@@ -3363,8 +3599,8 @@ Ask the user for permission. If they approve, try again. If they say "always", u
|
|
|
3363
3599
|
}
|
|
3364
3600
|
|
|
3365
3601
|
// src/capabilities/github/create-pr.ts
|
|
3366
|
-
import { tool as
|
|
3367
|
-
import { z as
|
|
3602
|
+
import { tool as tool26 } from "ai";
|
|
3603
|
+
import { z as z26 } from "zod";
|
|
3368
3604
|
|
|
3369
3605
|
// src/utils/github.ts
|
|
3370
3606
|
var GITHUB_API = "https://api.github.com";
|
|
@@ -3419,16 +3655,16 @@ async function githubRequest(path3, options = {}) {
|
|
|
3419
3655
|
|
|
3420
3656
|
// src/capabilities/github/create-pr.ts
|
|
3421
3657
|
function createCreatePrTool() {
|
|
3422
|
-
return
|
|
3658
|
+
return tool26({
|
|
3423
3659
|
description: "Create a pull request on GitHub. Requires GITHUB_TOKEN to be configured.",
|
|
3424
|
-
parameters:
|
|
3425
|
-
owner:
|
|
3426
|
-
repo:
|
|
3427
|
-
title:
|
|
3428
|
-
body:
|
|
3429
|
-
head:
|
|
3430
|
-
base:
|
|
3431
|
-
draft:
|
|
3660
|
+
parameters: z26.object({
|
|
3661
|
+
owner: z26.string().describe("Repository owner (username or org)"),
|
|
3662
|
+
repo: z26.string().describe("Repository name"),
|
|
3663
|
+
title: z26.string().describe("PR title"),
|
|
3664
|
+
body: z26.string().describe("PR description (markdown supported)").default(""),
|
|
3665
|
+
head: z26.string().describe("The branch containing the changes"),
|
|
3666
|
+
base: z26.string().describe("The branch to merge into").default("main"),
|
|
3667
|
+
draft: z26.boolean().describe("Create as draft PR").default(false)
|
|
3432
3668
|
}),
|
|
3433
3669
|
execute: async ({ owner, repo, title, body, head, base, draft }) => {
|
|
3434
3670
|
try {
|
|
@@ -3447,16 +3683,16 @@ ${draft ? "(draft)" : ""} ${result.state}`;
|
|
|
3447
3683
|
}
|
|
3448
3684
|
|
|
3449
3685
|
// src/capabilities/github/review-pr.ts
|
|
3450
|
-
import { tool as
|
|
3451
|
-
import { z as
|
|
3686
|
+
import { tool as tool27 } from "ai";
|
|
3687
|
+
import { z as z27 } from "zod";
|
|
3452
3688
|
function createReviewPrTool() {
|
|
3453
|
-
return
|
|
3689
|
+
return tool27({
|
|
3454
3690
|
description: "Get details of a pull request including the diff. Reviews the PR and returns the title, body, changed files, and diff. Optionally post a review comment.",
|
|
3455
|
-
parameters:
|
|
3456
|
-
owner:
|
|
3457
|
-
repo:
|
|
3458
|
-
number:
|
|
3459
|
-
comment:
|
|
3691
|
+
parameters: z27.object({
|
|
3692
|
+
owner: z27.string().describe("Repository owner (username or org)"),
|
|
3693
|
+
repo: z27.string().describe("Repository name"),
|
|
3694
|
+
number: z27.number().describe("PR number"),
|
|
3695
|
+
comment: z27.string().describe("Review comment to post on the PR (optional)").optional()
|
|
3460
3696
|
}),
|
|
3461
3697
|
execute: async ({ owner, repo, number, comment }) => {
|
|
3462
3698
|
try {
|
|
@@ -3522,17 +3758,17 @@ Failed to post review comment: ${err.message}`;
|
|
|
3522
3758
|
}
|
|
3523
3759
|
|
|
3524
3760
|
// src/capabilities/github/list-issues.ts
|
|
3525
|
-
import { tool as
|
|
3526
|
-
import { z as
|
|
3761
|
+
import { tool as tool28 } from "ai";
|
|
3762
|
+
import { z as z28 } from "zod";
|
|
3527
3763
|
function createListIssuesTool() {
|
|
3528
|
-
return
|
|
3764
|
+
return tool28({
|
|
3529
3765
|
description: "List GitHub issues for a repository. Requires GITHUB_TOKEN.",
|
|
3530
|
-
parameters:
|
|
3531
|
-
owner:
|
|
3532
|
-
repo:
|
|
3533
|
-
state:
|
|
3534
|
-
labels:
|
|
3535
|
-
limit:
|
|
3766
|
+
parameters: z28.object({
|
|
3767
|
+
owner: z28.string().describe("Repository owner (username or org)"),
|
|
3768
|
+
repo: z28.string().describe("Repository name"),
|
|
3769
|
+
state: z28.enum(["open", "closed", "all"]).describe("Filter by issue state").default("open"),
|
|
3770
|
+
labels: z28.string().describe("Comma-separated label names to filter by (optional)").optional(),
|
|
3771
|
+
limit: z28.number().describe("Maximum number of issues to return").default(10)
|
|
3536
3772
|
}),
|
|
3537
3773
|
execute: async ({ owner, repo, state, labels, limit }) => {
|
|
3538
3774
|
try {
|
|
@@ -3560,17 +3796,17 @@ ${lines.join("\n")}`;
|
|
|
3560
3796
|
}
|
|
3561
3797
|
|
|
3562
3798
|
// src/capabilities/github/create-issue.ts
|
|
3563
|
-
import { tool as
|
|
3564
|
-
import { z as
|
|
3799
|
+
import { tool as tool29 } from "ai";
|
|
3800
|
+
import { z as z29 } from "zod";
|
|
3565
3801
|
function createCreateIssueTool() {
|
|
3566
|
-
return
|
|
3802
|
+
return tool29({
|
|
3567
3803
|
description: "Create a new GitHub issue in a repository. Requires GITHUB_TOKEN.",
|
|
3568
|
-
parameters:
|
|
3569
|
-
owner:
|
|
3570
|
-
repo:
|
|
3571
|
-
title:
|
|
3572
|
-
body:
|
|
3573
|
-
labels:
|
|
3804
|
+
parameters: z29.object({
|
|
3805
|
+
owner: z29.string().describe("Repository owner (username or org)"),
|
|
3806
|
+
repo: z29.string().describe("Repository name"),
|
|
3807
|
+
title: z29.string().describe("Issue title"),
|
|
3808
|
+
body: z29.string().describe("Issue description (markdown supported)").default(""),
|
|
3809
|
+
labels: z29.array(z29.string()).describe("Label names to apply").optional()
|
|
3574
3810
|
}),
|
|
3575
3811
|
execute: async ({ owner, repo, title, body, labels }) => {
|
|
3576
3812
|
try {
|
|
@@ -3590,15 +3826,15 @@ function createCreateIssueTool() {
|
|
|
3590
3826
|
}
|
|
3591
3827
|
|
|
3592
3828
|
// src/capabilities/github/github-api.ts
|
|
3593
|
-
import { tool as
|
|
3594
|
-
import { z as
|
|
3829
|
+
import { tool as tool30 } from "ai";
|
|
3830
|
+
import { z as z30 } from "zod";
|
|
3595
3831
|
function createGithubApiTool() {
|
|
3596
|
-
return
|
|
3832
|
+
return tool30({
|
|
3597
3833
|
description: "Make a raw request to the GitHub API. Use this for any GitHub operation not covered by other tools. GET requests (read-only) are always allowed. Write operations (POST, PUT, PATCH, DELETE) will ask the user for approval via the permission system.",
|
|
3598
|
-
parameters:
|
|
3599
|
-
path:
|
|
3600
|
-
method:
|
|
3601
|
-
body:
|
|
3834
|
+
parameters: z30.object({
|
|
3835
|
+
path: z30.string().describe("Full API path (e.g., /repos/owner/repo/issues or /user)"),
|
|
3836
|
+
method: z30.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).describe("HTTP method").default("GET"),
|
|
3837
|
+
body: z30.string().describe("JSON body for write requests (as a JSON string)").optional()
|
|
3602
3838
|
}),
|
|
3603
3839
|
execute: async ({ path: path3, method, body }) => {
|
|
3604
3840
|
try {
|
|
@@ -3625,8 +3861,8 @@ function createGithubApiTool() {
|
|
|
3625
3861
|
}
|
|
3626
3862
|
|
|
3627
3863
|
// src/capabilities/web/fetch-url.ts
|
|
3628
|
-
import { tool as
|
|
3629
|
-
import { z as
|
|
3864
|
+
import { tool as tool31 } from "ai";
|
|
3865
|
+
import { z as z31 } from "zod";
|
|
3630
3866
|
var MAX_CONTENT_LENGTH = 15e3;
|
|
3631
3867
|
function stripHtml(html) {
|
|
3632
3868
|
let text = html;
|
|
@@ -3659,11 +3895,11 @@ function stripHtml(html) {
|
|
|
3659
3895
|
return text;
|
|
3660
3896
|
}
|
|
3661
3897
|
function createFetchUrlTool() {
|
|
3662
|
-
return
|
|
3898
|
+
return tool31({
|
|
3663
3899
|
description: "Fetch a URL and return its content as text. Strips HTML to readable markdown-like format. Useful for reading documentation, APIs, or web pages.",
|
|
3664
|
-
parameters:
|
|
3665
|
-
url:
|
|
3666
|
-
format:
|
|
3900
|
+
parameters: z31.object({
|
|
3901
|
+
url: z31.string().describe("The URL to fetch"),
|
|
3902
|
+
format: z31.enum(["text", "markdown"]).optional().describe("Output format (default: markdown)")
|
|
3667
3903
|
}),
|
|
3668
3904
|
execute: async ({ url, format }) => {
|
|
3669
3905
|
const outputFormat = format ?? "markdown";
|
|
@@ -3715,9 +3951,11 @@ var CapabilityRegistry = class {
|
|
|
3715
3951
|
scheduler;
|
|
3716
3952
|
tokenBudget;
|
|
3717
3953
|
sendFileHandler;
|
|
3954
|
+
sendMessageHandler;
|
|
3718
3955
|
currentChannelId = "cli";
|
|
3719
3956
|
currentChannelType = "cli";
|
|
3720
3957
|
chatCommandContext;
|
|
3958
|
+
currentCwd = process.cwd();
|
|
3721
3959
|
constructor(skillLoader, scheduler, tokenBudget) {
|
|
3722
3960
|
this.permissions = new PermissionManager();
|
|
3723
3961
|
this.skillLoader = skillLoader;
|
|
@@ -3737,26 +3975,40 @@ var CapabilityRegistry = class {
|
|
|
3737
3975
|
getChannelContext() {
|
|
3738
3976
|
return { channelId: this.currentChannelId, channelType: this.currentChannelType };
|
|
3739
3977
|
}
|
|
3978
|
+
getCwd() {
|
|
3979
|
+
return this.currentCwd;
|
|
3980
|
+
}
|
|
3981
|
+
setCwd(dir) {
|
|
3982
|
+
this.currentCwd = dir;
|
|
3983
|
+
}
|
|
3740
3984
|
setSendFileHandler(handler) {
|
|
3741
3985
|
this.sendFileHandler = handler;
|
|
3742
3986
|
}
|
|
3987
|
+
setSendMessageHandler(handler) {
|
|
3988
|
+
this.sendMessageHandler = handler;
|
|
3989
|
+
}
|
|
3743
3990
|
registerAll() {
|
|
3744
3991
|
const manifest = this.permissions.getManifest();
|
|
3745
3992
|
if (manifest.capabilities.filesystem.enabled) {
|
|
3746
|
-
this.tools.read_file = createReadFileTool(this.permissions);
|
|
3747
|
-
this.tools.write_file = createWriteFileTool(this.permissions);
|
|
3748
|
-
this.tools.create_file = createCreateFileTool(this.permissions);
|
|
3749
|
-
this.tools.list_dir = createListDirTool(this.permissions);
|
|
3750
|
-
this.tools.delete_file = createDeleteFileTool(this.permissions);
|
|
3751
|
-
this.tools.edit_file = createEditFileTool(this.permissions);
|
|
3993
|
+
this.tools.read_file = createReadFileTool(this.permissions, () => this.getCwd());
|
|
3994
|
+
this.tools.write_file = createWriteFileTool(this.permissions, () => this.getCwd());
|
|
3995
|
+
this.tools.create_file = createCreateFileTool(this.permissions, () => this.getCwd());
|
|
3996
|
+
this.tools.list_dir = createListDirTool(this.permissions, () => this.getCwd());
|
|
3997
|
+
this.tools.delete_file = createDeleteFileTool(this.permissions, () => this.getCwd());
|
|
3998
|
+
this.tools.edit_file = createEditFileTool(this.permissions, () => this.getCwd());
|
|
3752
3999
|
if (this.sendFileHandler) {
|
|
3753
|
-
this.tools.send_file = createSendFileTool(this.permissions, this.sendFileHandler);
|
|
4000
|
+
this.tools.send_file = createSendFileTool(this.permissions, () => this.getCwd(), this.sendFileHandler);
|
|
3754
4001
|
}
|
|
3755
|
-
this.tools.approve_scope = createApproveScopeTool(this.permissions);
|
|
4002
|
+
this.tools.approve_scope = createApproveScopeTool(this.permissions, () => this.getCwd());
|
|
3756
4003
|
logger.info("Filesystem tools registered");
|
|
3757
4004
|
}
|
|
4005
|
+
if (this.sendMessageHandler) {
|
|
4006
|
+
this.tools.send_message = createSendMessageTool(this.sendMessageHandler);
|
|
4007
|
+
logger.info("Messaging tool registered");
|
|
4008
|
+
}
|
|
3758
4009
|
if (manifest.capabilities.shell.enabled) {
|
|
3759
|
-
this.tools.run_command = createRunCommandTool(this.permissions);
|
|
4010
|
+
this.tools.run_command = createRunCommandTool(this.permissions, () => this.getCwd(), (dir) => this.setCwd(dir));
|
|
4011
|
+
this.tools.cd = createCdTool(() => this.getCwd(), (dir) => this.setCwd(dir));
|
|
3760
4012
|
this.tools.approve_command = createApproveCommandTool(this.permissions);
|
|
3761
4013
|
logger.info("Shell tools registered");
|
|
3762
4014
|
}
|
|
@@ -3777,12 +4029,12 @@ var CapabilityRegistry = class {
|
|
|
3777
4029
|
logger.info("Budget tool registered");
|
|
3778
4030
|
}
|
|
3779
4031
|
if (manifest.capabilities.git?.enabled) {
|
|
3780
|
-
this.tools.git_status = createGitStatusTool();
|
|
3781
|
-
this.tools.git_diff = createGitDiffTool();
|
|
3782
|
-
this.tools.git_log = createGitLogTool();
|
|
3783
|
-
this.tools.git_add = createGitAddTool();
|
|
3784
|
-
this.tools.git_commit = createGitCommitTool();
|
|
3785
|
-
this.tools.git_push = createGitPushTool(this.permissions);
|
|
4032
|
+
this.tools.git_status = createGitStatusTool(() => this.getCwd());
|
|
4033
|
+
this.tools.git_diff = createGitDiffTool(() => this.getCwd());
|
|
4034
|
+
this.tools.git_log = createGitLogTool(() => this.getCwd());
|
|
4035
|
+
this.tools.git_add = createGitAddTool(() => this.getCwd());
|
|
4036
|
+
this.tools.git_commit = createGitCommitTool(() => this.getCwd());
|
|
4037
|
+
this.tools.git_push = createGitPushTool(this.permissions, () => this.getCwd());
|
|
3786
4038
|
logger.info("Git tools registered");
|
|
3787
4039
|
}
|
|
3788
4040
|
if (isGitHubConfigured()) {
|
|
@@ -3808,7 +4060,7 @@ var CapabilityRegistry = class {
|
|
|
3808
4060
|
};
|
|
3809
4061
|
|
|
3810
4062
|
// src/skills/loader.ts
|
|
3811
|
-
import { existsSync as
|
|
4063
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10, readdirSync as readdirSync3, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
|
|
3812
4064
|
import { join as join8 } from "path";
|
|
3813
4065
|
import { parse as parseYaml5 } from "yaml";
|
|
3814
4066
|
var SKILL_FILE = "SKILL.md";
|
|
@@ -3838,7 +4090,7 @@ var SkillLoader = class {
|
|
|
3838
4090
|
discover() {
|
|
3839
4091
|
this.discovered.clear();
|
|
3840
4092
|
this.loaded.clear();
|
|
3841
|
-
if (!
|
|
4093
|
+
if (!existsSync15(this.skillsDir)) {
|
|
3842
4094
|
mkdirSync8(this.skillsDir, { recursive: true });
|
|
3843
4095
|
this.seedTemplate();
|
|
3844
4096
|
return [];
|
|
@@ -3847,7 +4099,7 @@ var SkillLoader = class {
|
|
|
3847
4099
|
for (const entry of entries) {
|
|
3848
4100
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
3849
4101
|
const skillPath = join8(this.skillsDir, entry.name, SKILL_FILE);
|
|
3850
|
-
if (!
|
|
4102
|
+
if (!existsSync15(skillPath)) continue;
|
|
3851
4103
|
try {
|
|
3852
4104
|
const raw = readFileSync10(skillPath, "utf-8");
|
|
3853
4105
|
const parsed = parseSkillMd(raw);
|
|
@@ -3869,7 +4121,7 @@ var SkillLoader = class {
|
|
|
3869
4121
|
for (const entry of readdirSync3(this.skillsDir, { withFileTypes: true })) {
|
|
3870
4122
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
3871
4123
|
const skillPath = join8(this.skillsDir, entry.name, SKILL_FILE);
|
|
3872
|
-
if (!
|
|
4124
|
+
if (!existsSync15(skillPath)) continue;
|
|
3873
4125
|
try {
|
|
3874
4126
|
const raw = readFileSync10(skillPath, "utf-8");
|
|
3875
4127
|
const parsed = parseSkillMd(raw);
|
|
@@ -3878,8 +4130,8 @@ var SkillLoader = class {
|
|
|
3878
4130
|
const skill = {
|
|
3879
4131
|
...parsed.meta,
|
|
3880
4132
|
instructions: parsed.instructions,
|
|
3881
|
-
scriptsDir:
|
|
3882
|
-
referencesDir:
|
|
4133
|
+
scriptsDir: existsSync15(join8(skillDir, "scripts")) ? join8(skillDir, "scripts") : void 0,
|
|
4134
|
+
referencesDir: existsSync15(join8(skillDir, "references")) ? join8(skillDir, "references") : void 0
|
|
3883
4135
|
};
|
|
3884
4136
|
this.loaded.set(name, skill);
|
|
3885
4137
|
return skill;
|
|
@@ -3900,7 +4152,7 @@ var SkillLoader = class {
|
|
|
3900
4152
|
}
|
|
3901
4153
|
saveSkill(name, content) {
|
|
3902
4154
|
const skillDir = join8(this.skillsDir, name);
|
|
3903
|
-
if (!
|
|
4155
|
+
if (!existsSync15(skillDir)) {
|
|
3904
4156
|
mkdirSync8(skillDir, { recursive: true });
|
|
3905
4157
|
}
|
|
3906
4158
|
writeFileSync10(join8(skillDir, SKILL_FILE), content, "utf-8");
|
|
@@ -3963,6 +4215,7 @@ function getManual() {
|
|
|
3963
4215
|
["edit_file", "Replace specific text in a file", "path, old_string, new_string"],
|
|
3964
4216
|
["list_dir", "List directory contents", "path"],
|
|
3965
4217
|
["delete_file", "Delete a file", "path"],
|
|
4218
|
+
["send_message", "Send a message to the paired Telegram owner", "content"],
|
|
3966
4219
|
["run_command", "Execute a shell command", "command"],
|
|
3967
4220
|
["approve_command", "Permanently approve a command type", 'command (e.g. "curl")'],
|
|
3968
4221
|
["fetch_url", "Fetch a URL and return content", "url, format? (text/markdown)"],
|
|
@@ -3999,6 +4252,7 @@ function getManual() {
|
|
|
3999
4252
|
["mercury doctor", "Reconfigure settings (Enter keeps current)"],
|
|
4000
4253
|
["mercury setup", "Re-run the setup wizard"],
|
|
4001
4254
|
["mercury status", "Show config and daemon status"],
|
|
4255
|
+
["mercury telegram unpair", "Clear the paired Telegram owner"],
|
|
4002
4256
|
["mercury help", "Show this manual"],
|
|
4003
4257
|
["mercury service install", "Install as system service (auto-start)"],
|
|
4004
4258
|
["mercury service uninstall", "Uninstall system service"],
|
|
@@ -4013,13 +4267,16 @@ function getManual() {
|
|
|
4013
4267
|
sections.push(chalk3.dim(" Type these during a conversation (no API calls)."));
|
|
4014
4268
|
sections.push("");
|
|
4015
4269
|
const chat = [
|
|
4270
|
+
["/start", "Pair this Telegram account to Mercury"],
|
|
4271
|
+
["/pair", "Pair this Telegram account to Mercury"],
|
|
4016
4272
|
["/help", "Show this manual"],
|
|
4017
4273
|
["/status", "Show config and budget info"],
|
|
4018
4274
|
["/tools", "List currently loaded tools"],
|
|
4019
4275
|
["/skills", "List installed skills"],
|
|
4020
4276
|
["/stream", "Toggle text streaming on/off (Telegram)"],
|
|
4021
4277
|
["/stream on", "Enable streaming (live text updates)"],
|
|
4022
|
-
["/stream off", "Disable streaming (single message)"]
|
|
4278
|
+
["/stream off", "Disable streaming (single message)"],
|
|
4279
|
+
["/unpair", "Remove Telegram pairing for this Mercury instance"]
|
|
4023
4280
|
];
|
|
4024
4281
|
for (const [cmd, desc] of chat) {
|
|
4025
4282
|
sections.push(` ${chalk3.white(cmd.padEnd(16))} ${desc}`);
|
|
@@ -4082,7 +4339,7 @@ function getManual() {
|
|
|
4082
4339
|
|
|
4083
4340
|
// src/cli/daemon.ts
|
|
4084
4341
|
import { spawn } from "child_process";
|
|
4085
|
-
import { existsSync as
|
|
4342
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, mkdirSync as mkdirSync9, openSync } from "fs";
|
|
4086
4343
|
import { join as join9 } from "path";
|
|
4087
4344
|
import process2 from "process";
|
|
4088
4345
|
import chalk4 from "chalk";
|
|
@@ -4096,7 +4353,7 @@ function logPath() {
|
|
|
4096
4353
|
}
|
|
4097
4354
|
function readPid() {
|
|
4098
4355
|
const path3 = pidPath();
|
|
4099
|
-
if (!
|
|
4356
|
+
if (!existsSync16(path3)) return null;
|
|
4100
4357
|
try {
|
|
4101
4358
|
const pid = parseInt(readFileSync11(path3, "utf-8").trim(), 10);
|
|
4102
4359
|
if (isNaN(pid)) return null;
|
|
@@ -4133,7 +4390,7 @@ function startBackground() {
|
|
|
4133
4390
|
}
|
|
4134
4391
|
}
|
|
4135
4392
|
const home = getMercuryHome();
|
|
4136
|
-
if (!
|
|
4393
|
+
if (!existsSync16(home)) {
|
|
4137
4394
|
mkdirSync9(home, { recursive: true });
|
|
4138
4395
|
}
|
|
4139
4396
|
const logFile = logPath();
|
|
@@ -4214,7 +4471,7 @@ function restartDaemon() {
|
|
|
4214
4471
|
}
|
|
4215
4472
|
function showLogs() {
|
|
4216
4473
|
const logFile = logPath();
|
|
4217
|
-
if (!
|
|
4474
|
+
if (!existsSync16(logFile)) {
|
|
4218
4475
|
console.log(chalk4.dim(" No daemon log file found."));
|
|
4219
4476
|
console.log("");
|
|
4220
4477
|
return;
|
|
@@ -4236,7 +4493,7 @@ function tryAutoDaemonize() {
|
|
|
4236
4493
|
}
|
|
4237
4494
|
}
|
|
4238
4495
|
const home = getMercuryHome();
|
|
4239
|
-
if (!
|
|
4496
|
+
if (!existsSync16(home)) {
|
|
4240
4497
|
mkdirSync9(home, { recursive: true });
|
|
4241
4498
|
}
|
|
4242
4499
|
const logFile = logPath();
|
|
@@ -4260,7 +4517,7 @@ function tryAutoDaemonize() {
|
|
|
4260
4517
|
}
|
|
4261
4518
|
|
|
4262
4519
|
// src/cli/service.ts
|
|
4263
|
-
import { existsSync as
|
|
4520
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4 } from "fs";
|
|
4264
4521
|
import { join as join10 } from "path";
|
|
4265
4522
|
import { homedir as homedir3 } from "os";
|
|
4266
4523
|
import chalk5 from "chalk";
|
|
@@ -4270,9 +4527,9 @@ var WIN_TASK_NAME = "MercuryAgent";
|
|
|
4270
4527
|
function isServiceInstalled() {
|
|
4271
4528
|
const platform = process.platform;
|
|
4272
4529
|
if (platform === "darwin") {
|
|
4273
|
-
return
|
|
4530
|
+
return existsSync17(join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist"));
|
|
4274
4531
|
} else if (platform === "linux") {
|
|
4275
|
-
return
|
|
4532
|
+
return existsSync17(join10(homedir3(), ".config", "systemd", "user", "mercury.service"));
|
|
4276
4533
|
} else if (platform === "win32") {
|
|
4277
4534
|
try {
|
|
4278
4535
|
execSync8(`schtasks /query /tn "${WIN_TASK_NAME}"`, { stdio: "pipe", shell: "cmd.exe" });
|
|
@@ -4328,7 +4585,7 @@ function showServiceStatus() {
|
|
|
4328
4585
|
function installMac() {
|
|
4329
4586
|
const plistDir = join10(homedir3(), "Library", "LaunchAgents");
|
|
4330
4587
|
const plistPath = join10(plistDir, "com.cosmicstack.mercury.plist");
|
|
4331
|
-
if (!
|
|
4588
|
+
if (!existsSync17(plistDir)) {
|
|
4332
4589
|
mkdirSync10(plistDir, { recursive: true });
|
|
4333
4590
|
}
|
|
4334
4591
|
const nodeBin = getNodeBinPath();
|
|
@@ -4389,7 +4646,7 @@ function installMac() {
|
|
|
4389
4646
|
}
|
|
4390
4647
|
function uninstallMac() {
|
|
4391
4648
|
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
4392
|
-
if (!
|
|
4649
|
+
if (!existsSync17(plistPath)) {
|
|
4393
4650
|
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4394
4651
|
console.log("");
|
|
4395
4652
|
process.exit(0);
|
|
@@ -4410,7 +4667,7 @@ function uninstallMac() {
|
|
|
4410
4667
|
}
|
|
4411
4668
|
function showMacStatus() {
|
|
4412
4669
|
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
4413
|
-
if (!
|
|
4670
|
+
if (!existsSync17(plistPath)) {
|
|
4414
4671
|
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4415
4672
|
console.log(chalk5.dim(" Run `mercury service install` to set it up."));
|
|
4416
4673
|
console.log("");
|
|
@@ -4428,7 +4685,7 @@ function showMacStatus() {
|
|
|
4428
4685
|
}
|
|
4429
4686
|
function installLinux() {
|
|
4430
4687
|
const systemdDir = join10(homedir3(), ".config", "systemd", "user");
|
|
4431
|
-
if (!
|
|
4688
|
+
if (!existsSync17(systemdDir)) {
|
|
4432
4689
|
mkdirSync10(systemdDir, { recursive: true });
|
|
4433
4690
|
}
|
|
4434
4691
|
const servicePath = join10(systemdDir, "mercury.service");
|
|
@@ -4480,7 +4737,7 @@ WantedBy=default.target`;
|
|
|
4480
4737
|
}
|
|
4481
4738
|
function uninstallLinux() {
|
|
4482
4739
|
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4483
|
-
if (!
|
|
4740
|
+
if (!existsSync17(servicePath)) {
|
|
4484
4741
|
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4485
4742
|
console.log("");
|
|
4486
4743
|
process.exit(0);
|
|
@@ -4506,7 +4763,7 @@ function uninstallLinux() {
|
|
|
4506
4763
|
}
|
|
4507
4764
|
function showLinuxStatus() {
|
|
4508
4765
|
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4509
|
-
if (!
|
|
4766
|
+
if (!existsSync17(servicePath)) {
|
|
4510
4767
|
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4511
4768
|
console.log(chalk5.dim(" Run `mercury service install` to set it up."));
|
|
4512
4769
|
console.log("");
|
|
@@ -4605,7 +4862,7 @@ async function runWithWatchdog(agentFn) {
|
|
|
4605
4862
|
await attempt();
|
|
4606
4863
|
}
|
|
4607
4864
|
function sleep(ms) {
|
|
4608
|
-
return new Promise((
|
|
4865
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
4609
4866
|
}
|
|
4610
4867
|
|
|
4611
4868
|
// src/index.ts
|
|
@@ -4644,10 +4901,10 @@ function splashScreen() {
|
|
|
4644
4901
|
}
|
|
4645
4902
|
async function ask(prompt) {
|
|
4646
4903
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
4647
|
-
return new Promise((
|
|
4904
|
+
return new Promise((resolve13) => {
|
|
4648
4905
|
rl.question(prompt, (answer) => {
|
|
4649
4906
|
rl.close();
|
|
4650
|
-
|
|
4907
|
+
resolve13(answer.trim());
|
|
4651
4908
|
});
|
|
4652
4909
|
});
|
|
4653
4910
|
}
|
|
@@ -4656,10 +4913,149 @@ function maskKey(key) {
|
|
|
4656
4913
|
if (key.length <= 8) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
4657
4914
|
return key.slice(0, 4) + "\u2022\u2022\u2022\u2022" + key.slice(-4);
|
|
4658
4915
|
}
|
|
4916
|
+
var PROVIDER_OPTIONS = [
|
|
4917
|
+
{ key: "deepseek", label: "DeepSeek" },
|
|
4918
|
+
{ key: "openai", label: "OpenAI" },
|
|
4919
|
+
{ key: "anthropic", label: "Anthropic" },
|
|
4920
|
+
{ key: "grok", label: "Grok (xAI)" },
|
|
4921
|
+
{ key: "ollamaCloud", label: "Ollama Cloud" },
|
|
4922
|
+
{ key: "ollamaLocal", label: "Ollama Local" }
|
|
4923
|
+
];
|
|
4924
|
+
function getConfiguredProviderNames(config) {
|
|
4925
|
+
return PROVIDER_OPTIONS.map((option) => option.key).filter((key) => isProviderConfigured(config.providers[key]));
|
|
4926
|
+
}
|
|
4927
|
+
function getProviderLabel(name) {
|
|
4928
|
+
return PROVIDER_OPTIONS.find((option) => option.key === name)?.label || name;
|
|
4929
|
+
}
|
|
4930
|
+
function parseProviderSelection(input) {
|
|
4931
|
+
const values = input.split(/[\s,]+/).map((value) => value.trim()).filter(Boolean);
|
|
4932
|
+
if (values.length === 0) return [];
|
|
4933
|
+
const selected = [];
|
|
4934
|
+
for (const value of values) {
|
|
4935
|
+
const index = parseInt(value, 10);
|
|
4936
|
+
if (isNaN(index) || index < 1 || index > PROVIDER_OPTIONS.length) {
|
|
4937
|
+
return null;
|
|
4938
|
+
}
|
|
4939
|
+
const provider = PROVIDER_OPTIONS[index - 1].key;
|
|
4940
|
+
if (!selected.includes(provider)) {
|
|
4941
|
+
selected.push(provider);
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
return selected;
|
|
4945
|
+
}
|
|
4946
|
+
async function chooseProvidersToConfigure(config, isReconfig) {
|
|
4947
|
+
const configured = getConfiguredProviderNames(config);
|
|
4948
|
+
while (true) {
|
|
4949
|
+
for (let i = 0; i < PROVIDER_OPTIONS.length; i++) {
|
|
4950
|
+
const option = PROVIDER_OPTIONS[i];
|
|
4951
|
+
const status = configured.includes(option.key) ? " (configured)" : "";
|
|
4952
|
+
console.log(chalk6.white(` ${i + 1}. ${option.label}${status}`));
|
|
4953
|
+
}
|
|
4954
|
+
console.log("");
|
|
4955
|
+
const prompt = isReconfig ? chalk6.white(" Choose providers to configure [comma-separated, Enter keeps current]: ") : chalk6.white(" Choose providers to configure [comma-separated, Enter for DeepSeek]: ");
|
|
4956
|
+
const input = await ask(prompt);
|
|
4957
|
+
const parsed = parseProviderSelection(input);
|
|
4958
|
+
if (parsed === null) {
|
|
4959
|
+
console.log(chalk6.red(" Please choose valid provider numbers, like `1` or `1,3,5`."));
|
|
4960
|
+
console.log("");
|
|
4961
|
+
continue;
|
|
4962
|
+
}
|
|
4963
|
+
if (parsed.length > 0) return parsed;
|
|
4964
|
+
if (!isReconfig) return ["deepseek"];
|
|
4965
|
+
return configured.length > 0 ? configured : ["deepseek"];
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
async function chooseDefaultProvider(config) {
|
|
4969
|
+
const configured = getConfiguredProviderNames(config);
|
|
4970
|
+
if (configured.length === 0) {
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
if (configured.length === 1) {
|
|
4974
|
+
config.providers.default = configured[0];
|
|
4975
|
+
console.log(chalk6.dim(` Default provider set to ${getProviderLabel(configured[0])}`));
|
|
4976
|
+
return;
|
|
4977
|
+
}
|
|
4978
|
+
const suggested = configured.includes("deepseek") ? "deepseek" : configured[0];
|
|
4979
|
+
console.log("");
|
|
4980
|
+
console.log(chalk6.bold.white(" Default Provider"));
|
|
4981
|
+
console.log(chalk6.dim(" Select the LLM provider Mercury should use first."));
|
|
4982
|
+
console.log("");
|
|
4983
|
+
for (let i = 0; i < configured.length; i++) {
|
|
4984
|
+
const provider = configured[i];
|
|
4985
|
+
const recommended = provider === suggested ? " (recommended)" : "";
|
|
4986
|
+
const current = provider === config.providers.default ? " (current)" : "";
|
|
4987
|
+
console.log(chalk6.white(` ${i + 1}. ${getProviderLabel(provider)}${recommended}${current}`));
|
|
4988
|
+
}
|
|
4989
|
+
console.log("");
|
|
4990
|
+
while (true) {
|
|
4991
|
+
const choice = await ask(chalk6.white(` Choose [1-${configured.length}] [Enter for ${getProviderLabel(suggested)}]: `));
|
|
4992
|
+
if (!choice) {
|
|
4993
|
+
config.providers.default = suggested;
|
|
4994
|
+
return;
|
|
4995
|
+
}
|
|
4996
|
+
const num = parseInt(choice, 10);
|
|
4997
|
+
if (num >= 1 && num <= configured.length) {
|
|
4998
|
+
config.providers.default = configured[num - 1];
|
|
4999
|
+
return;
|
|
5000
|
+
}
|
|
5001
|
+
console.log(chalk6.red(" Please choose a valid number from the list above."));
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
function looksLikeToken(value, minLength = 20) {
|
|
5005
|
+
return value.length >= minLength && !/\s/.test(value) && /[A-Za-z]/.test(value) && /\d/.test(value);
|
|
5006
|
+
}
|
|
5007
|
+
function validateApiKey(provider, value) {
|
|
5008
|
+
if (provider === "openai") {
|
|
5009
|
+
return /^sk-(proj-|svcacct-)?[A-Za-z0-9_-]{16,}$/i.test(value) ? null : "OpenAI keys must start with `sk-`, `sk-proj-`, or `sk-svcacct-`.";
|
|
5010
|
+
}
|
|
5011
|
+
if (provider === "anthropic") {
|
|
5012
|
+
return /^sk-ant-[A-Za-z0-9_-]{16,}$/i.test(value) ? null : "Anthropic keys must start with `sk-ant-`.";
|
|
5013
|
+
}
|
|
5014
|
+
if (provider === "deepseek") {
|
|
5015
|
+
return /^sk-[A-Za-z0-9_-]{16,}$/i.test(value) ? null : "DeepSeek keys must start with `sk-`.";
|
|
5016
|
+
}
|
|
5017
|
+
if (provider === "grok") {
|
|
5018
|
+
return looksLikeToken(value) ? null : "Grok keys must look like a real API token: long, no spaces, and not plain text.";
|
|
5019
|
+
}
|
|
5020
|
+
if (provider === "ollamaCloud") {
|
|
5021
|
+
return looksLikeToken(value) ? null : "Ollama Cloud keys must look like a real API token: long, no spaces, and not plain text.";
|
|
5022
|
+
}
|
|
5023
|
+
return null;
|
|
5024
|
+
}
|
|
5025
|
+
function validateBaseUrl(value) {
|
|
5026
|
+
try {
|
|
5027
|
+
const parsed = new URL(value);
|
|
5028
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
5029
|
+
return "Base URL must start with http:// or https://.";
|
|
5030
|
+
}
|
|
5031
|
+
return null;
|
|
5032
|
+
} catch {
|
|
5033
|
+
return "Please enter a valid URL.";
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
function validateModelName(value) {
|
|
5037
|
+
if (!value.trim()) return "Model name is required.";
|
|
5038
|
+
if (/\s/.test(value)) return "Model name cannot contain spaces.";
|
|
5039
|
+
return null;
|
|
5040
|
+
}
|
|
5041
|
+
async function promptValidatedValue(prompt, validator, existingValue, options) {
|
|
5042
|
+
while (true) {
|
|
5043
|
+
const value = await ask(prompt);
|
|
5044
|
+
if (!value) {
|
|
5045
|
+
if (existingValue) return existingValue;
|
|
5046
|
+
if (options?.allowSkip) return void 0;
|
|
5047
|
+
console.log(chalk6.red(" A value is required here."));
|
|
5048
|
+
continue;
|
|
5049
|
+
}
|
|
5050
|
+
const error = validator(value);
|
|
5051
|
+
if (!error) return value;
|
|
5052
|
+
console.log(chalk6.red(` ${error}`));
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
4659
5055
|
function appendToEnv(key, value) {
|
|
4660
5056
|
const envPath = join11(getMercuryHome(), ".env");
|
|
4661
5057
|
let envContent = "";
|
|
4662
|
-
if (
|
|
5058
|
+
if (existsSync18(envPath)) {
|
|
4663
5059
|
envContent = readFileSync12(envPath, "utf-8");
|
|
4664
5060
|
}
|
|
4665
5061
|
const lines = envContent.split("\n").filter((l) => !l.startsWith(`${key}=`) && l.trim() !== "");
|
|
@@ -4709,49 +5105,108 @@ async function configure(existingConfig) {
|
|
|
4709
5105
|
console.log("");
|
|
4710
5106
|
console.log(chalk6.bold.white(" LLM Providers"));
|
|
4711
5107
|
if (isReconfig) {
|
|
4712
|
-
console.log(chalk6.dim("
|
|
5108
|
+
console.log(chalk6.dim(" Choose which providers to configure now. Existing values are shown where available."));
|
|
4713
5109
|
} else {
|
|
4714
|
-
console.log(chalk6.dim("
|
|
5110
|
+
console.log(chalk6.dim(" Choose one or more providers. Press Enter to configure DeepSeek by default."));
|
|
4715
5111
|
}
|
|
4716
5112
|
console.log("");
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
if (deepseekKey) {
|
|
4720
|
-
config.providers.deepseek.apiKey = deepseekKey;
|
|
4721
|
-
}
|
|
4722
|
-
const oaiMask = isReconfig && config.providers.openai.apiKey ? ` [${maskKey(config.providers.openai.apiKey)}]` : " (Enter to skip)";
|
|
4723
|
-
const openaiKey = await ask(chalk6.white(` OpenAI API key${oaiMask}: `));
|
|
4724
|
-
if (openaiKey) config.providers.openai.apiKey = openaiKey;
|
|
4725
|
-
const antMask = isReconfig && config.providers.anthropic.apiKey ? ` [${maskKey(config.providers.anthropic.apiKey)}]` : " (Enter to skip)";
|
|
4726
|
-
const anthropicKey = await ask(chalk6.white(` Anthropic API key${antMask}: `));
|
|
4727
|
-
if (anthropicKey) config.providers.anthropic.apiKey = anthropicKey;
|
|
4728
|
-
const hasKey = config.providers.deepseek.apiKey || config.providers.openai.apiKey || config.providers.anthropic.apiKey;
|
|
4729
|
-
if (!hasKey) {
|
|
4730
|
-
console.log(chalk6.red("\n At least one LLM API key is required."));
|
|
4731
|
-
process.exit(1);
|
|
4732
|
-
}
|
|
4733
|
-
const availableProviders = [];
|
|
4734
|
-
if (config.providers.deepseek.apiKey) availableProviders.push("deepseek");
|
|
4735
|
-
if (config.providers.openai.apiKey) availableProviders.push("openai");
|
|
4736
|
-
if (config.providers.anthropic.apiKey) availableProviders.push("anthropic");
|
|
4737
|
-
if (isReconfig && availableProviders.length > 1) {
|
|
4738
|
-
console.log("");
|
|
4739
|
-
console.log(chalk6.bold.white(" Default Provider"));
|
|
4740
|
-
console.log(chalk6.dim(" Select the default LLM provider (the one used first)."));
|
|
5113
|
+
while (true) {
|
|
5114
|
+
const selectedProviders = await chooseProvidersToConfigure(config, isReconfig);
|
|
4741
5115
|
console.log("");
|
|
4742
|
-
for (
|
|
4743
|
-
|
|
4744
|
-
|
|
5116
|
+
for (const provider of selectedProviders) {
|
|
5117
|
+
if (provider === "deepseek") {
|
|
5118
|
+
const mask = isReconfig && config.providers.deepseek.apiKey ? ` [${maskKey(config.providers.deepseek.apiKey)}]` : "";
|
|
5119
|
+
const key = await promptValidatedValue(
|
|
5120
|
+
chalk6.white(` DeepSeek API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
5121
|
+
(value) => validateApiKey("deepseek", value),
|
|
5122
|
+
isReconfig ? config.providers.deepseek.apiKey : void 0,
|
|
5123
|
+
{ allowSkip: true }
|
|
5124
|
+
);
|
|
5125
|
+
if (key) {
|
|
5126
|
+
config.providers.deepseek.apiKey = key;
|
|
5127
|
+
config.providers.deepseek.enabled = true;
|
|
5128
|
+
}
|
|
5129
|
+
continue;
|
|
5130
|
+
}
|
|
5131
|
+
if (provider === "openai") {
|
|
5132
|
+
const mask = isReconfig && config.providers.openai.apiKey ? ` [${maskKey(config.providers.openai.apiKey)}]` : "";
|
|
5133
|
+
const key = await promptValidatedValue(
|
|
5134
|
+
chalk6.white(` OpenAI API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
5135
|
+
(value) => validateApiKey("openai", value),
|
|
5136
|
+
isReconfig ? config.providers.openai.apiKey : void 0,
|
|
5137
|
+
{ allowSkip: true }
|
|
5138
|
+
);
|
|
5139
|
+
if (key) {
|
|
5140
|
+
config.providers.openai.apiKey = key;
|
|
5141
|
+
config.providers.openai.enabled = true;
|
|
5142
|
+
}
|
|
5143
|
+
continue;
|
|
5144
|
+
}
|
|
5145
|
+
if (provider === "anthropic") {
|
|
5146
|
+
const mask = isReconfig && config.providers.anthropic.apiKey ? ` [${maskKey(config.providers.anthropic.apiKey)}]` : "";
|
|
5147
|
+
const key = await promptValidatedValue(
|
|
5148
|
+
chalk6.white(` Anthropic API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
5149
|
+
(value) => validateApiKey("anthropic", value),
|
|
5150
|
+
isReconfig ? config.providers.anthropic.apiKey : void 0,
|
|
5151
|
+
{ allowSkip: true }
|
|
5152
|
+
);
|
|
5153
|
+
if (key) {
|
|
5154
|
+
config.providers.anthropic.apiKey = key;
|
|
5155
|
+
config.providers.anthropic.enabled = true;
|
|
5156
|
+
}
|
|
5157
|
+
continue;
|
|
5158
|
+
}
|
|
5159
|
+
if (provider === "grok") {
|
|
5160
|
+
const mask = isReconfig && config.providers.grok.apiKey ? ` [${maskKey(config.providers.grok.apiKey)}]` : "";
|
|
5161
|
+
const key = await promptValidatedValue(
|
|
5162
|
+
chalk6.white(` Grok API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
5163
|
+
(value) => validateApiKey("grok", value),
|
|
5164
|
+
isReconfig ? config.providers.grok.apiKey : void 0,
|
|
5165
|
+
{ allowSkip: true }
|
|
5166
|
+
);
|
|
5167
|
+
if (key) {
|
|
5168
|
+
config.providers.grok.apiKey = key;
|
|
5169
|
+
config.providers.grok.enabled = true;
|
|
5170
|
+
}
|
|
5171
|
+
continue;
|
|
5172
|
+
}
|
|
5173
|
+
if (provider === "ollamaCloud") {
|
|
5174
|
+
const mask = isReconfig && config.providers.ollamaCloud.apiKey ? ` [${maskKey(config.providers.ollamaCloud.apiKey)}]` : "";
|
|
5175
|
+
const key = await promptValidatedValue(
|
|
5176
|
+
chalk6.white(` Ollama Cloud API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
5177
|
+
(value) => validateApiKey("ollamaCloud", value),
|
|
5178
|
+
isReconfig ? config.providers.ollamaCloud.apiKey : void 0,
|
|
5179
|
+
{ allowSkip: true }
|
|
5180
|
+
);
|
|
5181
|
+
if (key) {
|
|
5182
|
+
config.providers.ollamaCloud.apiKey = key;
|
|
5183
|
+
config.providers.ollamaCloud.enabled = true;
|
|
5184
|
+
}
|
|
5185
|
+
continue;
|
|
5186
|
+
}
|
|
5187
|
+
if (provider === "ollamaLocal") {
|
|
5188
|
+
config.providers.ollamaLocal.baseUrl = await promptValidatedValue(
|
|
5189
|
+
chalk6.white(` Ollama Local base URL [${config.providers.ollamaLocal.baseUrl}]: `),
|
|
5190
|
+
validateBaseUrl,
|
|
5191
|
+
config.providers.ollamaLocal.baseUrl
|
|
5192
|
+
);
|
|
5193
|
+
config.providers.ollamaLocal.model = await promptValidatedValue(
|
|
5194
|
+
chalk6.white(` Ollama Local model [${config.providers.ollamaLocal.model}]: `),
|
|
5195
|
+
validateModelName,
|
|
5196
|
+
config.providers.ollamaLocal.model
|
|
5197
|
+
);
|
|
5198
|
+
config.providers.ollamaLocal.enabled = true;
|
|
5199
|
+
}
|
|
4745
5200
|
}
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
5201
|
+
const configuredProviders = getConfiguredProviderNames(config);
|
|
5202
|
+
if (configuredProviders.length === 0) {
|
|
5203
|
+
console.log(chalk6.red(" You need to configure at least one LLM provider to continue."));
|
|
5204
|
+
console.log(chalk6.dim(" Let\u2019s try that step again."));
|
|
5205
|
+
console.log("");
|
|
5206
|
+
continue;
|
|
4751
5207
|
}
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
console.log(chalk6.dim(` Default provider set to ${config.providers.default}`));
|
|
5208
|
+
await chooseDefaultProvider(config);
|
|
5209
|
+
break;
|
|
4755
5210
|
}
|
|
4756
5211
|
hr();
|
|
4757
5212
|
console.log("");
|
|
@@ -4760,6 +5215,11 @@ async function configure(existingConfig) {
|
|
|
4760
5215
|
console.log(chalk6.dim(' Leave empty to keep current value. Enter "none" to disable.'));
|
|
4761
5216
|
} else {
|
|
4762
5217
|
console.log(chalk6.dim(" Leave empty to skip. You can add it later."));
|
|
5218
|
+
console.log(chalk6.dim(" To create a bot token:"));
|
|
5219
|
+
console.log(chalk6.dim(" 1. Open Telegram and message @BotFather"));
|
|
5220
|
+
console.log(chalk6.dim(" 2. Run /newbot and follow the prompts"));
|
|
5221
|
+
console.log(chalk6.dim(" 3. Copy the bot token BotFather gives you"));
|
|
5222
|
+
console.log(chalk6.dim(" 4. Paste that token here"));
|
|
4763
5223
|
}
|
|
4764
5224
|
console.log("");
|
|
4765
5225
|
const tgMask = isReconfig && config.channels.telegram.botToken ? ` [${maskKey(config.channels.telegram.botToken)}]` : "";
|
|
@@ -4767,7 +5227,11 @@ async function configure(existingConfig) {
|
|
|
4767
5227
|
if (isReconfig && telegramToken.toLowerCase() === "none") {
|
|
4768
5228
|
config.channels.telegram.enabled = false;
|
|
4769
5229
|
config.channels.telegram.botToken = "";
|
|
5230
|
+
clearTelegramPairing(config);
|
|
4770
5231
|
} else if (telegramToken) {
|
|
5232
|
+
if (telegramToken !== config.channels.telegram.botToken) {
|
|
5233
|
+
clearTelegramPairing(config);
|
|
5234
|
+
}
|
|
4771
5235
|
config.channels.telegram.botToken = telegramToken;
|
|
4772
5236
|
config.channels.telegram.enabled = true;
|
|
4773
5237
|
}
|
|
@@ -4878,10 +5342,10 @@ async function runAgent(isDaemon = false) {
|
|
|
4878
5342
|
const providers = new ProviderRegistry(config);
|
|
4879
5343
|
if (!providers.hasProviders()) {
|
|
4880
5344
|
if (isDaemon) {
|
|
4881
|
-
logger.error("No LLM providers available. Run `mercury doctor` to configure
|
|
5345
|
+
logger.error("No LLM providers available. Run `mercury doctor` to configure providers.");
|
|
4882
5346
|
return;
|
|
4883
5347
|
}
|
|
4884
|
-
console.log(chalk6.red(" No LLM providers available. Run `mercury doctor` to configure
|
|
5348
|
+
console.log(chalk6.red(" No LLM providers available. Run `mercury doctor` to configure providers."));
|
|
4885
5349
|
process.exit(1);
|
|
4886
5350
|
}
|
|
4887
5351
|
const available = providers.listAvailable();
|
|
@@ -4915,6 +5379,18 @@ async function runAgent(isDaemon = false) {
|
|
|
4915
5379
|
await msg.sendFile(filePath);
|
|
4916
5380
|
}
|
|
4917
5381
|
});
|
|
5382
|
+
capabilities.setSendMessageHandler(async (content) => {
|
|
5383
|
+
const telegram = channels.get("telegram");
|
|
5384
|
+
const pairedChatId = config.channels.telegram.pairedChatId;
|
|
5385
|
+
const pairedUserId = config.channels.telegram.pairedUserId;
|
|
5386
|
+
if (!config.channels.telegram.enabled || !telegram) {
|
|
5387
|
+
throw new Error("Telegram is not configured. Add a bot token in setup or run `mercury doctor`.");
|
|
5388
|
+
}
|
|
5389
|
+
if (pairedChatId == null || pairedUserId == null) {
|
|
5390
|
+
throw new Error("Telegram is not paired. Complete the pairing flow with /start or /pair from the Telegram owner account.");
|
|
5391
|
+
}
|
|
5392
|
+
await telegram.send(content, `telegram:${pairedChatId}`);
|
|
5393
|
+
});
|
|
4918
5394
|
if (process.env.GITHUB_TOKEN) {
|
|
4919
5395
|
setGitHubToken(process.env.GITHUB_TOKEN);
|
|
4920
5396
|
}
|
|
@@ -5055,8 +5531,9 @@ program.command("status").description("Show current configuration and daemon sta
|
|
|
5055
5531
|
if (config.identity.creator) {
|
|
5056
5532
|
console.log(` Creator: ${chalk6.white(config.identity.creator)}`);
|
|
5057
5533
|
}
|
|
5058
|
-
console.log(` Provider: ${chalk6.white(config.providers.default)}`);
|
|
5534
|
+
console.log(` Provider: ${chalk6.white(getProviderLabel(config.providers.default))}`);
|
|
5059
5535
|
console.log(` Telegram: ${config.channels.telegram.enabled ? chalk6.green("enabled") : chalk6.dim("disabled")}`);
|
|
5536
|
+
console.log(` Telegram Pairing: ${config.channels.telegram.pairedUserId != null ? chalk6.green(`paired to user ${config.channels.telegram.pairedUserId}${config.channels.telegram.pairedUsername ? ` (@${config.channels.telegram.pairedUsername})` : ""}`) : chalk6.dim("unpaired")}`);
|
|
5060
5537
|
console.log(` Skills: ${skills.length > 0 ? chalk6.green(skills.map((s) => s.name).join(", ")) : chalk6.dim("none")}`);
|
|
5061
5538
|
console.log(` Budget: ${chalk6.white(config.tokens.dailyBudget.toLocaleString())} tokens/day`);
|
|
5062
5539
|
console.log(` Setup: ${isSetupComplete() ? chalk6.green("complete") : chalk6.red("not done")}`);
|
|
@@ -5067,6 +5544,28 @@ program.command("status").description("Show current configuration and daemon sta
|
|
|
5067
5544
|
program.command("help").description("Show capabilities and commands manual").action(() => {
|
|
5068
5545
|
console.log(getManual());
|
|
5069
5546
|
});
|
|
5547
|
+
var telegramCmd = program.command("telegram").description("Manage Telegram pairing and access");
|
|
5548
|
+
telegramCmd.command("unpair").description("Clear the paired Telegram owner for this Mercury instance").action(() => {
|
|
5549
|
+
const config = loadConfig();
|
|
5550
|
+
const daemon = getDaemonStatus();
|
|
5551
|
+
if (config.channels.telegram.pairedUserId == null) {
|
|
5552
|
+
console.log("");
|
|
5553
|
+
console.log(chalk6.dim(" Telegram is already unpaired."));
|
|
5554
|
+
console.log("");
|
|
5555
|
+
return;
|
|
5556
|
+
}
|
|
5557
|
+
clearTelegramPairing(config);
|
|
5558
|
+
saveConfig(config);
|
|
5559
|
+
console.log("");
|
|
5560
|
+
console.log(chalk6.green(" \u2713 Telegram pairing cleared."));
|
|
5561
|
+
if (daemon.running) {
|
|
5562
|
+
console.log(chalk6.dim(" Restarting the background daemon to apply the change immediately..."));
|
|
5563
|
+
restartDaemon();
|
|
5564
|
+
} else {
|
|
5565
|
+
console.log(chalk6.dim(" The next private Telegram user to send /start will pair this Mercury instance."));
|
|
5566
|
+
}
|
|
5567
|
+
console.log("");
|
|
5568
|
+
});
|
|
5070
5569
|
var serviceCmd = program.command("service").description("Manage Mercury as a system service (auto-start, crash recovery)");
|
|
5071
5570
|
serviceCmd.command("install").description("Install Mercury as a system service (auto-start on boot)").action(() => {
|
|
5072
5571
|
installService();
|