@cosmicstack/mercury-agent 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  </p>
8
8
 
9
9
  <p align="center">
10
- Runs 24/7 from CLI or Telegram. 21 built-in tools. Extensible skills. Asks before it acts.
10
+ Runs 24/7 from CLI or Telegram. 31 built-in tools. Extensible skills. Asks before it acts.
11
11
  </p>
12
12
 
13
13
  <p align="center">
@@ -136,8 +136,9 @@ Type these during a conversation — they don't consume API tokens. Work on both
136
136
 
137
137
  | Category | Tools |
138
138
  |----------|-------|
139
- | **Filesystem** | `read_file`, `write_file`, `create_file`, `edit_file`, `list_dir`, `delete_file`, `send_file` |
140
- | **Shell** | `run_command`, `approve_command` |
139
+ | **Filesystem** | `read_file`, `write_file`, `create_file`, `edit_file`, `list_dir`, `delete_file`, `send_file`, `approve_scope` |
140
+ | **Shell** | `run_command`, `cd`, `approve_command` |
141
+ | **Messaging** | `send_message` |
141
142
  | **Git** | `git_status`, `git_diff`, `git_log`, `git_add`, `git_commit`, `git_push` |
142
143
  | **Web** | `fetch_url` |
143
144
  | **Skills** | `install_skill`, `list_skills`, `use_skill` |
@@ -151,6 +152,15 @@ Type these during a conversation — they don't consume API tokens. Work on both
151
152
  | **CLI** | Readline prompt, real-time text streaming, markdown rendering, file display |
152
153
  | **Telegram** | HTML formatting, file uploads (photos, audio, video, documents), typing indicators, `/budget` commands |
153
154
 
155
+ ### Telegram Pairing
156
+
157
+ Mercury uses a **single-owner pairing model** — only one Telegram account can be paired at a time.
158
+
159
+ - **Unpaired bots** only process `/start` or `/pair`. All other messages are ignored. This prevents unauthorized access.
160
+ - **To pair:** Send `/start` to your Mercury bot in a private chat. The bot records your user ID and chat ID.
161
+ - **To unpair:** Send `/unpair` to remove the pairing.
162
+ - Mercury only works in private (one-to-one) chats. Group messages are ignored.
163
+
154
164
  ## Scheduler
155
165
 
156
166
  - **Recurring**: `schedule_task` with cron expressions (`0 9 * * *` for daily at 9am)
@@ -165,6 +175,7 @@ All runtime data lives in `~/.mercury/` — not in your project directory.
165
175
  | Path | Purpose |
166
176
  |------|---------|
167
177
  | `~/.mercury/mercury.yaml` | Main config (providers, channels, budget) |
178
+ | `~/.mercury/.env` | API keys and tokens (loaded alongside project .env) |
168
179
  | `~/.mercury/soul/*.md` | Agent personality (soul, persona, taste, heartbeat) |
169
180
  | `~/.mercury/permissions.yaml` | Capabilities and approval rules |
170
181
  | `~/.mercury/skills/` | Installed skills |
@@ -178,12 +189,18 @@ All runtime data lives in `~/.mercury/` — not in your project directory.
178
189
 
179
190
  Configure multiple LLM providers. Mercury tries them in order and falls back automatically:
180
191
 
181
- - **DeepSeek** default, cost-effective
182
- - **OpenAI** — GPT-4o-mini and others
183
- - **Anthropic** Claude and others
184
- - **Grok / xAI** OpenAI-compatible Grok models
185
- - **Ollama Cloud** remote Ollama models via API key
186
- - **Ollama Local** models running on your local Ollama instance
192
+ | Provider | Default Model | API Key | Notes |
193
+ |----------|--------------|---------|-------|
194
+ | **DeepSeek** | deepseek-chat | `DEEPSEEK_API_KEY` | Default, cost-effective |
195
+ | **OpenAI** | gpt-4o-mini | `OPENAI_API_KEY` | GPT-4o, o3, etc. |
196
+ | **Anthropic** | claude-sonnet-4 | `ANTHROPIC_API_KEY` | Claude Sonnet, Haiku, Opus |
197
+ | **Grok (xAI)** | grok-4 | `GROK_API_KEY` | OpenAI-compatible endpoint |
198
+ | **Ollama Cloud** | gpt-oss:120b | `OLLAMA_CLOUD_API_KEY` | Remote Ollama via API |
199
+ | **Ollama Local** | gpt-oss:20b | No key needed | Local Ollama instance |
200
+
201
+ When a provider fails, Mercury automatically tries the next one. It remembers the last successful provider and starts there on the next request.
202
+
203
+ > **More providers incoming** — Google Gemini, Mistral, and others are on the roadmap. Mercury's OpenAI-compatible architecture also supports custom endpoints via base URL configuration.
187
204
 
