@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/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 existsSync16 } from "fs";
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", "openai"),
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.deepseek
663
+ config.providers.grok,
664
+ config.providers.ollamaCloud,
665
+ config.providers.ollamaLocal
587
666
  ];
588
667
  for (const pc of entries) {
589
- if (!pc.enabled || !pc.apiKey) continue;
668
+ if (!isProviderConfigured(pc)) continue;
590
669
  try {
591
- const provider = pc.name === "anthropic" ? new AnthropicProvider(pc) : new OpenAICompatProvider(pc);
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. You can create pull requests, review PRs, manage issues, and use the GitHub API.";
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 += ' When the user says "create a PR", use create_pr. When they ask about issues, use list_issues or create_issue. When they ask to review a PR, use review_pr. Always specify owner and repo parameters.';
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((resolve11) => {
1731
- this.rl?.question(question, (answer) => resolve11(answer.trim()));
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((resolve11) => {
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
- resolve11(answer.trim());
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
- if (!this.isAllowedChat(chatId)) return;
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((resolve11) => {
2003
- this.pendingApprovals.set(`${id}:yes`, () => resolve11("yes"));
2004
- this.pendingApprovals.set(`${id}:always`, () => resolve11("always"));
2005
- this.pendingApprovals.set(`${id}:no`, () => resolve11("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
- resolve11("no");
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
- isAllowedChat(chatId) {
2057
- const allowed = this.config.channels.telegram.allowedChatIds;
2058
- if (!allowed || allowed.length === 0) return true;
2059
- return allowed.includes(chatId);
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(tool30) {
2360
- if (this.elevatedCommands.has(tool30)) return true;
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/filesystem/approve-scope.ts
3152
+ // src/capabilities/messaging/send-message.ts
2899
3153
  import { tool as tool8 } from "ai";
2900
3154
  import { z as z8 } from "zod";
2901
- import { resolve as resolve10 } from "path";
2902
- function createApproveScopeTool(permissions) {
3155
+ function createSendMessageTool(sendMessage) {
2903
3156
  return tool8({
2904
- 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.',
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
- path: z8.string().describe("The directory path to request access to"),
2907
- mode: z8.enum(["read", "write"]).describe("The access mode needed")
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 tool9 } from "ai";
2922
- import { z as z9 } from "zod";
3199
+ import { tool as tool10 } from "ai";
3200
+ import { z as z10 } from "zod";
2923
3201
  import { execSync } from "child_process";
2924
- function createRunCommandTool(permissions) {
2925
- return tool9({
2926
- description: `Run a shell command. Commands run in the current working directory unless an absolute path is given.
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 \u2014 tell the user what command you want to run and ask for confirmation. If they say "yes", try again. If they say "always", use the approve_command tool.`,
2930
- parameters: z9.object({
2931
- command: z9.string().describe("The shell command to execute")
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: process.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 output = result?.trim() || "(no output)";
2955
- return output;
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 tool10 } from "ai";
2972
- import { z as z10 } from "zod";
3307
+ import { tool as tool12 } from "ai";
3308
+ import { z as z12 } from "zod";
2973
3309
  function createApproveCommandTool(permissions) {
2974
- return tool10({
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: z10.object({
2977
- command: z10.string().describe('The base command to permanently approve (e.g. "curl", "docker", "npm")')
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 tool11 } from "ai";
2989
- import { z as z11 } from "zod";
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 tool11({
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: z11.object({
2995
- content: z11.string().optional().describe("Raw SKILL.md markdown content with YAML frontmatter"),
2996
- url: z11.string().optional().describe("URL to fetch a SKILL.md from")
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 tool12 } from "ai";
3037
- import { z as z12 } from "zod";
3372
+ import { tool as tool14 } from "ai";
3373
+ import { z as z14 } from "zod";
3038
3374
  function createListSkillsTool(skillLoader) {
3039
- return tool12({
3375
+ return tool14({
3040
3376
  description: "List all installed skills with their names and descriptions.",
3041
- parameters: z12.object({}),
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 tool13 } from "ai";
3054
- import { z as z13 } from "zod";
3389
+ import { tool as tool15 } from "ai";
3390
+ import { z as z15 } from "zod";
3055
3391
  function createUseSkillTool(skillLoader, permissions) {
3056
- return tool13({
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: z13.object({
3059
- name: z13.string().describe("Name of the skill to invoke")
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 tool14 } from "ai";
3087
- import { z as z14 } from "zod";
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 tool14({
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: z14.object({
3093
- cron: z14.string().optional().describe('Cron expression for recurring tasks (e.g. "0 9 * * *" for daily at 9am)'),
3094
- delay_seconds: z14.number().optional().describe('Delay in seconds for one-shot tasks (e.g. 15 for "remind me in 15 seconds")'),
3095
- description: z14.string().describe("Human-readable description of what this task does"),
3096
- prompt: z14.string().optional().describe("Prompt to send to the agent when the task fires"),
3097
- skill_name: z14.string().optional().describe("Name of a skill to invoke when the task fires")
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 tool15 } from "ai";
3151
- import { z as z15 } from "zod";
3486
+ import { tool as tool17 } from "ai";
3487
+ import { z as z17 } from "zod";
3152
3488
  function createListTasksTool(scheduler) {
3153
- return tool15({
3489
+ return tool17({
3154
3490
  description: "List all scheduled tasks with their cron expressions and descriptions.",
3155
- parameters: z15.object({}),
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 tool16 } from "ai";
3171
- import { z as z16 } from "zod";
3506
+ import { tool as tool18 } from "ai";
3507
+ import { z as z18 } from "zod";
3172
3508
  function createCancelTaskTool(scheduler) {
3173
- return tool16({
3509
+ return tool18({
3174
3510
  description: "Cancel and remove a scheduled task by its ID.",
3175
- parameters: z16.object({
3176
- id: z16.string().describe("ID of the scheduled task to cancel")
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 tool17 } from "ai";
3193
- import { z as z17 } from "zod";
3528
+ import { tool as tool19 } from "ai";
3529
+ import { z as z19 } from "zod";
3194
3530
  function createBudgetStatusTool(tokenBudget) {
3195
- return tool17({
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: z17.object({}),
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 tool18 } from "ai";
3206
- import { z as z18 } from "zod";
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 tool18({
3544
+ function createGitStatusTool(getCwd) {
3545
+ return tool20({
3210
3546
  description: "Show the working tree status. Returns staged, unstaged, and untracked files.",
3211
- parameters: z18.object({
3212
- path: z18.string().optional().describe("Path to check (defaults to current directory)")
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 tool19 } from "ai";
3229
- import { z as z19 } from "zod";
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 tool19({
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: z19.object({
3235
- path: z19.string().optional().describe("File or directory to diff"),
3236
- staged: z19.boolean().optional().describe("Show staged changes (cached) instead of unstaged")
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 tool20 } from "ai";
3256
- import { z as z20 } from "zod";
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 tool20({
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: z20.object({
3262
- count: z20.number().optional().describe("Number of commits to show (default 10)"),
3263
- path: z20.string().optional().describe("File or directory to show log for")
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 tool21 } from "ai";
3282
- import { z as z21 } from "zod";
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 tool21({
3620
+ function createGitAddTool(getCwd) {
3621
+ return tool23({
3286
3622
  description: "Add file contents to the index (staging area). Prepares files for commit.",
3287
- parameters: z21.object({
3288
- paths: z21.array(z21.string()).describe("File paths to stage")
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 tool22 } from "ai";
3304
- import { z as z22 } from "zod";
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 tool22({
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: z22.object({
3311
- message: z22.string().describe("Commit 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 tool23 } from "ai";
3334
- import { z as z23 } from "zod";
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 tool23({
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: z23.object({
3340
- remote: z23.string().optional().describe("Remote name (default: origin)"),
3341
- branch: z23.string().optional().describe("Branch name (default: current 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 tool24 } from "ai";
3367
- import { z as z24 } from "zod";
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 tool24({
3758
+ return tool26({
3423
3759
  description: "Create a pull request on GitHub. Requires GITHUB_TOKEN to be configured.",
3424
- parameters: z24.object({
3425
- owner: z24.string().describe("Repository owner (username or org)"),
3426
- repo: z24.string().describe("Repository name"),
3427
- title: z24.string().describe("PR title"),
3428
- body: z24.string().describe("PR description (markdown supported)").default(""),
3429
- head: z24.string().describe("The branch containing the changes"),
3430
- base: z24.string().describe("The branch to merge into").default("main"),
3431
- draft: z24.boolean().describe("Create as draft PR").default(false)
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 tool25 } from "ai";
3451
- import { z as z25 } from "zod";
3786
+ import { tool as tool27 } from "ai";
3787
+ import { z as z27 } from "zod";
3452
3788
  function createReviewPrTool() {
3453
- return tool25({
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: z25.object({
3456
- owner: z25.string().describe("Repository owner (username or org)"),
3457
- repo: z25.string().describe("Repository name"),
3458
- number: z25.number().describe("PR number"),
3459
- comment: z25.string().describe("Review comment to post on the PR (optional)").optional()
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 tool26 } from "ai";
3526
- import { z as z26 } from "zod";
3861
+ import { tool as tool28 } from "ai";
3862
+ import { z as z28 } from "zod";
3527
3863
  function createListIssuesTool() {
3528
- return tool26({
3864
+ return tool28({
3529
3865
  description: "List GitHub issues for a repository. Requires GITHUB_TOKEN.",
3530
- parameters: z26.object({
3531
- owner: z26.string().describe("Repository owner (username or org)"),
3532
- repo: z26.string().describe("Repository name"),
3533
- state: z26.enum(["open", "closed", "all"]).describe("Filter by issue state").default("open"),
3534
- labels: z26.string().describe("Comma-separated label names to filter by (optional)").optional(),
3535
- limit: z26.number().describe("Maximum number of issues to return").default(10)
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 tool27 } from "ai";
3564
- import { z as z27 } from "zod";
3899
+ import { tool as tool29 } from "ai";
3900
+ import { z as z29 } from "zod";
3565
3901
  function createCreateIssueTool() {
3566
- return tool27({
3902
+ return tool29({
3567
3903
  description: "Create a new GitHub issue in a repository. Requires GITHUB_TOKEN.",
3568
- parameters: z27.object({
3569
- owner: z27.string().describe("Repository owner (username or org)"),
3570
- repo: z27.string().describe("Repository name"),
3571
- title: z27.string().describe("Issue title"),
3572
- body: z27.string().describe("Issue description (markdown supported)").default(""),
3573
- labels: z27.array(z27.string()).describe("Label names to apply").optional()
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 tool28 } from "ai";
3594
- import { z as z28 } from "zod";
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 tool28({
3597
- description: "Make a raw request to the GitHub API. Use this for any GitHub operation not covered by other tools. GET requests (read-only) are always allowed. Write operations (POST, PUT, PATCH, DELETE) will ask the user for approval via the permission system.",
3598
- parameters: z28.object({
3599
- path: z28.string().describe("Full API path (e.g., /repos/owner/repo/issues or /user)"),
3600
- method: z28.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).describe("HTTP method").default("GET"),
3601
- body: z28.string().describe("JSON body for write requests (as a JSON string)").optional()
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 tool29 } from "ai";
3629
- import { z as z29 } from "zod";
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 tool29({
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: z29.object({
3665
- url: z29.string().describe("The URL to fetch"),
3666
- format: z29.enum(["text", "markdown"]).optional().describe("Output format (default: markdown)")
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 existsSync13, readFileSync as readFileSync10, readdirSync as readdirSync3, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
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 (!existsSync13(this.skillsDir)) {
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 (!existsSync13(skillPath)) continue;
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 (!existsSync13(skillPath)) continue;
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: existsSync13(join8(skillDir, "scripts")) ? join8(skillDir, "scripts") : void 0,
3882
- referencesDir: existsSync13(join8(skillDir, "references")) ? join8(skillDir, "references") : void 0
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 (!existsSync13(skillDir)) {
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 existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, mkdirSync as mkdirSync9, openSync } from "fs";
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 (!existsSync14(path3)) return null;
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 (!existsSync14(home)) {
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 (!existsSync14(logFile)) {
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 (!existsSync14(home)) {
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 existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4 } from "fs";
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 existsSync15(join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist"));
4664
+ return existsSync17(join10(homedir3(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist"));
4274
4665
  } else if (platform === "linux") {
4275
- return existsSync15(join10(homedir3(), ".config", "systemd", "user", "mercury.service"));
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 (!existsSync15(plistDir)) {
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 (!existsSync15(plistPath)) {
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 (!existsSync15(plistPath)) {
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 (!existsSync15(systemdDir)) {
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 (!existsSync15(servicePath)) {
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 (!existsSync15(servicePath)) {
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((resolve11) => setTimeout(resolve11, ms));
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((resolve11) => {
5038
+ return new Promise((resolve13) => {
4648
5039
  rl.question(prompt, (answer) => {
4649
5040
  rl.close();
4650
- resolve11(answer.trim());
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 (existsSync16(envPath)) {
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(" Current keys shown masked. Enter new value to change, Enter to keep."));
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(" At least one API key is required."));
5244
+ console.log(chalk6.dim(" Choose one or more providers. Press Enter to configure DeepSeek by default."));
4715
5245
  }
4716
5246
  console.log("");
4717
- const dsMask = isReconfig && config.providers.deepseek.apiKey ? ` [${maskKey(config.providers.deepseek.apiKey)}]` : "";
4718
- const deepseekKey = await ask(chalk6.white(` DeepSeek API key${dsMask}: `));
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
- console.log(chalk6.bold.white(" Default Provider"));
4740
- console.log(chalk6.dim(" Select the default LLM provider (the one used first)."));
4741
- console.log("");
4742
- for (let i = 0; i < availableProviders.length; i++) {
4743
- const marker = availableProviders[i] === config.providers.default ? " (current)" : "";
4744
- console.log(chalk6.white(` ${i + 1}. ${availableProviders[i]}${marker}`));
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
- console.log("");
4747
- const choice = await ask(chalk6.white(` Choose [1-${availableProviders.length}] [Enter to keep ${config.providers.default}]: `));
4748
- const num = parseInt(choice, 10);
4749
- if (num >= 1 && num <= availableProviders.length) {
4750
- config.providers.default = availableProviders[num - 1];
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
- } else if (!isReconfig) {
4753
- config.providers.default = availableProviders[0];
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 API keys.");
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 API keys."));
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();