@cosmicstack/mercury-agent 0.3.3 → 0.4.0

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