188
205
  ## Architecture
189
206
 
package/dist/index.js CHANGED
@@ -762,6 +762,48 @@ var Lifecycle = class {
762
762
  };
763
763
 
764
764
  // src/core/agent.ts
765
+ var ToolCallLoopDetector = class {
766
+ recentCalls = [];
767
+ maxEntries = 10;
768
+ record(toolName, params) {
769
+ const paramsKey = JSON.stringify(params).slice(0, 100);
770
+ this.recentCalls.push({ tool: toolName, params: paramsKey });
771
+ if (this.recentCalls.length > this.maxEntries) {
772
+ this.recentCalls.shift();
773
+ }
774
+ }
775
+ detect() {
776
+ if (this.recentCalls.length < 3) return null;
777
+ const last = this.recentCalls[this.recentCalls.length - 1];
778
+ let consecutiveCount = 0;
779
+ for (let i = this.recentCalls.length - 1; i >= 0; i--) {
780
+ if (this.recentCalls[i].tool === last.tool && this.recentCalls[i].params === last.params) {
781
+ consecutiveCount++;
782
+ } else {
783
+ break;
784
+ }
785
+ }
786
+ if (consecutiveCount >= 3) {
787
+ return { tool: last.tool, count: consecutiveCount };
788
+ }
789
+ const lastTool = last.tool;
790
+ let toolCount = 0;
791
+ for (let i = this.recentCalls.length - 1; i >= 0; i--) {
792
+ if (this.recentCalls[i].tool === lastTool) {
793
+ toolCount++;
794
+ } else {
795
+ break;
796
+ }
797
+ }
798
+ if (toolCount >= 4) {
799
+ return { tool: lastTool, count: toolCount };
800
+ }
801
+ return null;
802
+ }
803
+ reset() {
804
+ this.recentCalls = [];
805
+ }
806
+ };
765
807
  var MAX_STEPS = 10;
