@cosmicstack/mercury-agent 0.3.4 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -6
- package/dist/index.js +920 -287
- 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) {
|
|
@@ -676,6 +762,48 @@ var Lifecycle = class {
|
|
|
676
762
|
};
|
|
677
763
|
|
|
678
764
|
// src/core/agent.ts
|
|
765
|
+
var ToolCallLoopDetector = class {
|
|
766
|
+
recentCalls = [];
|
|
767
|
+
maxEntries = 10;
|
|
768
|
+
record(toolName, params) {
|
|
769
|
+
const paramsKey = JSON.stringify(params).slice(0, 100);
|
|
770
|
+
this.recentCalls.push({ tool: toolName, params: paramsKey });
|
|
771
|
+
if (this.recentCalls.length > this.maxEntries) {
|
|
772
|
+
this.recentCalls.shift();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
detect() {
|
|
776
|
+
if (this.recentCalls.length < 3) return null;
|
|
777
|
+
const last = this.recentCalls[this.recentCalls.length - 1];
|
|
778
|
+
let consecutiveCount = 0;
|
|
779
|
+
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
780
|
+
if (this.recentCalls[i].tool === last.tool && this.recentCalls[i].params === last.params) {
|
|
781
|
+
consecutiveCount++;
|
|
782
|
+
} else {
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (consecutiveCount >= 3) {
|
|
787
|
+
return { tool: last.tool, count: consecutiveCount };
|
|
788
|
+
}
|
|
789
|
+
const lastTool = last.tool;
|
|
790
|
+
let toolCount = 0;
|
|
791
|
+
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
792
|
+
if (this.recentCalls[i].tool === lastTool) {
|
|
793
|
+
toolCount++;
|
|
794
|
+
} else {
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (toolCount >= 4) {
|
|
799
|
+
return { tool: lastTool, count: toolCount };
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
reset() {
|
|
804
|
+
this.recentCalls = [];
|
|
805
|
+
}
|
|
806
|
+
};
|
|
679
807
|
var MAX_STEPS = 10;
|
|
680
808
|
var Agent = class {
|
|
681
809
|
constructor(config, providers, identity, shortTerm, longTerm, episodic, channels, tokenBudget, capabilities, scheduler) {
|
|
@@ -837,6 +965,30 @@ You can override this:
|
|
|
837
965
|
const recentMemory = this.shortTerm.getRecent(msg.channelId, 10);
|
|
838
966
|
const relevantFacts = this.longTerm.search(msg.content, 3);
|
|
839
967
|
const messages = [];
|
|
968
|
+
const recentSteps = this.shortTerm.getRecent(msg.channelId, 4);
|
|
969
|
+
let loopWarning = null;
|
|
970
|
+
if (recentSteps.length >= 3) {
|
|
971
|
+
const toolCallPattern = /\[Using: (.+?)\]/g;
|
|
972
|
+
const toolCalls = [];
|
|
973
|
+
for (const m of recentSteps) {
|
|
974
|
+
if (m.role === "assistant") {
|
|
975
|
+
let match;
|
|
976
|
+
while ((match = toolCallPattern.exec(m.content)) !== null) {
|
|
977
|
+
toolCalls.push(match[1]);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (toolCalls.length >= 3) {
|
|
982
|
+
const last3 = toolCalls.slice(-3);
|
|
983
|
+
if (last3[0] === last3[1] && last3[1] === last3[2]) {
|
|
984
|
+
loopWarning = `[SYSTEM WARNING] You have called ${last3[0]} 3+ times in a row with the same result. Stop repeating this call. Try a different approach \u2014 if you're failing on permissions, try a different path. If you're failing on git push auth, use github_api with PUT /repos/{owner}/{repo}/contents/{path} to push files directly through the API.`;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (loopWarning) {
|
|
989
|
+
messages.push({ role: "user", content: loopWarning });
|
|
990
|
+
messages.push({ role: "assistant", content: "Understood. I will try a different approach." });
|
|
991
|
+
}
|
|
840
992
|
if (relevantFacts.length > 0) {
|
|
841
993
|
messages.push({
|
|
842
994
|
role: "user",
|
|
@@ -866,6 +1018,7 @@ You can override this:
|
|
|
866
1018
|
let usedProvider = null;
|
|
867
1019
|
let lastError = null;
|
|
868
1020
|
let streamedText = "";
|
|
1021
|
+
const loopDetector = new ToolCallLoopDetector();
|
|
869
1022
|
const canStream = msg.channelType === "cli" || msg.channelType === "telegram" && this.telegramStreaming;
|
|
870
1023
|
for (const provider of fallbackIterator) {
|
|
871
1024
|
try {
|
|
@@ -881,6 +1034,13 @@ You can override this:
|
|
|
881
1034
|
if (toolCalls && toolCalls.length > 0) {
|
|
882
1035
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
883
1036
|
logger.info({ tools: names }, "Tool call step");
|
|
1037
|
+
for (const tc of toolCalls) {
|
|
1038
|
+
loopDetector.record(tc.toolName, tc.args);
|
|
1039
|
+
}
|
|
1040
|
+
const loop = loopDetector.detect();
|
|
1041
|
+
if (loop) {
|
|
1042
|
+
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected");
|
|
1043
|
+
}
|
|
884
1044
|
await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
|
|
885
1045
|
});
|
|
886
1046
|
}
|
|
@@ -918,6 +1078,13 @@ You can override this:
|
|
|
918
1078
|
if (toolCalls && toolCalls.length > 0) {
|
|
919
1079
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
920
1080
|
logger.info({ tools: names }, "Tool call step");
|
|
1081
|
+
for (const tc of toolCalls) {
|
|
1082
|
+
loopDetector.record(tc.toolName, tc.args);
|
|
1083
|
+
}
|
|
1084
|
+
const loop = loopDetector.detect();
|
|
1085
|
+
if (loop) {
|
|
1086
|
+
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected");
|
|
1087
|
+
}
|
|
921
1088
|
if (channel && msg.channelType !== "internal") {
|
|
922
1089
|
await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
|
|
923
1090
|
});
|
|
@@ -1012,16 +1179,35 @@ You can override this:
|
|
|
1012
1179
|
if (this.tokenBudget.getUsagePercentage() > 70) {
|
|
1013
1180
|
prompt += "\nBe concise to conserve tokens.";
|
|
1014
1181
|
}
|
|
1182
|
+
prompt += `
|
|
1183
|
+
|
|
1184
|
+
Environment:
|
|
1185
|
+
- Platform: ${process.platform}
|
|
1186
|
+
- Working directory: ${this.capabilities.getCwd()}`;
|
|
1015
1187
|
const toolNames = this.capabilities.getToolNames();
|
|
1016
1188
|
const githubTools = ["create_pr", "review_pr", "list_issues", "create_issue", "github_api"];
|
|
1017
1189
|
const hasGitHub = githubTools.some((t) => toolNames.includes(t));
|
|
1018
1190
|
if (hasGitHub) {
|
|
1019
|
-
let githubHint = "\n\nGitHub companion is active.
|
|
1191
|
+
let githubHint = "\n\nGitHub companion is active.";
|
|
1020
1192
|
const { defaultOwner, defaultRepo } = this.config.github;
|
|
1021
1193
|
if (defaultOwner && defaultRepo) {
|
|
1022
1194
|
githubHint += ` Default repo: ${defaultOwner}/${defaultRepo}. Use this when the user doesn't specify a repo.`;
|
|
1023
1195
|
}
|
|
1024
|
-
githubHint +=
|
|
1196
|
+
githubHint += `
|
|
1197
|
+
|
|
1198
|
+
Available GitHub tools and when to use them:
|
|
1199
|
+
- git_add, git_commit, git_push: LOCAL git operations (stage, commit, push to a remote you have SSH/auth access to). All commits include "Co-authored-by: Mercury <mercury@cosmicstack.org>".
|
|
1200
|
+
- create_pr: Create a pull request on GitHub. The head branch must already exist on the remote.
|
|
1201
|
+
- review_pr: Get PR details and optionally post a review comment.
|
|
1202
|
+
- list_issues, create_issue: Browse and file issues.
|
|
1203
|
+
- github_api: Raw GitHub API access. IMPORTANT USE CASES:
|
|
1204
|
+
- Push files directly to GitHub via PUT /repos/{owner}/{repo}/contents/{path} when git push fails due to auth. The body must include "message" and "content" (base64-encoded file content). This creates a commit on GitHub with Mercury as co-author.
|
|
1205
|
+
- Delete files via DELETE /repos/{owner}/{repo}/contents/{path} with a "message" and "sha" in the body.
|
|
1206
|
+
- Any other GitHub API operation not covered by the other tools.
|
|
1207
|
+
|
|
1208
|
+
When the user asks to "push to GitHub" or "upload files" and git push fails, use github_api with PUT /repos/{owner}/{repo}/contents/{path} to push content directly through the API. This bypasses local git entirely.
|
|
1209
|
+
|
|
1210
|
+
Always specify owner and repo parameters on GitHub tools. The user's GitHub username is ${this.config.github.username || "not set"}.'`;
|
|
1025
1211
|
prompt += githubHint;
|
|
1026
1212
|
}
|
|
1027
1213
|
return prompt;
|
|
@@ -1190,11 +1376,13 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
|
|
|
1190
1376
|
if (cmd === "/status") {
|
|
1191
1377
|
const config = ctx.config();
|
|
1192
1378
|
const budget = ctx.tokenBudget();
|
|
1379
|
+
const telegramPairing = config.channels.telegram.pairedUserId != null ? `paired to user ${config.channels.telegram.pairedUserId}${config.channels.telegram.pairedUsername ? ` (@${config.channels.telegram.pairedUsername})` : ""}` : "unpaired";
|
|
1193
1380
|
const lines = [
|
|
1194
1381
|
`**${config.identity.name}** \u2014 Status`,
|
|
1195
1382
|
`Owner: ${config.identity.owner || "(not set)"}`,
|
|
1196
1383
|
`Provider: ${config.providers.default}`,
|
|
1197
1384
|
`Telegram: ${config.channels.telegram.enabled ? "enabled" : "disabled"}`,
|
|
1385
|
+
`Telegram pairing: ${telegramPairing}`,
|
|
1198
1386
|
`Budget: ${budget.getStatusText()}`,
|
|
1199
1387
|
`Skills: ${ctx.skillNames().length > 0 ? ctx.skillNames().join(", ") : "none"}`
|
|
1200
1388
|
];
|
|
@@ -1727,16 +1915,16 @@ var CLIChannel = class extends BaseChannel {
|
|
|
1727
1915
|
}
|
|
1728
1916
|
}
|
|
1729
1917
|
async prompt(question) {
|
|
1730
|
-
return new Promise((
|
|
1731
|
-
this.rl?.question(question, (answer) =>
|
|
1918
|
+
return new Promise((resolve13) => {
|
|
1919
|
+
this.rl?.question(question, (answer) => resolve13(answer.trim()));
|
|
1732
1920
|
});
|
|
1733
1921
|
}
|
|
1734
1922
|
async askPermission(prompt) {
|
|
1735
|
-
return new Promise((
|
|
1923
|
+
return new Promise((resolve13) => {
|
|
1736
1924
|
console.log("");
|
|
1737
1925
|
console.log(chalk2.yellow(` \u26A0 ${prompt}`));
|
|
1738
1926
|
this.rl?.question(chalk2.yellow(" > "), (answer) => {
|
|
1739
|
-
|
|
1927
|
+
resolve13(answer.trim());
|
|
1740
1928
|
});
|
|
1741
1929
|
});
|
|
1742
1930
|
}
|
|
@@ -1752,6 +1940,7 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1752
1940
|
constructor(config) {
|
|
1753
1941
|
super();
|
|
1754
1942
|
this.config = config;
|
|
1943
|
+
this.ownerChatId = config.channels.telegram.pairedChatId ?? null;
|
|
1755
1944
|
}
|
|
1756
1945
|
config;
|
|
1757
1946
|
type = "telegram";
|
|
@@ -1773,9 +1962,33 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1773
1962
|
bot.api.config.use(autoRetry());
|
|
1774
1963
|
bot.on("message:text", async (ctx) => {
|
|
1775
1964
|
const chatId = ctx.chat.id;
|
|
1776
|
-
|
|
1965
|
+
const userId = ctx.from?.id;
|
|
1966
|
+
const text = ctx.message.text?.trim() || "";
|
|
1967
|
+
if (!userId) return;
|
|
1968
|
+
if (ctx.chat.type !== "private") {
|
|
1969
|
+
await this.sendDirectMessage(chatId, "This bot is only available in private one-to-one chats.");
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
if (!this.isPaired()) {
|
|
1973
|
+
await this.handleUnpairedMessage(userId, chatId, text, ctx.from?.username);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (!this.isAuthorizedUser(userId)) {
|
|
1977
|
+
await this.sendDirectMessage(chatId, "This bot is not available to you.");
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1777
1980
|
this.ownerChatId = chatId;
|
|
1778
1981
|
logger.info({ chatId, text: ctx.message.text?.slice(0, 50) }, "Telegram message received");
|
|
1982
|
+
const command = text.toLowerCase();
|
|
1983
|
+
if (command === "/start" || command === "/pair") {
|
|
1984
|
+
await this.sendDirectMessage(chatId, this.getPairingStatusMessage());
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
if (command === "/unpair") {
|
|
1988
|
+
this.unpair();
|
|
1989
|
+
await this.sendDirectMessage(chatId, "Telegram pairing removed. Send /start to pair this Mercury instance again.");
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1779
1992
|
const msg = {
|
|
1780
1993
|
id: ctx.message.message_id.toString(),
|
|
1781
1994
|
channelId: `telegram:${chatId}`,
|
|
@@ -1815,6 +2028,8 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1815
2028
|
async registerCommands() {
|
|
1816
2029
|
if (!this.bot) return;
|
|
1817
2030
|
const commands = [
|
|
2031
|
+
{ command: "start", description: "Pair this Telegram account to Mercury" },
|
|
2032
|
+
{ command: "pair", description: "Pair this Telegram account to Mercury" },
|
|
1818
2033
|
{ command: "help", description: "Show capabilities and commands manual" },
|
|
1819
2034
|
{ command: "status", description: "Show agent config, budget, and uptime" },
|
|
1820
2035
|
{ command: "tools", description: "List all loaded tools" },
|
|
@@ -1823,7 +2038,8 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1823
2038
|
{ command: "budget_override", description: "Override budget for one request" },
|
|
1824
2039
|
{ command: "budget_reset", description: "Reset token usage to zero" },
|
|
1825
2040
|
{ command: "budget_set", description: "Set new daily token budget" },
|
|
1826
|
-
{ command: "stream", description: "Toggle text streaming on/off" }
|
|
2041
|
+
{ command: "stream", description: "Toggle text streaming on/off" },
|
|
2042
|
+
{ command: "unpair", description: "Remove Telegram pairing for this Mercury instance" }
|
|
1827
2043
|
];
|
|
1828
2044
|
try {
|
|
1829
2045
|
await this.bot.api.setMyCommands(commands);
|
|
@@ -1999,15 +2215,15 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
1999
2215
|
reply_markup: keyboard
|
|
2000
2216
|
});
|
|
2001
2217
|
}
|
|
2002
|
-
return new Promise((
|
|
2003
|
-
this.pendingApprovals.set(`${id}:yes`, () =>
|
|
2004
|
-
this.pendingApprovals.set(`${id}:always`, () =>
|
|
2005
|
-
this.pendingApprovals.set(`${id}:no`, () =>
|
|
2218
|
+
return new Promise((resolve13) => {
|
|
2219
|
+
this.pendingApprovals.set(`${id}:yes`, () => resolve13("yes"));
|
|
2220
|
+
this.pendingApprovals.set(`${id}:always`, () => resolve13("always"));
|
|
2221
|
+
this.pendingApprovals.set(`${id}:no`, () => resolve13("no"));
|
|
2006
2222
|
setTimeout(() => {
|
|
2007
2223
|
this.pendingApprovals.delete(`${id}:yes`);
|
|
2008
2224
|
this.pendingApprovals.delete(`${id}:always`);
|
|
2009
2225
|
this.pendingApprovals.delete(`${id}:no`);
|
|
2010
|
-
|
|
2226
|
+
resolve13("no");
|
|
2011
2227
|
}, 12e4);
|
|
2012
2228
|
});
|
|
2013
2229
|
}
|
|
@@ -2044,19 +2260,57 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2044
2260
|
return [".mp4", ".mov", ".avi", ".mkv", ".webm"].includes(ext);
|
|
2045
2261
|
}
|
|
2046
2262
|
parseChatId(targetId) {
|
|
2047
|
-
if (!targetId) return this.ownerChatId;
|
|
2263
|
+
if (!targetId) return this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null;
|
|
2048
2264
|
if (targetId.startsWith("telegram:")) {
|
|
2049
2265
|
const raw = Number(targetId.split(":")[1]);
|
|
2050
|
-
return isNaN(raw) ? this.ownerChatId : raw;
|
|
2266
|
+
return isNaN(raw) ? this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null : raw;
|
|
2051
2267
|
}
|
|
2052
|
-
if (targetId === "notification") return this.ownerChatId;
|
|
2268
|
+
if (targetId === "notification") return this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null;
|
|
2053
2269
|
const num = Number(targetId);
|
|
2054
|
-
return isNaN(num) ? this.ownerChatId : num;
|
|
2270
|
+
return isNaN(num) ? this.ownerChatId ?? this.config.channels.telegram.pairedChatId ?? null : num;
|
|
2271
|
+
}
|
|
2272
|
+
isPaired() {
|
|
2273
|
+
return typeof this.config.channels.telegram.pairedUserId === "number";
|
|
2055
2274
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2275
|
+
isAuthorizedUser(userId) {
|
|
2276
|
+
return this.config.channels.telegram.pairedUserId === userId;
|
|
2277
|
+
}
|
|
2278
|
+
async handleUnpairedMessage(userId, chatId, text, username) {
|
|
2279
|
+
const command = text.toLowerCase();
|
|
2280
|
+
if (command === "/start" || command === "/pair") {
|
|
2281
|
+
setTelegramPairing(this.config, userId, chatId, username);
|
|
2282
|
+
saveConfig(this.config);
|
|
2283
|
+
this.ownerChatId = chatId;
|
|
2284
|
+
logger.info({ chatId, userId, username }, "Telegram paired to owner");
|
|
2285
|
+
await this.sendDirectMessage(chatId, this.getPairingStatusMessage(true));
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
await this.sendDirectMessage(
|
|
2289
|
+
chatId,
|
|
2290
|
+
"This Mercury instance is not paired yet. Send /start to pair this bot to your Telegram account."
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
getPairingStatusMessage(newlyPaired = false) {
|
|
2294
|
+
const username = this.config.channels.telegram.pairedUsername ? ` (@${this.config.channels.telegram.pairedUsername})` : "";
|
|
2295
|
+
const prefix = newlyPaired ? "Telegram paired successfully." : "This Telegram account is already paired.";
|
|
2296
|
+
return `${prefix}
|
|
2297
|
+
|
|
2298
|
+
Owner user ID: ${this.config.channels.telegram.pairedUserId}${username}`;
|
|
2299
|
+
}
|
|
2300
|
+
unpair() {
|
|
2301
|
+
clearTelegramPairing(this.config);
|
|
2302
|
+
saveConfig(this.config);
|
|
2303
|
+
this.ownerChatId = null;
|
|
2304
|
+
logger.info("Telegram pairing cleared");
|
|
2305
|
+
}
|
|
2306
|
+
async sendDirectMessage(chatId, content) {
|
|
2307
|
+
if (!this.bot) return;
|
|
2308
|
+
try {
|
|
2309
|
+
await this.bot.api.sendMessage(chatId, mdToTelegram(content), { parse_mode: "HTML" });
|
|
2310
|
+
} catch {
|
|
2311
|
+
await this.bot.api.sendMessage(chatId, content).catch(() => {
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2060
2314
|
}
|
|
2061
2315
|
};
|
|
2062
2316
|
|
|
@@ -2356,8 +2610,8 @@ var PermissionManager = class {
|
|
|
2356
2610
|
clearElevation() {
|
|
2357
2611
|
this.elevatedCommands.clear();
|
|
2358
2612
|
}
|
|
2359
|
-
isElevated(
|
|
2360
|
-
if (this.elevatedCommands.has(
|
|
2613
|
+
isElevated(tool32) {
|
|
2614
|
+
if (this.elevatedCommands.has(tool32)) return true;
|
|
2361
2615
|
return false;
|
|
2362
2616
|
}
|
|
2363
2617
|
isShellElevated() {
|
|
@@ -2617,15 +2871,15 @@ Allow access?`;
|
|
|
2617
2871
|
import { tool } from "ai";
|
|
2618
2872
|
import { z } from "zod";
|
|
2619
2873
|
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
2620
|
-
import { resolve as resolve3 } from "path";
|
|
2621
|
-
function createReadFileTool(permissions) {
|
|
2874
|
+
import { resolve as resolve3, isAbsolute } from "path";
|
|
2875
|
+
function createReadFileTool(permissions, getCwd) {
|
|
2622
2876
|
return tool({
|
|
2623
2877
|
description: "Read the contents of a file. The path must be within an allowed scope.",
|
|
2624
2878
|
parameters: z.object({
|
|
2625
2879
|
path: z.string().describe("Absolute or relative path to the file")
|
|
2626
2880
|
}),
|
|
2627
2881
|
execute: async ({ path: path3 }) => {
|
|
2628
|
-
const resolved = resolve3(path3);
|
|
2882
|
+
const resolved = isAbsolute(path3) ? resolve3(path3) : resolve3(getCwd(), path3);
|
|
2629
2883
|
const check = await permissions.checkFsAccess(resolved, "read");
|
|
2630
2884
|
if (!check.allowed) {
|
|
2631
2885
|
const parentDir = resolve3(resolved, "..");
|
|
@@ -2654,8 +2908,8 @@ function createReadFileTool(permissions) {
|
|
|
2654
2908
|
import { tool as tool2 } from "ai";
|
|
2655
2909
|
import { z as z2 } from "zod";
|
|
2656
2910
|
import { existsSync as existsSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
2657
|
-
import { resolve as resolve4 } from "path";
|
|
2658
|
-
function createWriteFileTool(permissions) {
|
|
2911
|
+
import { resolve as resolve4, isAbsolute as isAbsolute2 } from "path";
|
|
2912
|
+
function createWriteFileTool(permissions, getCwd) {
|
|
2659
2913
|
return tool2({
|
|
2660
2914
|
description: "Write content to an existing file. The path must be within a writable scope.",
|
|
2661
2915
|
parameters: z2.object({
|
|
@@ -2663,7 +2917,7 @@ function createWriteFileTool(permissions) {
|
|
|
2663
2917
|
content: z2.string().describe("The content to write to the file")
|
|
2664
2918
|
}),
|
|
2665
2919
|
execute: async ({ path: path3, content }) => {
|
|
2666
|
-
const resolved = resolve4(path3);
|
|
2920
|
+
const resolved = isAbsolute2(path3) ? resolve4(path3) : resolve4(getCwd(), path3);
|
|
2667
2921
|
const check = await permissions.checkFsAccess(resolved, "write");
|
|
2668
2922
|
if (!check.allowed) {
|
|
2669
2923
|
const parentDir = resolve4(resolved, "..");
|
|
@@ -2686,8 +2940,8 @@ function createWriteFileTool(permissions) {
|
|
|
2686
2940
|
import { tool as tool3 } from "ai";
|
|
2687
2941
|
import { z as z3 } from "zod";
|
|
2688
2942
|
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) {
|
|
2943
|
+
import { resolve as resolve5, dirname as dirname2, isAbsolute as isAbsolute3 } from "path";
|
|
2944
|
+
function createCreateFileTool(permissions, getCwd) {
|
|
2691
2945
|
return tool3({
|
|
2692
2946
|
description: "Create a new file with the given content. Also creates parent directories if needed. The path must be within a writable scope.",
|
|
2693
2947
|
parameters: z3.object({
|
|
@@ -2695,7 +2949,7 @@ function createCreateFileTool(permissions) {
|
|
|
2695
2949
|
content: z3.string().describe("The content of the new file")
|
|
2696
2950
|
}),
|
|
2697
2951
|
execute: async ({ path: path3, content }) => {
|
|
2698
|
-
const resolved = resolve5(path3);
|
|
2952
|
+
const resolved = isAbsolute3(path3) ? resolve5(path3) : resolve5(getCwd(), path3);
|
|
2699
2953
|
const check = await permissions.checkFsAccess(resolved, "write");
|
|
2700
2954
|
if (!check.allowed) {
|
|
2701
2955
|
const parentDir = resolve5(resolved, "..");
|
|
@@ -2722,15 +2976,15 @@ function createCreateFileTool(permissions) {
|
|
|
2722
2976
|
import { tool as tool4 } from "ai";
|
|
2723
2977
|
import { z as z4 } from "zod";
|
|
2724
2978
|
import { existsSync as existsSync10, readdirSync as readdirSync2, statSync } from "fs";
|
|
2725
|
-
import { resolve as resolve6 } from "path";
|
|
2726
|
-
function createListDirTool(permissions) {
|
|
2979
|
+
import { resolve as resolve6, isAbsolute as isAbsolute4 } from "path";
|
|
2980
|
+
function createListDirTool(permissions, getCwd) {
|
|
2727
2981
|
return tool4({
|
|
2728
2982
|
description: "List the contents of a directory. Shows file names, types, and sizes.",
|
|
2729
2983
|
parameters: z4.object({
|
|
2730
2984
|
path: z4.string().describe("Absolute or relative path to the directory")
|
|
2731
2985
|
}),
|
|
2732
2986
|
execute: async ({ path: path3 }) => {
|
|
2733
|
-
const resolved = resolve6(path3);
|
|
2987
|
+
const resolved = isAbsolute4(path3) ? resolve6(path3) : resolve6(getCwd(), path3);
|
|
2734
2988
|
const check = await permissions.checkFsAccess(resolved, "read");
|
|
2735
2989
|
if (!check.allowed) {
|
|
2736
2990
|
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 +3034,15 @@ function formatSize(bytes) {
|
|
|
2780
3034
|
import { tool as tool5 } from "ai";
|
|
2781
3035
|
import { z as z5 } from "zod";
|
|
2782
3036
|
import { existsSync as existsSync11, unlinkSync as unlinkSync2 } from "fs";
|
|
2783
|
-
import { resolve as resolve7 } from "path";
|
|
2784
|
-
function createDeleteFileTool(permissions) {
|
|
3037
|
+
import { resolve as resolve7, isAbsolute as isAbsolute5 } from "path";
|
|
3038
|
+
function createDeleteFileTool(permissions, getCwd) {
|
|
2785
3039
|
return tool5({
|
|
2786
3040
|
description: "Delete a file. This action cannot be undone. The path must be within a writable scope. Always asks for confirmation.",
|
|
2787
3041
|
parameters: z5.object({
|
|
2788
3042
|
path: z5.string().describe("Absolute or relative path to the file to delete")
|
|
2789
3043
|
}),
|
|
2790
3044
|
execute: async ({ path: path3 }) => {
|
|
2791
|
-
const resolved = resolve7(path3);
|
|
3045
|
+
const resolved = isAbsolute5(path3) ? resolve7(path3) : resolve7(getCwd(), path3);
|
|
2792
3046
|
const check = await permissions.checkFsAccess(resolved, "write");
|
|
2793
3047
|
if (!check.allowed) {
|
|
2794
3048
|
const parentDir = resolve7(resolved, "..");
|
|
@@ -2815,8 +3069,8 @@ function createDeleteFileTool(permissions) {
|
|
|
2815
3069
|
import { tool as tool6 } from "ai";
|
|
2816
3070
|
import { z as z6 } from "zod";
|
|
2817
3071
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
2818
|
-
import { resolve as resolve8 } from "path";
|
|
2819
|
-
function createEditFileTool(permissions) {
|
|
3072
|
+
import { resolve as resolve8, isAbsolute as isAbsolute6 } from "path";
|
|
3073
|
+
function createEditFileTool(permissions, getCwd) {
|
|
2820
3074
|
return tool6({
|
|
2821
3075
|
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
3076
|
parameters: z6.object({
|
|
@@ -2825,7 +3079,7 @@ function createEditFileTool(permissions) {
|
|
|
2825
3079
|
new_string: z6.string().describe("The text to replace it with")
|
|
2826
3080
|
}),
|
|
2827
3081
|
execute: async ({ path: path3, old_string, new_string }) => {
|
|
2828
|
-
const resolved = resolve8(path3);
|
|
3082
|
+
const resolved = isAbsolute6(path3) ? resolve8(path3) : resolve8(getCwd(), path3);
|
|
2829
3083
|
const fsCheck = await permissions.checkFsAccess(resolved, "write");
|
|
2830
3084
|
if (!fsCheck.allowed) {
|
|
2831
3085
|
const parentDir = resolve8(resolved, "..");
|
|
@@ -2859,15 +3113,15 @@ function createEditFileTool(permissions) {
|
|
|
2859
3113
|
import { tool as tool7 } from "ai";
|
|
2860
3114
|
import { z as z7 } from "zod";
|
|
2861
3115
|
import { existsSync as existsSync12, statSync as statSync2 } from "fs";
|
|
2862
|
-
import { resolve as resolve9, basename } from "path";
|
|
2863
|
-
function createSendFileTool(permissions, sendFile) {
|
|
3116
|
+
import { resolve as resolve9, basename, isAbsolute as isAbsolute7 } from "path";
|
|
3117
|
+
function createSendFileTool(permissions, getCwd, sendFile) {
|
|
2864
3118
|
return tool7({
|
|
2865
3119
|
description: "Send a file to the user. On Telegram the file is uploaded as an attachment. On CLI the file path and size are displayed. The path must be within an allowed read scope.",
|
|
2866
3120
|
parameters: z7.object({
|
|
2867
3121
|
path: z7.string().describe("Absolute or relative path to the file to send")
|
|
2868
3122
|
}),
|
|
2869
3123
|
execute: async ({ path: path3 }) => {
|
|
2870
|
-
const resolved = resolve9(path3);
|
|
3124
|
+
const resolved = isAbsolute7(path3) ? resolve9(path3) : resolve9(getCwd(), path3);
|
|
2871
3125
|
const check = await permissions.checkFsAccess(resolved, "read");
|
|
2872
3126
|
if (!check.allowed) {
|
|
2873
3127
|
const parentDir = resolve9(resolved, "..");
|
|
@@ -2895,19 +3149,43 @@ function createSendFileTool(permissions, sendFile) {
|
|
|
2895
3149
|
});
|
|
2896
3150
|
}
|
|
2897
3151
|
|
|
2898
|
-
// src/capabilities/
|
|
3152
|
+
// src/capabilities/messaging/send-message.ts
|
|
2899
3153
|
import { tool as tool8 } from "ai";
|
|
2900
3154
|
import { z as z8 } from "zod";
|
|
2901
|
-
|
|
2902
|
-
function createApproveScopeTool(permissions) {
|
|
3155
|
+
function createSendMessageTool(sendMessage) {
|
|
2903
3156
|
return tool8({
|
|
2904
|
-
description:
|
|
3157
|
+
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
3158
|
parameters: z8.object({
|
|
2906
|
-
|
|
2907
|
-
|
|
3159
|
+
content: z8.string().describe("The message content to send to the paired Telegram owner")
|
|
3160
|
+
}),
|
|
3161
|
+
execute: async ({ content }) => {
|
|
3162
|
+
const trimmed = content.trim();
|
|
3163
|
+
if (!trimmed) {
|
|
3164
|
+
return "Error: Message content cannot be empty.";
|
|
3165
|
+
}
|
|
3166
|
+
try {
|
|
3167
|
+
await sendMessage(trimmed);
|
|
3168
|
+
return "Message sent to the paired Telegram owner.";
|
|
3169
|
+
} catch (err) {
|
|
3170
|
+
return `Error sending message: ${err.message}`;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// src/capabilities/filesystem/approve-scope.ts
|
|
3177
|
+
import { tool as tool9 } from "ai";
|
|
3178
|
+
import { z as z9 } from "zod";
|
|
3179
|
+
import { resolve as resolve10, isAbsolute as isAbsolute8 } from "path";
|
|
3180
|
+
function createApproveScopeTool(permissions, getCwd) {
|
|
3181
|
+
return tool9({
|
|
3182
|
+
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.',
|
|
3183
|
+
parameters: z9.object({
|
|
3184
|
+
path: z9.string().describe("The directory path to request access to"),
|
|
3185
|
+
mode: z9.enum(["read", "write"]).describe("The access mode needed")
|
|
2908
3186
|
}),
|
|
2909
3187
|
execute: async ({ path: path3, mode }) => {
|
|
2910
|
-
const resolved = resolve10(path3);
|
|
3188
|
+
const resolved = isAbsolute8(path3) ? resolve10(path3) : resolve10(getCwd(), path3);
|
|
2911
3189
|
const result = await permissions.requestScopeExternal(resolved, mode);
|
|
2912
3190
|
if (result.allowed) {
|
|
2913
3191
|
return `Access approved for ${mode} access to ${resolved}. You can now retry the file operation.`;
|
|
@@ -2918,17 +3196,19 @@ function createApproveScopeTool(permissions) {
|
|
|
2918
3196
|
}
|
|
2919
3197
|
|
|
2920
3198
|
// src/capabilities/shell/run-command.ts
|
|
2921
|
-
import { tool as
|
|
2922
|
-
import { z as
|
|
3199
|
+
import { tool as tool10 } from "ai";
|
|
3200
|
+
import { z as z10 } from "zod";
|
|
2923
3201
|
import { execSync } from "child_process";
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3202
|
+
import { resolve as resolve11, isAbsolute as isAbsolute9 } from "path";
|
|
3203
|
+
import { existsSync as existsSync13 } from "fs";
|
|
3204
|
+
function createRunCommandTool(permissions, getCwd, setCwd) {
|
|
3205
|
+
return tool10({
|
|
3206
|
+
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
3207
|
Blocked commands (sudo, rm -rf /, etc.) are never executed.
|
|
2928
3208
|
Auto-approved commands (ls, cat, git status, curl, etc.) run without asking.
|
|
2929
|
-
Other commands require user approval
|
|
2930
|
-
parameters:
|
|
2931
|
-
command:
|
|
3209
|
+
Other commands require user approval.`,
|
|
3210
|
+
parameters: z10.object({
|
|
3211
|
+
command: z10.string().describe("The shell command to execute")
|
|
2932
3212
|
}),
|
|
2933
3213
|
execute: async ({ command }) => {
|
|
2934
3214
|
const check = await permissions.checkShellCommand(command);
|
|
@@ -2942,20 +3222,25 @@ Tell the user what this command does and ask for permission. If they approve, tr
|
|
|
2942
3222
|
}
|
|
2943
3223
|
return `Error: ${check.reason}`;
|
|
2944
3224
|
}
|
|
3225
|
+
const cwd = getCwd();
|
|
2945
3226
|
try {
|
|
2946
|
-
logger.info({ cmd: command }, "Executing shell command");
|
|
3227
|
+
logger.info({ cmd: command, cwd }, "Executing shell command");
|
|
2947
3228
|
const result = execSync(command, {
|
|
2948
|
-
cwd
|
|
3229
|
+
cwd,
|
|
2949
3230
|
timeout: 3e4,
|
|
2950
3231
|
maxBuffer: 1024 * 1024,
|
|
2951
3232
|
encoding: "utf-8",
|
|
2952
3233
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2953
3234
|
});
|
|
2954
|
-
const
|
|
2955
|
-
|
|
3235
|
+
const trimmedOutput = result?.trim() || "(no output)";
|
|
3236
|
+
detectCd(command, cwd, setCwd);
|
|
3237
|
+
return trimmedOutput;
|
|
2956
3238
|
} catch (err) {
|
|
2957
3239
|
const stderr = err.stderr?.trim();
|
|
2958
3240
|
const stdout = err.stdout?.trim();
|
|
3241
|
+
if (stdout || stderr) {
|
|
3242
|
+
detectCd(command, cwd, setCwd);
|
|
3243
|
+
}
|
|
2959
3244
|
let msg = `Command exited with code ${err.status || "unknown"}`;
|
|
2960
3245
|
if (stdout) msg += `
|
|
2961
3246
|
Output: ${stdout}`;
|
|
@@ -2966,15 +3251,66 @@ Error: ${stderr}`;
|
|
|
2966
3251
|
}
|
|
2967
3252
|
});
|
|
2968
3253
|
}
|
|
3254
|
+
function detectCd(command, currentCwd, setCwd) {
|
|
3255
|
+
const trimmed = command.trim();
|
|
3256
|
+
const cdOnly = trimmed.match(/^cd\s+(.+)$/);
|
|
3257
|
+
if (cdOnly) {
|
|
3258
|
+
const target = cdOnly[1].replace(/^["']|["']$/g, "").replace(/~/, process.env.HOME || "");
|
|
3259
|
+
const resolved = isAbsolute9(target) ? target : resolve11(currentCwd, target);
|
|
3260
|
+
if (existsSync13(resolved)) {
|
|
3261
|
+
setCwd(resolved);
|
|
3262
|
+
}
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
const cdChain = trimmed.match(/cd\s+(.+?)\s*&&/);
|
|
3266
|
+
if (cdChain) {
|
|
3267
|
+
const target = cdChain[1].replace(/^["']|["']$/g, "").replace(/~/, process.env.HOME || "");
|
|
3268
|
+
const resolved = isAbsolute9(target) ? target : resolve11(currentCwd, target);
|
|
3269
|
+
if (existsSync13(resolved)) {
|
|
3270
|
+
setCwd(resolved);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
// src/capabilities/shell/cd.ts
|
|
3276
|
+
import { tool as tool11 } from "ai";
|
|
3277
|
+
import { z as z11 } from "zod";
|
|
3278
|
+
import { resolve as resolve12, isAbsolute as isAbsolute10 } from "path";
|
|
3279
|
+
import { existsSync as existsSync14, statSync as statSync3 } from "fs";
|
|
3280
|
+
function createCdTool(getCwd, setCwd) {
|
|
3281
|
+
return tool11({
|
|
3282
|
+
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.",
|
|
3283
|
+
parameters: z11.object({
|
|
3284
|
+
path: z11.string().describe("The directory to change to. Can be absolute or relative to the current directory.")
|
|
3285
|
+
}),
|
|
3286
|
+
execute: async ({ path: path3 }) => {
|
|
3287
|
+
const cwd = getCwd();
|
|
3288
|
+
const resolved = isAbsolute10(path3) ? resolve12(path3) : resolve12(cwd, path3);
|
|
3289
|
+
if (!existsSync14(resolved)) {
|
|
3290
|
+
return `Error: Directory not found: ${resolved}`;
|
|
3291
|
+
}
|
|
3292
|
+
try {
|
|
3293
|
+
const stat = statSync3(resolved);
|
|
3294
|
+
if (!stat.isDirectory()) {
|
|
3295
|
+
return `Error: Not a directory: ${resolved}`;
|
|
3296
|
+
}
|
|
3297
|
+
} catch {
|
|
3298
|
+
return `Error: Cannot access: ${resolved}`;
|
|
3299
|
+
}
|
|
3300
|
+
setCwd(resolved);
|
|
3301
|
+
return `Changed directory to ${resolved}`;
|
|
3302
|
+
}
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
2969
3305
|
|
|
2970
3306
|
// src/capabilities/shell/approve-command.ts
|
|
2971
|
-
import { tool as
|
|
2972
|
-
import { z as
|
|
3307
|
+
import { tool as tool12 } from "ai";
|
|
3308
|
+
import { z as z12 } from "zod";
|
|
2973
3309
|
function createApproveCommandTool(permissions) {
|
|
2974
|
-
return
|
|
3310
|
+
return tool12({
|
|
2975
3311
|
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:
|
|
3312
|
+
parameters: z12.object({
|
|
3313
|
+
command: z12.string().describe('The base command to permanently approve (e.g. "curl", "docker", "npm")')
|
|
2978
3314
|
}),
|
|
2979
3315
|
execute: async ({ command }) => {
|
|
2980
3316
|
const baseCmd = command.trim().split(/\s+/)[0];
|
|
@@ -2985,15 +3321,15 @@ function createApproveCommandTool(permissions) {
|
|
|
2985
3321
|
}
|
|
2986
3322
|
|
|
2987
3323
|
// src/capabilities/skills/install-skill.ts
|
|
2988
|
-
import { tool as
|
|
2989
|
-
import { z as
|
|
3324
|
+
import { tool as tool13 } from "ai";
|
|
3325
|
+
import { z as z13 } from "zod";
|
|
2990
3326
|
import { parse as parseYaml4 } from "yaml";
|
|
2991
3327
|
function createInstallSkillTool(skillLoader) {
|
|
2992
|
-
return
|
|
3328
|
+
return tool13({
|
|
2993
3329
|
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:
|
|
3330
|
+
parameters: z13.object({
|
|
3331
|
+
content: z13.string().optional().describe("Raw SKILL.md markdown content with YAML frontmatter"),
|
|
3332
|
+
url: z13.string().optional().describe("URL to fetch a SKILL.md from")
|
|
2997
3333
|
}),
|
|
2998
3334
|
execute: async ({ content, url }) => {
|
|
2999
3335
|
let skillContent;
|
|
@@ -3033,12 +3369,12 @@ function createInstallSkillTool(skillLoader) {
|
|
|
3033
3369
|
}
|
|
3034
3370
|
|
|
3035
3371
|
// src/capabilities/skills/list-skills.ts
|
|
3036
|
-
import { tool as
|
|
3037
|
-
import { z as
|
|
3372
|
+
import { tool as tool14 } from "ai";
|
|
3373
|
+
import { z as z14 } from "zod";
|
|
3038
3374
|
function createListSkillsTool(skillLoader) {
|
|
3039
|
-
return
|
|
3375
|
+
return tool14({
|
|
3040
3376
|
description: "List all installed skills with their names and descriptions.",
|
|
3041
|
-
parameters:
|
|
3377
|
+
parameters: z14.object({}),
|
|
3042
3378
|
execute: async () => {
|
|
3043
3379
|
const skills = skillLoader.getDiscovered();
|
|
3044
3380
|
if (skills.length === 0) {
|
|
@@ -3050,13 +3386,13 @@ function createListSkillsTool(skillLoader) {
|
|
|
3050
3386
|
}
|
|
3051
3387
|
|
|
3052
3388
|
// src/capabilities/skills/use-skill.ts
|
|
3053
|
-
import { tool as
|
|
3054
|
-
import { z as
|
|
3389
|
+
import { tool as tool15 } from "ai";
|
|
3390
|
+
import { z as z15 } from "zod";
|
|
3055
3391
|
function createUseSkillTool(skillLoader, permissions) {
|
|
3056
|
-
return
|
|
3392
|
+
return tool15({
|
|
3057
3393
|
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:
|
|
3394
|
+
parameters: z15.object({
|
|
3395
|
+
name: z15.string().describe("Name of the skill to invoke")
|
|
3060
3396
|
}),
|
|
3061
3397
|
execute: async ({ name }) => {
|
|
3062
3398
|
const skill = skillLoader.load(name);
|
|
@@ -3083,18 +3419,18 @@ Allowed tools: ${skill["allowed-tools"].join(", ")}`;
|
|
|
3083
3419
|
}
|
|
3084
3420
|
|
|
3085
3421
|
// src/capabilities/scheduler/schedule-task.ts
|
|
3086
|
-
import { tool as
|
|
3087
|
-
import { z as
|
|
3422
|
+
import { tool as tool16 } from "ai";
|
|
3423
|
+
import { z as z16 } from "zod";
|
|
3088
3424
|
import cron2 from "node-cron";
|
|
3089
3425
|
function createScheduleTaskTool(scheduler, getContext) {
|
|
3090
|
-
return
|
|
3426
|
+
return tool16({
|
|
3091
3427
|
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:
|
|
3428
|
+
parameters: z16.object({
|
|
3429
|
+
cron: z16.string().optional().describe('Cron expression for recurring tasks (e.g. "0 9 * * *" for daily at 9am)'),
|
|
3430
|
+
delay_seconds: z16.number().optional().describe('Delay in seconds for one-shot tasks (e.g. 15 for "remind me in 15 seconds")'),
|
|
3431
|
+
description: z16.string().describe("Human-readable description of what this task does"),
|
|
3432
|
+
prompt: z16.string().optional().describe("Prompt to send to the agent when the task fires"),
|
|
3433
|
+
skill_name: z16.string().optional().describe("Name of a skill to invoke when the task fires")
|
|
3098
3434
|
}),
|
|
3099
3435
|
execute: async ({ cron: cronExpr, delay_seconds, description, prompt, skill_name }) => {
|
|
3100
3436
|
if (!cronExpr && !delay_seconds) {
|
|
@@ -3147,12 +3483,12 @@ function createScheduleTaskTool(scheduler, getContext) {
|
|
|
3147
3483
|
}
|
|
3148
3484
|
|
|
3149
3485
|
// src/capabilities/scheduler/list-tasks.ts
|
|
3150
|
-
import { tool as
|
|
3151
|
-
import { z as
|
|
3486
|
+
import { tool as tool17 } from "ai";
|
|
3487
|
+
import { z as z17 } from "zod";
|
|
3152
3488
|
function createListTasksTool(scheduler) {
|
|
3153
|
-
return
|
|
3489
|
+
return tool17({
|
|
3154
3490
|
description: "List all scheduled tasks with their cron expressions and descriptions.",
|
|
3155
|
-
parameters:
|
|
3491
|
+
parameters: z17.object({}),
|
|
3156
3492
|
execute: async () => {
|
|
3157
3493
|
const manifests = scheduler.getManifests();
|
|
3158
3494
|
if (manifests.length === 0) {
|
|
@@ -3167,13 +3503,13 @@ function createListTasksTool(scheduler) {
|
|
|
3167
3503
|
}
|
|
3168
3504
|
|
|
3169
3505
|
// src/capabilities/scheduler/cancel-task.ts
|
|
3170
|
-
import { tool as
|
|
3171
|
-
import { z as
|
|
3506
|
+
import { tool as tool18 } from "ai";
|
|
3507
|
+
import { z as z18 } from "zod";
|
|
3172
3508
|
function createCancelTaskTool(scheduler) {
|
|
3173
|
-
return
|
|
3509
|
+
return tool18({
|
|
3174
3510
|
description: "Cancel and remove a scheduled task by its ID.",
|
|
3175
|
-
parameters:
|
|
3176
|
-
id:
|
|
3511
|
+
parameters: z18.object({
|
|
3512
|
+
id: z18.string().describe("ID of the scheduled task to cancel")
|
|
3177
3513
|
}),
|
|
3178
3514
|
execute: async ({ id }) => {
|
|
3179
3515
|
const manifests = scheduler.getManifests();
|
|
@@ -3189,12 +3525,12 @@ function createCancelTaskTool(scheduler) {
|
|
|
3189
3525
|
}
|
|
3190
3526
|
|
|
3191
3527
|
// src/capabilities/system/budget-status.ts
|
|
3192
|
-
import { tool as
|
|
3193
|
-
import { z as
|
|
3528
|
+
import { tool as tool19 } from "ai";
|
|
3529
|
+
import { z as z19 } from "zod";
|
|
3194
3530
|
function createBudgetStatusTool(tokenBudget) {
|
|
3195
|
-
return
|
|
3531
|
+
return tool19({
|
|
3196
3532
|
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:
|
|
3533
|
+
parameters: z19.object({}),
|
|
3198
3534
|
execute: async () => {
|
|
3199
3535
|
return tokenBudget.getStatusText();
|
|
3200
3536
|
}
|
|
@@ -3202,19 +3538,19 @@ function createBudgetStatusTool(tokenBudget) {
|
|
|
3202
3538
|
}
|
|
3203
3539
|
|
|
3204
3540
|
// src/capabilities/git/git-status.ts
|
|
3205
|
-
import { tool as
|
|
3206
|
-
import { z as
|
|
3541
|
+
import { tool as tool20 } from "ai";
|
|
3542
|
+
import { z as z20 } from "zod";
|
|
3207
3543
|
import { execSync as execSync2 } from "child_process";
|
|
3208
|
-
function createGitStatusTool() {
|
|
3209
|
-
return
|
|
3544
|
+
function createGitStatusTool(getCwd) {
|
|
3545
|
+
return tool20({
|
|
3210
3546
|
description: "Show the working tree status. Returns staged, unstaged, and untracked files.",
|
|
3211
|
-
parameters:
|
|
3212
|
-
path:
|
|
3547
|
+
parameters: z20.object({
|
|
3548
|
+
path: z20.string().optional().describe("Path to check (defaults to current directory)")
|
|
3213
3549
|
}),
|
|
3214
3550
|
execute: async ({ path: path3 }) => {
|
|
3215
3551
|
try {
|
|
3216
3552
|
const cmd = path3 ? `git -C "${path3}" status --porcelain` : "git status --porcelain";
|
|
3217
|
-
const result = execSync2(cmd, { encoding: "utf-8", timeout: 1e4 });
|
|
3553
|
+
const result = execSync2(cmd, { encoding: "utf-8", timeout: 1e4, cwd: getCwd() });
|
|
3218
3554
|
if (!result.trim()) return "Working tree clean \u2014 no changes.";
|
|
3219
3555
|
return result.trim();
|
|
3220
3556
|
} catch (err) {
|
|
@@ -3225,22 +3561,22 @@ function createGitStatusTool() {
|
|
|
3225
3561
|
}
|
|
3226
3562
|
|
|
3227
3563
|
// src/capabilities/git/git-diff.ts
|
|
3228
|
-
import { tool as
|
|
3229
|
-
import { z as
|
|
3564
|
+
import { tool as tool21 } from "ai";
|
|
3565
|
+
import { z as z21 } from "zod";
|
|
3230
3566
|
import { execSync as execSync3 } from "child_process";
|
|
3231
|
-
function createGitDiffTool() {
|
|
3232
|
-
return
|
|
3567
|
+
function createGitDiffTool(getCwd) {
|
|
3568
|
+
return tool21({
|
|
3233
3569
|
description: "Show changes between commits, commit and working tree, etc. Shows what has been modified.",
|
|
3234
|
-
parameters:
|
|
3235
|
-
path:
|
|
3236
|
-
staged:
|
|
3570
|
+
parameters: z21.object({
|
|
3571
|
+
path: z21.string().optional().describe("File or directory to diff"),
|
|
3572
|
+
staged: z21.boolean().optional().describe("Show staged changes (cached) instead of unstaged")
|
|
3237
3573
|
}),
|
|
3238
3574
|
execute: async ({ path: path3, staged }) => {
|
|
3239
3575
|
try {
|
|
3240
3576
|
let cmd = "git diff";
|
|
3241
3577
|
if (staged) cmd += " --cached";
|
|
3242
3578
|
if (path3) cmd += ` -- "${path3}"`;
|
|
3243
|
-
const result = execSync3(cmd, { encoding: "utf-8", timeout: 15e3 });
|
|
3579
|
+
const result = execSync3(cmd, { encoding: "utf-8", timeout: 15e3, cwd: getCwd() });
|
|
3244
3580
|
if (!result.trim()) return "No differences found.";
|
|
3245
3581
|
const truncated = result.length > 15e3 ? result.slice(0, 15e3) + "\n... (truncated)" : result;
|
|
3246
3582
|
return truncated;
|
|
@@ -3252,22 +3588,22 @@ function createGitDiffTool() {
|
|
|
3252
3588
|
}
|
|
3253
3589
|
|
|
3254
3590
|
// src/capabilities/git/git-log.ts
|
|
3255
|
-
import { tool as
|
|
3256
|
-
import { z as
|
|
3591
|
+
import { tool as tool22 } from "ai";
|
|
3592
|
+
import { z as z22 } from "zod";
|
|
3257
3593
|
import { execSync as execSync4 } from "child_process";
|
|
3258
|
-
function createGitLogTool() {
|
|
3259
|
-
return
|
|
3594
|
+
function createGitLogTool(getCwd) {
|
|
3595
|
+
return tool22({
|
|
3260
3596
|
description: "Show commit logs. Returns recent commit history with hash, author, date, and message.",
|
|
3261
|
-
parameters:
|
|
3262
|
-
count:
|
|
3263
|
-
path:
|
|
3597
|
+
parameters: z22.object({
|
|
3598
|
+
count: z22.number().optional().describe("Number of commits to show (default 10)"),
|
|
3599
|
+
path: z22.string().optional().describe("File or directory to show log for")
|
|
3264
3600
|
}),
|
|
3265
3601
|
execute: async ({ count, path: path3 }) => {
|
|
3266
3602
|
try {
|
|
3267
3603
|
const n = count ?? 10;
|
|
3268
3604
|
let cmd = `git log --oneline --decorate -${n}`;
|
|
3269
3605
|
if (path3) cmd += ` -- "${path3}"`;
|
|
3270
|
-
const result = execSync4(cmd, { encoding: "utf-8", timeout: 1e4 });
|
|
3606
|
+
const result = execSync4(cmd, { encoding: "utf-8", timeout: 1e4, cwd: getCwd() });
|
|
3271
3607
|
if (!result.trim()) return "No commits found.";
|
|
3272
3608
|
return result.trim();
|
|
3273
3609
|
} catch (err) {
|
|
@@ -3278,19 +3614,19 @@ function createGitLogTool() {
|
|
|
3278
3614
|
}
|
|
3279
3615
|
|
|
3280
3616
|
// src/capabilities/git/git-add.ts
|
|
3281
|
-
import { tool as
|
|
3282
|
-
import { z as
|
|
3617
|
+
import { tool as tool23 } from "ai";
|
|
3618
|
+
import { z as z23 } from "zod";
|
|
3283
3619
|
import { execSync as execSync5 } from "child_process";
|
|
3284
|
-
function createGitAddTool() {
|
|
3285
|
-
return
|
|
3620
|
+
function createGitAddTool(getCwd) {
|
|
3621
|
+
return tool23({
|
|
3286
3622
|
description: "Add file contents to the index (staging area). Prepares files for commit.",
|
|
3287
|
-
parameters:
|
|
3288
|
-
paths:
|
|
3623
|
+
parameters: z23.object({
|
|
3624
|
+
paths: z23.array(z23.string()).describe("File paths to stage")
|
|
3289
3625
|
}),
|
|
3290
3626
|
execute: async ({ paths }) => {
|
|
3291
3627
|
try {
|
|
3292
3628
|
const fileArgs = paths.map((p) => `"${p}"`).join(" ");
|
|
3293
|
-
const result = execSync5(`git add ${fileArgs}`, { encoding: "utf-8", timeout: 1e4 });
|
|
3629
|
+
const result = execSync5(`git add ${fileArgs}`, { encoding: "utf-8", timeout: 1e4, cwd: getCwd() });
|
|
3294
3630
|
return `Staged ${paths.length} file(s): ${paths.join(", ")}`;
|
|
3295
3631
|
} catch (err) {
|
|
3296
3632
|
return `Error: ${err.stderr?.trim() || err.message}`;
|
|
@@ -3300,15 +3636,15 @@ function createGitAddTool() {
|
|
|
3300
3636
|
}
|
|
3301
3637
|
|
|
3302
3638
|
// src/capabilities/git/git-commit.ts
|
|
3303
|
-
import { tool as
|
|
3304
|
-
import { z as
|
|
3639
|
+
import { tool as tool24 } from "ai";
|
|
3640
|
+
import { z as z24 } from "zod";
|
|
3305
3641
|
import { execSync as execSync6 } from "child_process";
|
|
3306
3642
|
var CO_AUTHOR = "Mercury <mercury@cosmicstack.org>";
|
|
3307
|
-
function createGitCommitTool() {
|
|
3308
|
-
return
|
|
3643
|
+
function createGitCommitTool(getCwd) {
|
|
3644
|
+
return tool24({
|
|
3309
3645
|
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:
|
|
3646
|
+
parameters: z24.object({
|
|
3647
|
+
message: z24.string().describe("Commit message")
|
|
3312
3648
|
}),
|
|
3313
3649
|
execute: async ({ message }) => {
|
|
3314
3650
|
try {
|
|
@@ -3316,7 +3652,7 @@ function createGitCommitTool() {
|
|
|
3316
3652
|
|
|
3317
3653
|
Co-authored-by: ${CO_AUTHOR}`;
|
|
3318
3654
|
const escapedMsg = fullMessage.replace(/"/g, '\\"');
|
|
3319
|
-
const result = execSync6(`git commit -m "${escapedMsg}"`, { encoding: "utf-8", timeout: 1e4 });
|
|
3655
|
+
const result = execSync6(`git commit -m "${escapedMsg}"`, { encoding: "utf-8", timeout: 1e4, cwd: getCwd() });
|
|
3320
3656
|
return result.trim() || "Committed successfully.";
|
|
3321
3657
|
} catch (err) {
|
|
3322
3658
|
const stderr = err.stderr?.trim() || "";
|
|
@@ -3330,15 +3666,15 @@ Co-authored-by: ${CO_AUTHOR}`;
|
|
|
3330
3666
|
}
|
|
3331
3667
|
|
|
3332
3668
|
// src/capabilities/git/git-push.ts
|
|
3333
|
-
import { tool as
|
|
3334
|
-
import { z as
|
|
3669
|
+
import { tool as tool25 } from "ai";
|
|
3670
|
+
import { z as z25 } from "zod";
|
|
3335
3671
|
import { execSync as execSync7 } from "child_process";
|
|
3336
|
-
function createGitPushTool(permissions) {
|
|
3337
|
-
return
|
|
3672
|
+
function createGitPushTool(permissions, getCwd) {
|
|
3673
|
+
return tool25({
|
|
3338
3674
|
description: "Push commits to a remote repository. This modifies a remote and requires approval.",
|
|
3339
|
-
parameters:
|
|
3340
|
-
remote:
|
|
3341
|
-
branch:
|
|
3675
|
+
parameters: z25.object({
|
|
3676
|
+
remote: z25.string().optional().describe("Remote name (default: origin)"),
|
|
3677
|
+
branch: z25.string().optional().describe("Branch name (default: current branch)")
|
|
3342
3678
|
}),
|
|
3343
3679
|
execute: async ({ remote, branch }) => {
|
|
3344
3680
|
const cmd = `git push ${remote || "origin"} ${branch || ""}`.trim();
|
|
@@ -3353,7 +3689,7 @@ Ask the user for permission. If they approve, try again. If they say "always", u
|
|
|
3353
3689
|
return `Error: ${check.reason}`;
|
|
3354
3690
|
}
|
|
3355
3691
|
try {
|
|
3356
|
-
const result = execSync7(cmd, { encoding: "utf-8", timeout: 3e4 });
|
|
3692
|
+
const result = execSync7(cmd, { encoding: "utf-8", timeout: 3e4, cwd: getCwd() });
|
|
3357
3693
|
return result.trim() || "Pushed successfully.";
|
|
3358
3694
|
} catch (err) {
|
|
3359
3695
|
return `Error: ${err.stderr?.trim() || err.message}`;
|
|
@@ -3363,8 +3699,8 @@ Ask the user for permission. If they approve, try again. If they say "always", u
|
|
|
3363
3699
|
}
|
|
3364
3700
|
|
|
3365
3701
|
// src/capabilities/github/create-pr.ts
|
|
3366
|
-
import { tool as
|
|
3367
|
-
import { z as
|
|
3702
|
+
import { tool as tool26 } from "ai";
|
|
3703
|
+
import { z as z26 } from "zod";
|
|
3368
3704
|
|
|
3369
3705
|
// src/utils/github.ts
|
|
3370
3706
|
var GITHUB_API = "https://api.github.com";
|
|
@@ -3419,16 +3755,16 @@ async function githubRequest(path3, options = {}) {
|
|
|
3419
3755
|
|
|
3420
3756
|
// src/capabilities/github/create-pr.ts
|
|
3421
3757
|
function createCreatePrTool() {
|
|
3422
|
-
return
|
|
3758
|
+
return tool26({
|
|
3423
3759
|
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:
|
|
3760
|
+
parameters: z26.object({
|
|
3761
|
+
owner: z26.string().describe("Repository owner (username or org)"),
|
|
3762
|
+
repo: z26.string().describe("Repository name"),
|
|
3763
|
+
title: z26.string().describe("PR title"),
|
|
3764
|
+
body: z26.string().describe("PR description (markdown supported)").default(""),
|
|
3765
|
+
head: z26.string().describe("The branch containing the changes"),
|
|
3766
|
+
base: z26.string().describe("The branch to merge into").default("main"),
|
|
3767
|
+
draft: z26.boolean().describe("Create as draft PR").default(false)
|
|
3432
3768
|
}),
|
|
3433
3769
|
execute: async ({ owner, repo, title, body, head, base, draft }) => {
|
|
3434
3770
|
try {
|
|
@@ -3447,16 +3783,16 @@ ${draft ? "(draft)" : ""} ${result.state}`;
|
|
|
3447
3783
|
}
|
|
3448
3784
|
|
|
3449
3785
|
// src/capabilities/github/review-pr.ts
|
|
3450
|
-
import { tool as
|
|
3451
|
-
import { z as
|
|
3786
|
+
import { tool as tool27 } from "ai";
|
|
3787
|
+
import { z as z27 } from "zod";
|
|
3452
3788
|
function createReviewPrTool() {
|
|
3453
|
-
return
|
|
3789
|
+
return tool27({
|
|
3454
3790
|
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:
|
|
3791
|
+
parameters: z27.object({
|
|
3792
|
+
owner: z27.string().describe("Repository owner (username or org)"),
|
|
3793
|
+
repo: z27.string().describe("Repository name"),
|
|
3794
|
+
number: z27.number().describe("PR number"),
|
|
3795
|
+
comment: z27.string().describe("Review comment to post on the PR (optional)").optional()
|
|
3460
3796
|
}),
|
|
3461
3797
|
execute: async ({ owner, repo, number, comment }) => {
|
|
3462
3798
|
try {
|
|
@@ -3522,17 +3858,17 @@ Failed to post review comment: ${err.message}`;
|
|
|
3522
3858
|
}
|
|
3523
3859
|
|
|
3524
3860
|
// src/capabilities/github/list-issues.ts
|
|
3525
|
-
import { tool as
|
|
3526
|
-
import { z as
|
|
3861
|
+
import { tool as tool28 } from "ai";
|
|
3862
|
+
import { z as z28 } from "zod";
|
|
3527
3863
|
function createListIssuesTool() {
|
|
3528
|
-
return
|
|
3864
|
+
return tool28({
|
|
3529
3865
|
description: "List GitHub issues for a repository. Requires GITHUB_TOKEN.",
|
|
3530
|
-
parameters:
|
|
3531
|
-
owner:
|
|
3532
|
-
repo:
|
|
3533
|
-
state:
|
|
3534
|
-
labels:
|
|
3535
|
-
limit:
|
|
3866
|
+
parameters: z28.object({
|
|
3867
|
+
owner: z28.string().describe("Repository owner (username or org)"),
|
|
3868
|
+
repo: z28.string().describe("Repository name"),
|
|
3869
|
+
state: z28.enum(["open", "closed", "all"]).describe("Filter by issue state").default("open"),
|
|
3870
|
+
labels: z28.string().describe("Comma-separated label names to filter by (optional)").optional(),
|
|
3871
|
+
limit: z28.number().describe("Maximum number of issues to return").default(10)
|
|
3536
3872
|
}),
|
|
3537
3873
|
execute: async ({ owner, repo, state, labels, limit }) => {
|
|
3538
3874
|
try {
|
|
@@ -3560,17 +3896,17 @@ ${lines.join("\n")}`;
|
|
|
3560
3896
|
}
|
|
3561
3897
|
|
|
3562
3898
|
// src/capabilities/github/create-issue.ts
|
|
3563
|
-
import { tool as
|
|
3564
|
-
import { z as
|
|
3899
|
+
import { tool as tool29 } from "ai";
|
|
3900
|
+
import { z as z29 } from "zod";
|
|
3565
3901
|
function createCreateIssueTool() {
|
|
3566
|
-
return
|
|
3902
|
+
return tool29({
|
|
3567
3903
|
description: "Create a new GitHub issue in a repository. Requires GITHUB_TOKEN.",
|
|
3568
|
-
parameters:
|
|
3569
|
-
owner:
|
|
3570
|
-
repo:
|
|
3571
|
-
title:
|
|
3572
|
-
body:
|
|
3573
|
-
labels:
|
|
3904
|
+
parameters: z29.object({
|
|
3905
|
+
owner: z29.string().describe("Repository owner (username or org)"),
|
|
3906
|
+
repo: z29.string().describe("Repository name"),
|
|
3907
|
+
title: z29.string().describe("Issue title"),
|
|
3908
|
+
body: z29.string().describe("Issue description (markdown supported)").default(""),
|
|
3909
|
+
labels: z29.array(z29.string()).describe("Label names to apply").optional()
|
|
3574
3910
|
}),
|
|
3575
3911
|
execute: async ({ owner, repo, title, body, labels }) => {
|
|
3576
3912
|
try {
|
|
@@ -3590,15 +3926,46 @@ function createCreateIssueTool() {
|
|
|
3590
3926
|
}
|
|
3591
3927
|
|
|
3592
3928
|
// src/capabilities/github/github-api.ts
|
|
3593
|
-
import { tool as
|
|
3594
|
-
import { z as
|
|
3929
|
+
import { tool as tool30 } from "ai";
|
|
3930
|
+
import { z as z30 } from "zod";
|
|
3931
|
+
var CO_AUTHOR_NAME = "Mercury";
|
|
3932
|
+
var CO_AUTHOR_EMAIL = "mercury@cosmicstack.org";
|
|
3933
|
+
var CO_AUTHOR_TRAILER = `Co-authored-by: ${CO_AUTHOR_NAME} <${CO_AUTHOR_EMAIL}>`;
|
|
3934
|
+
function isContentCreatePath(path3) {
|
|
3935
|
+
return /^\/repos\/[^/]+\/[^/]+\/contents\//.test(path3);
|
|
3936
|
+
}
|
|
3937
|
+
function injectCoAuthor(body) {
|
|
3938
|
+
const result = { ...body };
|
|
3939
|
+
if (typeof result.message === "string" && !result.message.includes(CO_AUTHOR_TRAILER)) {
|
|
3940
|
+
result.message += `
|
|
3941
|
+
|
|
3942
|
+
${CO_AUTHOR_TRAILER}`;
|
|
3943
|
+
}
|
|
3944
|
+
if (!result.committer || typeof result.committer !== "object") {
|
|
3945
|
+
result.committer = { name: CO_AUTHOR_NAME, email: CO_AUTHOR_EMAIL };
|
|
3946
|
+
}
|
|
3947
|
+
if (!result.author || typeof result.author !== "object") {
|
|
3948
|
+
result.author = { name: CO_AUTHOR_NAME, email: CO_AUTHOR_EMAIL };
|
|
3949
|
+
}
|
|
3950
|
+
return result;
|
|
3951
|
+
}
|
|
3595
3952
|
function createGithubApiTool() {
|
|
3596
|
-
return
|
|
3597
|
-
description:
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3953
|
+
return tool30({
|
|
3954
|
+
description: `Make a raw request to the GitHub API. GET requests (read-only) are always allowed. Write operations (POST, PUT, PATCH, DELETE) may require user approval.
|
|
3955
|
+
|
|
3956
|
+
Common operations you can perform:
|
|
3957
|
+
- Push a file: PUT /repos/{owner}/{repo}/contents/{path} \u2014 body must include "message" (commit message) and "content" (base64-encoded file). For updates, also include "sha" from the current file. Co-authored-by Mercury is automatically included.
|
|
3958
|
+
- Delete a file: DELETE /repos/{owner}/{repo}/contents/{path} \u2014 body must include "message" and "sha".
|
|
3959
|
+
- List branches: GET /repos/{owner}/{repo}/branches
|
|
3960
|
+
- Get file contents: GET /repos/{owner}/{repo}/contents/{path}
|
|
3961
|
+
- Search code: GET /search/code?q={query}
|
|
3962
|
+
- Any other GitHub API v3 endpoint.
|
|
3963
|
+
|
|
3964
|
+
IMPORTANT: When the user wants to push code or files to GitHub and git push fails (auth issues, no SSH key, etc.), use PUT /repos/{owner}/{repo}/contents/{path} to create or update files directly through the API. This bypasses local git and creates a commit with Mercury as co-author.`,
|
|
3965
|
+
parameters: z30.object({
|
|
3966
|
+
path: z30.string().describe("Full API path (e.g., /repos/owner/repo/issues or /repos/owner/repo/contents/path/to/file)"),
|
|
3967
|
+
method: z30.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).describe("HTTP method").default("GET"),
|
|
3968
|
+
body: z30.string().describe("JSON body for write requests (as a JSON string)").optional()
|
|
3602
3969
|
}),
|
|
3603
3970
|
execute: async ({ path: path3, method, body }) => {
|
|
3604
3971
|
try {
|
|
@@ -3610,6 +3977,9 @@ function createGithubApiTool() {
|
|
|
3610
3977
|
return "Error: body must be valid JSON.";
|
|
3611
3978
|
}
|
|
3612
3979
|
}
|
|
3980
|
+
if (parsedBody && isContentCreatePath(path3) && (method === "PUT" || method === "POST" || method === "PATCH")) {
|
|
3981
|
+
parsedBody = injectCoAuthor(parsedBody);
|
|
3982
|
+
}
|
|
3613
3983
|
const result = await githubRequest(path3, {
|
|
3614
3984
|
method,
|
|
3615
3985
|
body: parsedBody
|
|
@@ -3625,8 +3995,8 @@ function createGithubApiTool() {
|
|
|
3625
3995
|
}
|
|
3626
3996
|
|
|
3627
3997
|
// src/capabilities/web/fetch-url.ts
|
|
3628
|
-
import { tool as
|
|
3629
|
-
import { z as
|
|
3998
|
+
import { tool as tool31 } from "ai";
|
|
3999
|
+
import { z as z31 } from "zod";
|
|
3630
4000
|
var MAX_CONTENT_LENGTH = 15e3;
|
|
3631
4001
|
function stripHtml(html) {
|
|
3632
4002
|
let text = html;
|
|
@@ -3659,11 +4029,11 @@ function stripHtml(html) {
|
|
|
3659
4029
|
return text;
|
|
3660
4030
|
}
|
|
3661
4031
|
function createFetchUrlTool() {
|
|
3662
|
-
return
|
|
4032
|
+
return tool31({
|
|
3663
4033
|
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:
|
|
4034
|
+
parameters: z31.object({
|
|
4035
|
+
url: z31.string().describe("The URL to fetch"),
|
|
4036
|
+
format: z31.enum(["text", "markdown"]).optional().describe("Output format (default: markdown)")
|
|
3667
4037
|
}),
|
|
3668
4038
|
execute: async ({ url, format }) => {
|
|
3669
4039
|
const outputFormat = format ?? "markdown";
|
|
@@ -3715,9 +4085,11 @@ var CapabilityRegistry = class {
|
|
|
3715
4085
|
scheduler;
|
|
3716
4086
|
tokenBudget;
|
|
3717
4087
|
sendFileHandler;
|
|
4088
|
+
sendMessageHandler;
|
|
3718
4089
|
currentChannelId = "cli";
|
|
3719
4090
|
currentChannelType = "cli";
|
|
3720
4091
|
chatCommandContext;
|
|
4092
|
+
currentCwd = process.cwd();
|
|
3721
4093
|
constructor(skillLoader, scheduler, tokenBudget) {
|
|
3722
4094
|
this.permissions = new PermissionManager();
|
|
3723
4095
|
this.skillLoader = skillLoader;
|
|
@@ -3737,26 +4109,40 @@ var CapabilityRegistry = class {
|
|
|
3737
4109
|
getChannelContext() {
|
|
3738
4110
|
return { channelId: this.currentChannelId, channelType: this.currentChannelType };
|
|
3739
4111
|
}
|
|
4112
|
+
getCwd() {
|
|
4113
|
+
return this.currentCwd;
|
|
4114
|
+
}
|
|
4115
|
+
setCwd(dir) {
|
|
4116
|
+
this.currentCwd = dir;
|
|
4117
|
+
}
|
|
3740
4118
|
setSendFileHandler(handler) {
|
|
3741
4119
|
this.sendFileHandler = handler;
|
|
3742
4120
|
}
|
|
4121
|
+
setSendMessageHandler(handler) {
|
|
4122
|
+
this.sendMessageHandler = handler;
|
|
4123
|
+
}
|
|
3743
4124
|
registerAll() {
|
|
3744
4125
|
const manifest = this.permissions.getManifest();
|
|
3745
4126
|
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);
|
|
4127
|
+
this.tools.read_file = createReadFileTool(this.permissions, () => this.getCwd());
|
|
4128
|
+
this.tools.write_file = createWriteFileTool(this.permissions, () => this.getCwd());
|
|
4129
|
+
this.tools.create_file = createCreateFileTool(this.permissions, () => this.getCwd());
|
|
4130
|
+
this.tools.list_dir = createListDirTool(this.permissions, () => this.getCwd());
|
|
4131
|
+
this.tools.delete_file = createDeleteFileTool(this.permissions, () => this.getCwd());
|
|
4132
|
+
this.tools.edit_file = createEditFileTool(this.permissions, () => this.getCwd());
|
|
3752
4133
|
if (this.sendFileHandler) {
|
|
3753
|
-
this.tools.send_file = createSendFileTool(this.permissions, this.sendFileHandler);
|
|
4134
|
+
this.tools.send_file = createSendFileTool(this.permissions, () => this.getCwd(), this.sendFileHandler);
|
|
3754
4135
|
}
|
|
3755
|
-
this.tools.approve_scope = createApproveScopeTool(this.permissions);
|
|
4136
|
+
this.tools.approve_scope = createApproveScopeTool(this.permissions, () => this.getCwd());
|
|
3756
4137
|
logger.info("Filesystem tools registered");
|
|
3757
4138
|
}
|
|
4139
|
+
if (this.sendMessageHandler) {
|
|
4140
|
+
this.tools.send_message = createSendMessageTool(this.sendMessageHandler);
|
|
4141
|
+
logger.info("Messaging tool registered");
|
|
4142
|
+
}
|
|
3758
4143
|
if (manifest.capabilities.shell.enabled) {
|
|
3759
|
-
this.tools.run_command = createRunCommandTool(this.permissions);
|
|
4144
|
+
this.tools.run_command = createRunCommandTool(this.permissions, () => this.getCwd(), (dir) => this.setCwd(dir));
|
|
4145
|
+
this.tools.cd = createCdTool(() => this.getCwd(), (dir) => this.setCwd(dir));
|
|
3760
4146
|
this.tools.approve_command = createApproveCommandTool(this.permissions);
|
|
3761
4147
|
logger.info("Shell tools registered");
|
|
3762
4148
|
}
|
|
@@ -3777,12 +4163,12 @@ var CapabilityRegistry = class {
|
|
|
3777
4163
|
logger.info("Budget tool registered");
|
|
3778
4164
|
}
|
|
3779
4165
|
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);
|
|
4166
|
+
this.tools.git_status = createGitStatusTool(() => this.getCwd());
|
|
4167
|
+
this.tools.git_diff = createGitDiffTool(() => this.getCwd());
|
|
4168
|
+
this.tools.git_log = createGitLogTool(() => this.getCwd());
|
|
4169
|
+
this.tools.git_add = createGitAddTool(() => this.getCwd());
|
|
4170
|
+
this.tools.git_commit = createGitCommitTool(() => this.getCwd());
|
|
4171
|
+
this.tools.git_push = createGitPushTool(this.permissions, () => this.getCwd());
|
|
3786
4172
|
logger.info("Git tools registered");
|
|
3787
4173
|
}
|
|
3788
4174
|
if (isGitHubConfigured()) {
|
|
@@ -3808,7 +4194,7 @@ var CapabilityRegistry = class {
|
|
|
3808
4194
|
};
|
|
3809
4195
|
|
|
3810
4196
|
// src/skills/loader.ts
|
|
3811
|
-
import { existsSync as
|
|
4197
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10, readdirSync as readdirSync3, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
|
|
3812
4198
|
import { join as join8 } from "path";
|
|
3813
4199
|
import { parse as parseYaml5 } from "yaml";
|
|
3814
4200
|
var SKILL_FILE = "SKILL.md";
|
|
@@ -3838,7 +4224,7 @@ var SkillLoader = class {
|
|
|
3838
4224
|
discover() {
|
|
3839
4225
|
this.discovered.clear();
|
|
3840
4226
|
this.loaded.clear();
|
|
3841
|
-
if (!
|
|
4227
|
+
if (!existsSync15(this.skillsDir)) {
|
|
3842
4228
|
mkdirSync8(this.skillsDir, { recursive: true });
|
|
3843
4229
|
this.seedTemplate();
|
|
3844
4230
|
return [];
|
|
@@ -3847,7 +4233,7 @@ var SkillLoader = class {
|
|
|
3847
4233
|
for (const entry of entries) {
|
|
3848
4234
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
3849
4235
|
const skillPath = join8(this.skillsDir, entry.name, SKILL_FILE);
|
|
3850
|
-
if (!
|
|
4236
|
+
if (!existsSync15(skillPath)) continue;
|
|
3851
4237
|
try {
|
|
3852
4238
|
const raw = readFileSync10(skillPath, "utf-8");
|
|
3853
4239
|
const parsed = parseSkillMd(raw);
|
|
@@ -3869,7 +4255,7 @@ var SkillLoader = class {
|
|
|
3869
4255
|
for (const entry of readdirSync3(this.skillsDir, { withFileTypes: true })) {
|
|
3870
4256
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
3871
4257
|
const skillPath = join8(this.skillsDir, entry.name, SKILL_FILE);
|
|
3872
|
-
if (!
|
|
4258
|
+
if (!existsSync15(skillPath)) continue;
|
|
3873
4259
|
try {
|
|
3874
4260
|
const raw = readFileSync10(skillPath, "utf-8");
|
|
3875
4261
|
const parsed = parseSkillMd(raw);
|
|
@@ -3878,8 +4264,8 @@ var SkillLoader = class {
|
|
|
3878
4264
|
const skill = {
|
|
3879
4265
|
...parsed.meta,
|
|
3880
4266
|
instructions: parsed.instructions,
|
|
3881
|
-
scriptsDir:
|
|
3882
|
-
referencesDir:
|
|
4267
|
+
scriptsDir: existsSync15(join8(skillDir, "scripts")) ? join8(skillDir, "scripts") : void 0,
|
|
4268
|
+
referencesDir: existsSync15(join8(skillDir, "references")) ? join8(skillDir, "references") : void 0
|
|
3883
4269
|
};
|
|
3884
4270
|
this.loaded.set(name, skill);
|
|
3885
4271
|
return skill;
|
|
@@ -3900,7 +4286,7 @@ var SkillLoader = class {
|
|
|
3900
4286
|
}
|
|
3901
4287
|
saveSkill(name, content) {
|
|
3902
4288
|
const skillDir = join8(this.skillsDir, name);
|
|
3903
|
-
if (!
|
|
4289
|
+
if (!existsSync15(skillDir)) {
|
|
3904
4290
|
mkdirSync8(skillDir, { recursive: true });
|
|
3905
4291
|
}
|
|
3906
4292
|
writeFileSync10(join8(skillDir, SKILL_FILE), content, "utf-8");
|
|
@@ -3963,6 +4349,7 @@ function getManual() {
|
|
|
3963
4349
|
["edit_file", "Replace specific text in a file", "path, old_string, new_string"],
|
|
3964
4350
|
["list_dir", "List directory contents", "path"],
|
|
3965
4351
|
["delete_file", "Delete a file", "path"],
|
|
4352
|
+
["send_message", "Send a message to the paired Telegram owner", "content"],
|
|
3966
4353
|
["run_command", "Execute a shell command", "command"],
|
|
3967
4354
|
["approve_command", "Permanently approve a command type", 'command (e.g. "curl")'],
|
|
3968
4355
|
["fetch_url", "Fetch a URL and return content", "url, format? (text/markdown)"],
|
|
@@ -3999,6 +4386,7 @@ function getManual() {
|
|
|
3999
4386
|
["mercury doctor", "Reconfigure settings (Enter keeps current)"],
|
|
4000
4387
|
["mercury setup", "Re-run the setup wizard"],
|
|
4001
4388
|
["mercury status", "Show config and daemon status"],
|
|
4389
|
+
["mercury telegram unpair", "Clear the paired Telegram owner"],
|
|
4002
4390
|
["mercury help", "Show this manual"],
|
|
4003
4391
|
["mercury service install", "Install as system service (auto-start)"],
|
|
4004
4392
|
["mercury service uninstall", "Uninstall system service"],
|
|
@@ -4013,13 +4401,16 @@ function getManual() {
|
|
|
4013
4401
|
sections.push(chalk3.dim(" Type these during a conversation (no API calls)."));
|
|
4014
4402
|
sections.push("");
|
|
4015
4403
|
const chat = [
|
|
4404
|
+
["/start", "Pair this Telegram account to Mercury"],
|
|
4405
|
+
["/pair", "Pair this Telegram account to Mercury"],
|
|
4016
4406
|
["/help", "Show this manual"],
|
|
4017
4407
|
["/status", "Show config and budget info"],
|
|
4018
4408
|
["/tools", "List currently loaded tools"],
|
|
4019
4409
|
["/skills", "List installed skills"],
|
|
4020
4410
|
["/stream", "Toggle text streaming on/off (Telegram)"],
|
|
4021
4411
|
["/stream on", "Enable streaming (live text updates)"],
|
|
4022
|
-
["/stream off", "Disable streaming (single message)"]
|
|
4412
|
+
["/stream off", "Disable streaming (single message)"],
|
|
4413
|
+
["/unpair", "Remove Telegram pairing for this Mercury instance"]
|
|
4023
4414
|
];
|
|
4024
4415
|
for (const [cmd, desc] of chat) {
|
|
4025
4416
|
sections.push(` ${chalk3.white(cmd.padEnd(16))} ${desc}`);
|
|
@@ -4082,7 +4473,7 @@ function getManual() {
|
|
|
4082
4473
|
|
|
4083
4474
|
// src/cli/daemon.ts
|
|
4084
4475
|
import { spawn } from "child_process";
|
|
4085
|
-
import { existsSync as
|
|
4476
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, mkdirSync as mkdirSync9, openSync } from "fs";
|
|
4086
4477
|
import { join as join9 } from "path";
|
|
4087
4478
|
import process2 from "process";
|
|
4088
4479
|
import chalk4 from "chalk";
|
|
@@ -4096,7 +4487,7 @@ function logPath() {
|
|
|
4096
4487
|
}
|
|
4097
4488
|
function readPid() {
|
|
4098
4489
|
const path3 = pidPath();
|
|
4099
|
-
if (!
|
|
4490
|
+
if (!existsSync16(path3)) return null;
|
|
4100
4491
|
try {
|
|
4101
4492
|
const pid = parseInt(readFileSync11(path3, "utf-8").trim(), 10);
|
|
4102
4493
|
if (isNaN(pid)) return null;
|
|
@@ -4133,7 +4524,7 @@ function startBackground() {
|
|
|
4133
4524
|
}
|
|
4134
4525
|
}
|
|
4135
4526
|
const home = getMercuryHome();
|
|
4136
|
-
if (!
|
|
4527
|
+
if (!existsSync16(home)) {
|
|
4137
4528
|
mkdirSync9(home, { recursive: true });
|
|
4138
4529
|
}
|
|
4139
4530
|
const logFile = logPath();
|
|
@@ -4214,7 +4605,7 @@ function restartDaemon() {
|
|
|
4214
4605
|
}
|
|
4215
4606
|
function showLogs() {
|
|
4216
4607
|
const logFile = logPath();
|
|
4217
|
-
if (!
|
|
4608
|
+
if (!existsSync16(logFile)) {
|
|
4218
4609
|
console.log(chalk4.dim(" No daemon log file found."));
|
|
4219
4610
|
console.log("");
|
|
4220
4611
|
return;
|
|
@@ -4236,7 +4627,7 @@ function tryAutoDaemonize() {
|
|
|
4236
4627
|
}
|
|
4237
4628
|
}
|
|
4238
4629
|
const home = getMercuryHome();
|
|
4239
|
-
if (!
|
|
4630
|
+
if (!existsSync16(home)) {
|
|
4240
4631
|
mkdirSync9(home, { recursive: true });
|
|
4241
4632
|
}
|
|
4242
4633
|
const logFile = logPath();
|
|
@@ -4260,7 +4651,7 @@ function tryAutoDaemonize() {
|
|
|
4260
4651
|
}
|
|
4261
4652
|
|
|
4262
4653
|
// src/cli/service.ts
|
|
4263
|
-
import { existsSync as
|
|
4654
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4 } from "fs";
|
|
4264
4655
|
import { join as join10 } from "path";
|
|
4265
4656
|
import { homedir as homedir3 } from "os";
|
|
4266
4657
|
import chalk5 from "chalk";
|
|
@@ -4270,9 +4661,9 @@ var WIN_TASK_NAME = "MercuryAgent";
|
|
|
4270
4661
|
function isServiceInstalled() {
|
|
4271
4662
|
const platform = process.platform;
|
|
4272
4663
|
if (platform === "darwin") {
|
|
4273
|
-
return
|
|
4664
|
+
return existsSync17(join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist"));
|
|
4274
4665
|
} else if (platform === "linux") {
|
|
4275
|
-
return
|
|
4666
|
+
return existsSync17(join10(homedir3(), ".config", "systemd", "user", "mercury.service"));
|
|
4276
4667
|
} else if (platform === "win32") {
|
|
4277
4668
|
try {
|
|
4278
4669
|
execSync8(`schtasks /query /tn "${WIN_TASK_NAME}"`, { stdio: "pipe", shell: "cmd.exe" });
|
|
@@ -4328,7 +4719,7 @@ function showServiceStatus() {
|
|
|
4328
4719
|
function installMac() {
|
|
4329
4720
|
const plistDir = join10(homedir3(), "Library", "LaunchAgents");
|
|
4330
4721
|
const plistPath = join10(plistDir, "com.cosmicstack.mercury.plist");
|
|
4331
|
-
if (!
|
|
4722
|
+
if (!existsSync17(plistDir)) {
|
|
4332
4723
|
mkdirSync10(plistDir, { recursive: true });
|
|
4333
4724
|
}
|
|
4334
4725
|
const nodeBin = getNodeBinPath();
|
|
@@ -4389,7 +4780,7 @@ function installMac() {
|
|
|
4389
4780
|
}
|
|
4390
4781
|
function uninstallMac() {
|
|
4391
4782
|
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
4392
|
-
if (!
|
|
4783
|
+
if (!existsSync17(plistPath)) {
|
|
4393
4784
|
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4394
4785
|
console.log("");
|
|
4395
4786
|
process.exit(0);
|
|
@@ -4410,7 +4801,7 @@ function uninstallMac() {
|
|
|
4410
4801
|
}
|
|
4411
4802
|
function showMacStatus() {
|
|
4412
4803
|
const plistPath = join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
4413
|
-
if (!
|
|
4804
|
+
if (!existsSync17(plistPath)) {
|
|
4414
4805
|
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4415
4806
|
console.log(chalk5.dim(" Run `mercury service install` to set it up."));
|
|
4416
4807
|
console.log("");
|
|
@@ -4428,7 +4819,7 @@ function showMacStatus() {
|
|
|
4428
4819
|
}
|
|
4429
4820
|
function installLinux() {
|
|
4430
4821
|
const systemdDir = join10(homedir3(), ".config", "systemd", "user");
|
|
4431
|
-
if (!
|
|
4822
|
+
if (!existsSync17(systemdDir)) {
|
|
4432
4823
|
mkdirSync10(systemdDir, { recursive: true });
|
|
4433
4824
|
}
|
|
4434
4825
|
const servicePath = join10(systemdDir, "mercury.service");
|
|
@@ -4480,7 +4871,7 @@ WantedBy=default.target`;
|
|
|
4480
4871
|
}
|
|
4481
4872
|
function uninstallLinux() {
|
|
4482
4873
|
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4483
|
-
if (!
|
|
4874
|
+
if (!existsSync17(servicePath)) {
|
|
4484
4875
|
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4485
4876
|
console.log("");
|
|
4486
4877
|
process.exit(0);
|
|
@@ -4506,7 +4897,7 @@ function uninstallLinux() {
|
|
|
4506
4897
|
}
|
|
4507
4898
|
function showLinuxStatus() {
|
|
4508
4899
|
const servicePath = join10(homedir3(), ".config", "systemd", "user", "mercury.service");
|
|
4509
|
-
if (!
|
|
4900
|
+
if (!existsSync17(servicePath)) {
|
|
4510
4901
|
console.log(chalk5.yellow(" Mercury service is not installed."));
|
|
4511
4902
|
console.log(chalk5.dim(" Run `mercury service install` to set it up."));
|
|
4512
4903
|
console.log("");
|
|
@@ -4605,7 +4996,7 @@ async function runWithWatchdog(agentFn) {
|
|
|
4605
4996
|
await attempt();
|
|
4606
4997
|
}
|
|
4607
4998
|
function sleep(ms) {
|
|
4608
|
-
return new Promise((
|
|
4999
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
4609
5000
|
}
|
|
4610
5001
|
|
|
4611
5002
|
// src/index.ts
|
|
@@ -4644,10 +5035,10 @@ function splashScreen() {
|
|
|
4644
5035
|
}
|
|
4645
5036
|
async function ask(prompt) {
|
|
4646
5037
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
4647
|
-
return new Promise((
|
|
5038
|
+
return new Promise((resolve13) => {
|
|
4648
5039
|
rl.question(prompt, (answer) => {
|
|
4649
5040
|
rl.close();
|
|
4650
|
-
|
|
5041
|
+
resolve13(answer.trim());
|
|
4651
5042
|
});
|
|
4652
5043
|
});
|
|
4653
5044
|
}
|
|
@@ -4656,10 +5047,149 @@ function maskKey(key) {
|
|
|
4656
5047
|
if (key.length <= 8) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
4657
5048
|
return key.slice(0, 4) + "\u2022\u2022\u2022\u2022" + key.slice(-4);
|
|
4658
5049
|
}
|
|
5050
|
+
var PROVIDER_OPTIONS = [
|
|
5051
|
+
{ key: "deepseek", label: "DeepSeek" },
|
|
5052
|
+
{ key: "openai", label: "OpenAI" },
|
|
5053
|
+
{ key: "anthropic", label: "Anthropic" },
|
|
5054
|
+
{ key: "grok", label: "Grok (xAI)" },
|
|
5055
|
+
{ key: "ollamaCloud", label: "Ollama Cloud" },
|
|
5056
|
+
{ key: "ollamaLocal", label: "Ollama Local" }
|
|
5057
|
+
];
|
|
5058
|
+
function getConfiguredProviderNames(config) {
|
|
5059
|
+
return PROVIDER_OPTIONS.map((option) => option.key).filter((key) => isProviderConfigured(config.providers[key]));
|
|
5060
|
+
}
|
|
5061
|
+
function getProviderLabel(name) {
|
|
5062
|
+
return PROVIDER_OPTIONS.find((option) => option.key === name)?.label || name;
|
|
5063
|
+
}
|
|
5064
|
+
function parseProviderSelection(input) {
|
|
5065
|
+
const values = input.split(/[\s,]+/).map((value) => value.trim()).filter(Boolean);
|
|
5066
|
+
if (values.length === 0) return [];
|
|
5067
|
+
const selected = [];
|
|
5068
|
+
for (const value of values) {
|
|
5069
|
+
const index = parseInt(value, 10);
|
|
5070
|
+
if (isNaN(index) || index < 1 || index > PROVIDER_OPTIONS.length) {
|
|
5071
|
+
return null;
|
|
5072
|
+
}
|
|
5073
|
+
const provider = PROVIDER_OPTIONS[index - 1].key;
|
|
5074
|
+
if (!selected.includes(provider)) {
|
|
5075
|
+
selected.push(provider);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
return selected;
|
|
5079
|
+
}
|
|
5080
|
+
async function chooseProvidersToConfigure(config, isReconfig) {
|
|
5081
|
+
const configured = getConfiguredProviderNames(config);
|
|
5082
|
+
while (true) {
|
|
5083
|
+
for (let i = 0; i < PROVIDER_OPTIONS.length; i++) {
|
|
5084
|
+
const option = PROVIDER_OPTIONS[i];
|
|
5085
|
+
const status = configured.includes(option.key) ? " (configured)" : "";
|
|
5086
|
+
console.log(chalk6.white(` ${i + 1}. ${option.label}${status}`));
|
|
5087
|
+
}
|
|
5088
|
+
console.log("");
|
|
5089
|
+
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]: ");
|
|
5090
|
+
const input = await ask(prompt);
|
|
5091
|
+
const parsed = parseProviderSelection(input);
|
|
5092
|
+
if (parsed === null) {
|
|
5093
|
+
console.log(chalk6.red(" Please choose valid provider numbers, like `1` or `1,3,5`."));
|
|
5094
|
+
console.log("");
|
|
5095
|
+
continue;
|
|
5096
|
+
}
|
|
5097
|
+
if (parsed.length > 0) return parsed;
|
|
5098
|
+
if (!isReconfig) return ["deepseek"];
|
|
5099
|
+
return configured.length > 0 ? configured : ["deepseek"];
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
async function chooseDefaultProvider(config) {
|
|
5103
|
+
const configured = getConfiguredProviderNames(config);
|
|
5104
|
+
if (configured.length === 0) {
|
|
5105
|
+
return;
|
|
5106
|
+
}
|
|
5107
|
+
if (configured.length === 1) {
|
|
5108
|
+
config.providers.default = configured[0];
|
|
5109
|
+
console.log(chalk6.dim(` Default provider set to ${getProviderLabel(configured[0])}`));
|
|
5110
|
+
return;
|
|
5111
|
+
}
|
|
5112
|
+
const suggested = configured.includes("deepseek") ? "deepseek" : configured[0];
|
|
5113
|
+
console.log("");
|
|
5114
|
+
console.log(chalk6.bold.white(" Default Provider"));
|
|
5115
|
+
console.log(chalk6.dim(" Select the LLM provider Mercury should use first."));
|
|
5116
|
+
console.log("");
|
|
5117
|
+
for (let i = 0; i < configured.length; i++) {
|
|
5118
|
+
const provider = configured[i];
|
|
5119
|
+
const recommended = provider === suggested ? " (recommended)" : "";
|
|
5120
|
+
const current = provider === config.providers.default ? " (current)" : "";
|
|
5121
|
+
console.log(chalk6.white(` ${i + 1}. ${getProviderLabel(provider)}${recommended}${current}`));
|
|
5122
|
+
}
|
|
5123
|
+
console.log("");
|
|
5124
|
+
while (true) {
|
|
5125
|
+
const choice = await ask(chalk6.white(` Choose [1-${configured.length}] [Enter for ${getProviderLabel(suggested)}]: `));
|
|
5126
|
+
if (!choice) {
|
|
5127
|
+
config.providers.default = suggested;
|
|
5128
|
+
return;
|
|
5129
|
+
}
|
|
5130
|
+
const num = parseInt(choice, 10);
|
|
5131
|
+
if (num >= 1 && num <= configured.length) {
|
|
5132
|
+
config.providers.default = configured[num - 1];
|
|
5133
|
+
return;
|
|
5134
|
+
}
|
|
5135
|
+
console.log(chalk6.red(" Please choose a valid number from the list above."));
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
function looksLikeToken(value, minLength = 20) {
|
|
5139
|
+
return value.length >= minLength && !/\s/.test(value) && /[A-Za-z]/.test(value) && /\d/.test(value);
|
|
5140
|
+
}
|
|
5141
|
+
function validateApiKey(provider, value) {
|
|
5142
|
+
if (provider === "openai") {
|
|
5143
|
+
return /^sk-(proj-|svcacct-)?[A-Za-z0-9_-]{16,}$/i.test(value) ? null : "OpenAI keys must start with `sk-`, `sk-proj-`, or `sk-svcacct-`.";
|
|
5144
|
+
}
|
|
5145
|
+
if (provider === "anthropic") {
|
|
5146
|
+
return /^sk-ant-[A-Za-z0-9_-]{16,}$/i.test(value) ? null : "Anthropic keys must start with `sk-ant-`.";
|
|
5147
|
+
}
|
|
5148
|
+
if (provider === "deepseek") {
|
|
5149
|
+
return /^sk-[A-Za-z0-9_-]{16,}$/i.test(value) ? null : "DeepSeek keys must start with `sk-`.";
|
|
5150
|
+
}
|
|
5151
|
+
if (provider === "grok") {
|
|
5152
|
+
return looksLikeToken(value) ? null : "Grok keys must look like a real API token: long, no spaces, and not plain text.";
|
|
5153
|
+
}
|
|
5154
|
+
if (provider === "ollamaCloud") {
|
|
5155
|
+
return looksLikeToken(value) ? null : "Ollama Cloud keys must look like a real API token: long, no spaces, and not plain text.";
|
|
5156
|
+
}
|
|
5157
|
+
return null;
|
|
5158
|
+
}
|
|
5159
|
+
function validateBaseUrl(value) {
|
|
5160
|
+
try {
|
|
5161
|
+
const parsed = new URL(value);
|
|
5162
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
5163
|
+
return "Base URL must start with http:// or https://.";
|
|
5164
|
+
}
|
|
5165
|
+
return null;
|
|
5166
|
+
} catch {
|
|
5167
|
+
return "Please enter a valid URL.";
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
function validateModelName(value) {
|
|
5171
|
+
if (!value.trim()) return "Model name is required.";
|
|
5172
|
+
if (/\s/.test(value)) return "Model name cannot contain spaces.";
|
|
5173
|
+
return null;
|
|
5174
|
+
}
|
|
5175
|
+
async function promptValidatedValue(prompt, validator, existingValue, options) {
|
|
5176
|
+
while (true) {
|
|
5177
|
+
const value = await ask(prompt);
|
|
5178
|
+
if (!value) {
|
|
5179
|
+
if (existingValue) return existingValue;
|
|
5180
|
+
if (options?.allowSkip) return void 0;
|
|
5181
|
+
console.log(chalk6.red(" A value is required here."));
|
|
5182
|
+
continue;
|
|
5183
|
+
}
|
|
5184
|
+
const error = validator(value);
|
|
5185
|
+
if (!error) return value;
|
|
5186
|
+
console.log(chalk6.red(` ${error}`));
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
4659
5189
|
function appendToEnv(key, value) {
|
|
4660
5190
|
const envPath = join11(getMercuryHome(), ".env");
|
|
4661
5191
|
let envContent = "";
|
|
4662
|
-
if (
|
|
5192
|
+
if (existsSync18(envPath)) {
|
|
4663
5193
|
envContent = readFileSync12(envPath, "utf-8");
|
|
4664
5194
|
}
|
|
4665
5195
|
const lines = envContent.split("\n").filter((l) => !l.startsWith(`${key}=`) && l.trim() !== "");
|
|
@@ -4709,49 +5239,108 @@ async function configure(existingConfig) {
|
|
|
4709
5239
|
console.log("");
|
|
4710
5240
|
console.log(chalk6.bold.white(" LLM Providers"));
|
|
4711
5241
|
if (isReconfig) {
|
|
4712
|
-
console.log(chalk6.dim("
|
|
5242
|
+
console.log(chalk6.dim(" Choose which providers to configure now. Existing values are shown where available."));
|
|
4713
5243
|
} else {
|
|
4714
|
-
console.log(chalk6.dim("
|
|
5244
|
+
console.log(chalk6.dim(" Choose one or more providers. Press Enter to configure DeepSeek by default."));
|
|
4715
5245
|
}
|
|
4716
5246
|
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) {
|
|
5247
|
+
while (true) {
|
|
5248
|
+
const selectedProviders = await chooseProvidersToConfigure(config, isReconfig);
|
|
4738
5249
|
console.log("");
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
5250
|
+
for (const provider of selectedProviders) {
|
|
5251
|
+
if (provider === "deepseek") {
|
|
5252
|
+
const mask = isReconfig && config.providers.deepseek.apiKey ? ` [${maskKey(config.providers.deepseek.apiKey)}]` : "";
|
|
5253
|
+
const key = await promptValidatedValue(
|
|
5254
|
+
chalk6.white(` DeepSeek API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
5255
|
+
(value) => validateApiKey("deepseek", value),
|
|
5256
|
+
isReconfig ? config.providers.deepseek.apiKey : void 0,
|
|
5257
|
+
{ allowSkip: true }
|
|
5258
|
+
);
|
|
5259
|
+
if (key) {
|
|
5260
|
+
config.providers.deepseek.apiKey = key;
|
|
5261
|
+
config.providers.deepseek.enabled = true;
|
|
5262
|
+
}
|
|
5263
|
+
continue;
|
|
5264
|
+
}
|
|
5265
|
+
if (provider === "openai") {
|
|
5266
|
+
const mask = isReconfig && config.providers.openai.apiKey ? ` [${maskKey(config.providers.openai.apiKey)}]` : "";
|
|
5267
|
+
const key = await promptValidatedValue(
|
|
5268
|
+
chalk6.white(` OpenAI API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
5269
|
+
(value) => validateApiKey("openai", value),
|
|
5270
|
+
isReconfig ? config.providers.openai.apiKey : void 0,
|
|
5271
|
+
{ allowSkip: true }
|
|
5272
|
+
);
|
|
5273
|
+
if (key) {
|
|
5274
|
+
config.providers.openai.apiKey = key;
|
|
5275
|
+
config.providers.openai.enabled = true;
|
|
5276
|
+
}
|
|
5277
|
+
continue;
|
|
5278
|
+
}
|
|
5279
|
+
if (provider === "anthropic") {
|
|
5280
|
+
const mask = isReconfig && config.providers.anthropic.apiKey ? ` [${maskKey(config.providers.anthropic.apiKey)}]` : "";
|
|
5281
|
+
const key = await promptValidatedValue(
|
|
5282
|
+
chalk6.white(` Anthropic API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
5283
|
+
(value) => validateApiKey("anthropic", value),
|
|
5284
|
+
isReconfig ? config.providers.anthropic.apiKey : void 0,
|
|
5285
|
+
{ allowSkip: true }
|
|
5286
|
+
);
|
|
5287
|
+
if (key) {
|
|
5288
|
+
config.providers.anthropic.apiKey = key;
|
|
5289
|
+
config.providers.anthropic.enabled = true;
|
|
5290
|
+
}
|
|
5291
|
+
continue;
|
|
5292
|
+
}
|
|
5293
|
+
if (provider === "grok") {
|
|
5294
|
+
const mask = isReconfig && config.providers.grok.apiKey ? ` [${maskKey(config.providers.grok.apiKey)}]` : "";
|
|
5295
|
+
const key = await promptValidatedValue(
|
|
5296
|
+
chalk6.white(` Grok API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
5297
|
+
(value) => validateApiKey("grok", value),
|
|
5298
|
+
isReconfig ? config.providers.grok.apiKey : void 0,
|
|
5299
|
+
{ allowSkip: true }
|
|
5300
|
+
);
|
|
5301
|
+
if (key) {
|
|
5302
|
+
config.providers.grok.apiKey = key;
|
|
5303
|
+
config.providers.grok.enabled = true;
|
|
5304
|
+
}
|
|
5305
|
+
continue;
|
|
5306
|
+
}
|
|
5307
|
+
if (provider === "ollamaCloud") {
|
|
5308
|
+
const mask = isReconfig && config.providers.ollamaCloud.apiKey ? ` [${maskKey(config.providers.ollamaCloud.apiKey)}]` : "";
|
|
5309
|
+
const key = await promptValidatedValue(
|
|
5310
|
+
chalk6.white(` Ollama Cloud API key${mask}${isReconfig ? "" : " (Enter to skip)"}: `),
|
|
5311
|
+
(value) => validateApiKey("ollamaCloud", value),
|
|
5312
|
+
isReconfig ? config.providers.ollamaCloud.apiKey : void 0,
|
|
5313
|
+
{ allowSkip: true }
|
|
5314
|
+
);
|
|
5315
|
+
if (key) {
|
|
5316
|
+
config.providers.ollamaCloud.apiKey = key;
|
|
5317
|
+
config.providers.ollamaCloud.enabled = true;
|
|
5318
|
+
}
|
|
5319
|
+
continue;
|
|
5320
|
+
}
|
|
5321
|
+
if (provider === "ollamaLocal") {
|
|
5322
|
+
config.providers.ollamaLocal.baseUrl = await promptValidatedValue(
|
|
5323
|
+
chalk6.white(` Ollama Local base URL [${config.providers.ollamaLocal.baseUrl}]: `),
|
|
5324
|
+
validateBaseUrl,
|
|
5325
|
+
config.providers.ollamaLocal.baseUrl
|
|
5326
|
+
);
|
|
5327
|
+
config.providers.ollamaLocal.model = await promptValidatedValue(
|
|
5328
|
+
chalk6.white(` Ollama Local model [${config.providers.ollamaLocal.model}]: `),
|
|
5329
|
+
validateModelName,
|
|
5330
|
+
config.providers.ollamaLocal.model
|
|
5331
|
+
);
|
|
5332
|
+
config.providers.ollamaLocal.enabled = true;
|
|
5333
|
+
}
|
|
4745
5334
|
}
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
5335
|
+
const configuredProviders = getConfiguredProviderNames(config);
|
|
5336
|
+
if (configuredProviders.length === 0) {
|
|
5337
|
+
console.log(chalk6.red(" You need to configure at least one LLM provider to continue."));
|
|
5338
|
+
console.log(chalk6.dim(" Let\u2019s try that step again."));
|
|
5339
|
+
console.log("");
|
|
5340
|
+
continue;
|
|
4751
5341
|
}
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
console.log(chalk6.dim(` Default provider set to ${config.providers.default}`));
|
|
5342
|
+
await chooseDefaultProvider(config);
|
|
5343
|
+
break;
|
|
4755
5344
|
}
|
|
4756
5345
|
hr();
|
|
4757
5346
|
console.log("");
|
|
@@ -4760,6 +5349,11 @@ async function configure(existingConfig) {
|
|
|
4760
5349
|
console.log(chalk6.dim(' Leave empty to keep current value. Enter "none" to disable.'));
|
|
4761
5350
|
} else {
|
|
4762
5351
|
console.log(chalk6.dim(" Leave empty to skip. You can add it later."));
|
|
5352
|
+
console.log(chalk6.dim(" To create a bot token:"));
|
|
5353
|
+
console.log(chalk6.dim(" 1. Open Telegram and message @BotFather"));
|
|
5354
|
+
console.log(chalk6.dim(" 2. Run /newbot and follow the prompts"));
|
|
5355
|
+
console.log(chalk6.dim(" 3. Copy the bot token BotFather gives you"));
|
|
5356
|
+
console.log(chalk6.dim(" 4. Paste that token here"));
|
|
4763
5357
|
}
|
|
4764
5358
|
console.log("");
|
|
4765
5359
|
const tgMask = isReconfig && config.channels.telegram.botToken ? ` [${maskKey(config.channels.telegram.botToken)}]` : "";
|
|
@@ -4767,7 +5361,11 @@ async function configure(existingConfig) {
|
|
|
4767
5361
|
if (isReconfig && telegramToken.toLowerCase() === "none") {
|
|
4768
5362
|
config.channels.telegram.enabled = false;
|
|
4769
5363
|
config.channels.telegram.botToken = "";
|
|
5364
|
+
clearTelegramPairing(config);
|
|
4770
5365
|
} else if (telegramToken) {
|
|
5366
|
+
if (telegramToken !== config.channels.telegram.botToken) {
|
|
5367
|
+
clearTelegramPairing(config);
|
|
5368
|
+
}
|
|
4771
5369
|
config.channels.telegram.botToken = telegramToken;
|
|
4772
5370
|
config.channels.telegram.enabled = true;
|
|
4773
5371
|
}
|
|
@@ -4878,10 +5476,10 @@ async function runAgent(isDaemon = false) {
|
|
|
4878
5476
|
const providers = new ProviderRegistry(config);
|
|
4879
5477
|
if (!providers.hasProviders()) {
|
|
4880
5478
|
if (isDaemon) {
|
|
4881
|
-
logger.error("No LLM providers available. Run `mercury doctor` to configure
|
|
5479
|
+
logger.error("No LLM providers available. Run `mercury doctor` to configure providers.");
|
|
4882
5480
|
return;
|
|
4883
5481
|
}
|
|
4884
|
-
console.log(chalk6.red(" No LLM providers available. Run `mercury doctor` to configure
|
|
5482
|
+
console.log(chalk6.red(" No LLM providers available. Run `mercury doctor` to configure providers."));
|
|
4885
5483
|
process.exit(1);
|
|
4886
5484
|
}
|
|
4887
5485
|
const available = providers.listAvailable();
|
|
@@ -4915,6 +5513,18 @@ async function runAgent(isDaemon = false) {
|
|
|
4915
5513
|
await msg.sendFile(filePath);
|
|
4916
5514
|
}
|
|
4917
5515
|
});
|
|
5516
|
+
capabilities.setSendMessageHandler(async (content) => {
|
|
5517
|
+
const telegram = channels.get("telegram");
|
|
5518
|
+
const pairedChatId = config.channels.telegram.pairedChatId;
|
|
5519
|
+
const pairedUserId = config.channels.telegram.pairedUserId;
|
|
5520
|
+
if (!config.channels.telegram.enabled || !telegram) {
|
|
5521
|
+
throw new Error("Telegram is not configured. Add a bot token in setup or run `mercury doctor`.");
|
|
5522
|
+
}
|
|
5523
|
+
if (pairedChatId == null || pairedUserId == null) {
|
|
5524
|
+
throw new Error("Telegram is not paired. Complete the pairing flow with /start or /pair from the Telegram owner account.");
|
|
5525
|
+
}
|
|
5526
|
+
await telegram.send(content, `telegram:${pairedChatId}`);
|
|
5527
|
+
});
|
|
4918
5528
|
if (process.env.GITHUB_TOKEN) {
|
|
4919
5529
|
setGitHubToken(process.env.GITHUB_TOKEN);
|
|
4920
5530
|
}
|
|
@@ -5055,8 +5665,9 @@ program.command("status").description("Show current configuration and daemon sta
|
|
|
5055
5665
|
if (config.identity.creator) {
|
|
5056
5666
|
console.log(` Creator: ${chalk6.white(config.identity.creator)}`);
|
|
5057
5667
|
}
|
|
5058
|
-
console.log(` Provider: ${chalk6.white(config.providers.default)}`);
|
|
5668
|
+
console.log(` Provider: ${chalk6.white(getProviderLabel(config.providers.default))}`);
|
|
5059
5669
|
console.log(` Telegram: ${config.channels.telegram.enabled ? chalk6.green("enabled") : chalk6.dim("disabled")}`);
|
|
5670
|
+
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
5671
|
console.log(` Skills: ${skills.length > 0 ? chalk6.green(skills.map((s) => s.name).join(", ")) : chalk6.dim("none")}`);
|
|
5061
5672
|
console.log(` Budget: ${chalk6.white(config.tokens.dailyBudget.toLocaleString())} tokens/day`);
|
|
5062
5673
|
console.log(` Setup: ${isSetupComplete() ? chalk6.green("complete") : chalk6.red("not done")}`);
|
|
@@ -5067,6 +5678,28 @@ program.command("status").description("Show current configuration and daemon sta
|
|
|
5067
5678
|
program.command("help").description("Show capabilities and commands manual").action(() => {
|
|
5068
5679
|
console.log(getManual());
|
|
5069
5680
|
});
|
|
5681
|
+
var telegramCmd = program.command("telegram").description("Manage Telegram pairing and access");
|
|
5682
|
+
telegramCmd.command("unpair").description("Clear the paired Telegram owner for this Mercury instance").action(() => {
|
|
5683
|
+
const config = loadConfig();
|
|
5684
|
+
const daemon = getDaemonStatus();
|
|
5685
|
+
if (config.channels.telegram.pairedUserId == null) {
|
|
5686
|
+
console.log("");
|
|
5687
|
+
console.log(chalk6.dim(" Telegram is already unpaired."));
|
|
5688
|
+
console.log("");
|
|
5689
|
+
return;
|
|
5690
|
+
}
|
|
5691
|
+
clearTelegramPairing(config);
|
|
5692
|
+
saveConfig(config);
|
|
5693
|
+
console.log("");
|
|
5694
|
+
console.log(chalk6.green(" \u2713 Telegram pairing cleared."));
|
|
5695
|
+
if (daemon.running) {
|
|
5696
|
+
console.log(chalk6.dim(" Restarting the background daemon to apply the change immediately..."));
|
|
5697
|
+
restartDaemon();
|
|
5698
|
+
} else {
|
|
5699
|
+
console.log(chalk6.dim(" The next private Telegram user to send /start will pair this Mercury instance."));
|
|
5700
|
+
}
|
|
5701
|
+
console.log("");
|
|
5702
|
+
});
|
|
5070
5703
|
var serviceCmd = program.command("service").description("Manage Mercury as a system service (auto-start, crash recovery)");
|
|
5071
5704
|
serviceCmd.command("install").description("Install Mercury as a system service (auto-start on boot)").action(() => {
|
|
5072
5705
|
installService();
|