@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 +26 -9
- package/dist/index.js +138 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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.
|
|
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 +=
|
|
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:
|
|
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 /
|
|
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
|