766
808
  var Agent = class {
767
809
  constructor(config, providers, identity, shortTerm, longTerm, episodic, channels, tokenBudget, capabilities, scheduler) {
@@ -923,6 +965,30 @@ You can override this:
923
965
  const recentMemory = this.shortTerm.getRecent(msg.channelId, 10);
924
966
  const relevantFacts = this.longTerm.search(msg.content, 3);
925
967
  const messages = [];
968
+ const recentSteps = this.shortTerm.getRecent(msg.channelId, 4);
969
+ let loopWarning = null;
970
+ if (recentSteps.length >= 3) {
971
+ const toolCallPattern = /\[Using: (.+?)\]/g;
972
+ const toolCalls = [];
973
+ for (const m of recentSteps) {
974
+ if (m.role === "assistant") {
975
+ let match;
976
+ while ((match = toolCallPattern.exec(m.content)) !== null) {
977
+ toolCalls.push(match[1]);
978
+ }
979
+ }
980
+ }
981
+ if (toolCalls.length >= 3) {
982
+ const last3 = toolCalls.slice(-3);
983
+ if (last3[0] === last3[1] && last3[1] === last3[2]) {
984
+ loopWarning = `[SYSTEM WARNING] You have called ${last3[0]} 3+ times in a row with the same result. Stop repeating this call. Try a different approach \u2014 if you're failing on permissions, try a different path. If you're failing on git push auth, use github_api with PUT /repos/{owner}/{repo}/contents/{path} to push files directly through the API.`;
985
+ }
986
+ }
987
+ }
988
+ if (loopWarning) {
989
+ messages.push({ role: "user", content: loopWarning });
990
+ messages.push({ role: "assistant", content: "Understood. I will try a different approach." });
991
+ }
926
992
  if (relevantFacts.length > 0) {
927
993
  messages.push({
928
994
  role: "user",
@@ -952,6 +1018,7 @@ You can override this:
952
1018
  let usedProvider = null;
953
1019
  let lastError = null;
954
1020
  let streamedText = "";
1021
+ const loopDetector = new ToolCallLoopDetector();
955
1022
  const canStream = msg.channelType === "cli" || msg.channelType === "telegram" && this.telegramStreaming;
956
1023
  for (const provider of fallbackIterator) {
957
1024
  try {
@@ -967,6 +1034,13 @@ You can override this:
967
1034
  if (toolCalls && toolCalls.length > 0) {
968
1035
  const names = toolCalls.map((tc) => tc.toolName).join(", ");
969
1036
  logger.info({ tools: names }, "Tool call step");
1037
+ for (const tc of toolCalls) {
1038
+ loopDetector.record(tc.toolName, tc.args);
1039
+ }
1040
+ const loop = loopDetector.detect();
1041
+ if (loop) {
1042
+ logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected");
1043
+ }
970
1044
  await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
971
1045
  });
972
1046
  }
@@ -1004,6 +1078,13 @@ You can override this:
1004
1078
  if (toolCalls && toolCalls.length > 0) {
1005
1079
  const names = toolCalls.map((tc) => tc.toolName).join(", ");
1006
1080
  logger.info({ tools: names }, "Tool call step");
1081
+ for (const tc of toolCalls) {
1082
+ loopDetector.record(tc.toolName, tc.args);
1083
+ }
1084
+ const loop = loopDetector.detect();
1085
+ if (loop) {
1086
+ logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected");
1087
+ }
1007
1088
  if (channel && msg.channelType !== "internal") {
1008
1089
  await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
1009
1090
  });
@@ -1098,16 +1179,35 @@ You can override this:
1098
1179
  if (this.tokenBudget.getUsagePercentage() > 70) {
1099
1180
  prompt += "\nBe concise to conserve tokens.";
1100
1181
  }
1182
+ prompt += `
1183
+
1184
+ Environment:
1185
+ - Platform: ${process.platform}
1186
+ - Working directory: ${this.capabilities.getCwd()}`;
1101
1187
  const toolNames = this.capabilities.getToolNames();
1102
1188
  const githubTools = ["create_pr", "review_pr", "list_issues", "create_issue", "github_api"];
1103
1189
  const hasGitHub = githubTools.some((t) => toolNames.includes(t));
1104
1190
  if (hasGitHub) {
1105
- let githubHint = "\n\nGitHub companion is active. You can create pull requests, review PRs, manage issues, and use the GitHub API.";
1191
+ let githubHint = "\n\nGitHub companion is active.";
1106
1192
  const { defaultOwner, defaultRepo } = this.config.github;
1107
1193
  if (defaultOwner && defaultRepo) {
1108
1194
  githubHint += ` Default repo: ${defaultOwner}/${defaultRepo}. Use this when the user doesn't specify a repo.`;
1109
1195
  }
1110
- githubHint += ' When the user says "create a PR", use create_pr. When they ask about issues, use list_issues or create_issue. When they ask to review a PR, use review_pr. Always specify owner and repo parameters.';
1196
+ githubHint += `
1197
+
1198
+ Available GitHub tools and when to use them:
1199
+ - git_add, git_commit, git_push: LOCAL git operations (stage, commit, push to a remote you have SSH/auth access to). All commits include "Co-authored-by: Mercury <mercury@cosmicstack.org>".
1200
+ - create_pr: Create a pull request on GitHub. The head branch must already exist on the remote.
1201
+ - review_pr: Get PR details and optionally post a review comment.
1202
+ - list_issues, create_issue: Browse and file issues.
1203
+ - github_api: Raw GitHub API access. IMPORTANT USE CASES:
1204
+ - Push files directly to GitHub via PUT /repos/{owner}/{repo}/contents/{path} when git push fails due to auth. The body must include "message" and "content" (base64-encoded file content). This creates a commit on GitHub with Mercury as co-author.
1205
+ - Delete files via DELETE /repos/{owner}/{repo}/contents/{path} with a "message" and "sha" in the body.
1206
+ - Any other GitHub API operation not covered by the other tools.
1207
+
1208
+ When the user asks to "push to GitHub" or "upload files" and git push fails, use github_api with PUT /repos/{owner}/{repo}/contents/{path} to push content directly through the API. This bypasses local git entirely.
1209
+
1210
+ Always specify owner and repo parameters on GitHub tools. The user's GitHub username is ${this.config.github.username || "not set"}.'`;
1111
1211
  prompt += githubHint;
1112
1212
  }
1113
1213
  return prompt;
@@ -3828,11 +3928,42 @@ function createCreateIssueTool() {
3828
3928
  // src/capabilities/github/github-api.ts
3829
3929
  import { tool as tool30 } from "ai";
3830
3930
  import { z as z30 } from "zod";
3931
+ var CO_AUTHOR_NAME = "Mercury";
3932
+ var CO_AUTHOR_EMAIL = "mercury@cosmicstack.org";
3933
+ var CO_AUTHOR_TRAILER = `Co-authored-by: ${CO_AUTHOR_NAME} <${CO_AUTHOR_EMAIL}>`;
3934
+ function isContentCreatePath(path3) {
3935
+ return /^\/repos\/[^/]+\/[^/]+\/contents\//.test(path3);
3936
+ }
3937
+ function injectCoAuthor(body) {
3938
+ const result = { ...body };
3939
+ if (typeof result.message === "string" && !result.message.includes(CO_AUTHOR_TRAILER)) {
3940
+ result.message += `
3941
+
3942
+ ${CO_AUTHOR_TRAILER}`;
3943
+ }
3944
+ if (!result.committer || typeof result.committer !== "object") {
3945
+ result.committer = { name: CO_AUTHOR_NAME, email: CO_AUTHOR_EMAIL };
3946
+ }
3947
+ if (!result.author || typeof result.author !== "object") {
3948
+ result.author = { name: CO_AUTHOR_NAME, email: CO_AUTHOR_EMAIL };
3949
+ }
3950
+ return result;
3951
+ }
3831
3952
  function createGithubApiTool() {
3832
3953
  return tool30({
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.",
3954
+ description: `Make a raw request to the GitHub API. GET requests (read-only) are always allowed. Write operations (POST, PUT, PATCH, DELETE) may require user approval.
3955
+
3956
+ Common operations you can perform:
3957
+ - Push a file: PUT /repos/{owner}/{repo}/contents/{path} \u2014 body must include "message" (commit message) and "content" (base64-encoded file). For updates, also include "sha" from the current file. Co-authored-by Mercury is automatically included.
3958
+ - Delete a file: DELETE /repos/{owner}/{repo}/contents/{path} \u2014 body must include "message" and "sha".
3959
+ - List branches: GET /repos/{owner}/{repo}/branches
3960
+ - Get file contents: GET /repos/{owner}/{repo}/contents/{path}
3961
+ - Search code: GET /search/code?q={query}
3962
+ - Any other GitHub API v3 endpoint.
3963
+
3964
+ IMPORTANT: When the user wants to push code or files to GitHub and git push fails (auth issues, no SSH key, etc.), use PUT /repos/{owner}/{repo}/contents/{path} to create or update files directly through the API. This bypasses local git and creates a commit with Mercury as co-author.`,
3834
3965
  parameters: z30.object({
3835
- path: z30.string().describe("Full API path (e.g., /repos/owner/repo/issues or /user)"),
3966
+ path: z30.string().describe("Full API path (e.g., /repos/owner/repo/issues or /repos/owner/repo/contents/path/to/file)"),
3836
3967
  method: z30.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).describe("HTTP method").default("GET"),
3837
3968
  body: z30.string().describe("JSON body for write requests (as a JSON string)").optional()
3838
3969
  }),
@@ -3846,6 +3977,9 @@ function createGithubApiTool() {
3846
3977
  return "Error: body must be valid JSON.";
3847
3978
  }
3848
3979
  }
3980
+ if (parsedBody && isContentCreatePath(path3) && (method === "PUT" || method === "POST" || method === "PATCH")) {
3981
+ parsedBody = injectCoAuthor(parsedBody);
3982
+ }
3849
3983
  const result = await githubRequest(path3, {
3850
3984
  method,
3851
3985
  body: parsedBody