@cosmicstack/mercury-agent 0.5.1 → 0.5.3

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
@@ -235,4 +235,4 @@ MIT © [Cosmic Stack](https://github.com/cosmicstack-labs)
235
235
 
236
236
  ## Suggestions and Contributions
237
237
 
238
- For suggestions, contributions, or any inquiries, please reach out to us at [support@cosmicstack.org](mailto:support@cosmicstack.org).
238
+ For suggestions, contributions, or any inquiries, please reach out to us at [mercury@cosmicstack.org](mailto:mercury@cosmicstack.org).
package/dist/index.js CHANGED
@@ -916,9 +916,34 @@ var BaseChannel = class {
916
916
  import { Marked } from "marked";
917
917
  import chalk from "chalk";
918
918
  var lexer = new Marked();
919
+ var HTML_ENTITIES = {
920
+ "&": "&",
921
+ "&lt;": "<",
922
+ "&gt;": ">",
923
+ "&quot;": '"',
924
+ "&#39;": "'",
925
+ "&#x27;": "'",
926
+ "&apos;": "'",
927
+ "&nbsp;": " "
928
+ };
929
+ function decodeHtmlEntities(text) {
930
+ return text.replace(/&(?:#[xX]?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match) => {
931
+ if (HTML_ENTITIES[match]) return HTML_ENTITIES[match];
932
+ if (match.startsWith("&#x") || match.startsWith("&#X")) {
933
+ const code = parseInt(match.slice(3, -1), 16);
934
+ return isNaN(code) ? match : String.fromCodePoint(code);
935
+ }
936
+ if (match.startsWith("&#")) {
937
+ const code = parseInt(match.slice(2, -1), 10);
938
+ return isNaN(code) ? match : String.fromCodePoint(code);
939
+ }
940
+ return match;
941
+ });
942
+ }
919
943
  function renderMarkdown(text) {
920
944
  try {
921
- const tokens = lexer.lexer(text);
945
+ const decoded = decodeHtmlEntities(text);
946
+ const tokens = lexer.lexer(decoded);
922
947
  const result = renderTokens(tokens);
923
948
  return result.replace(/\n{3,}/g, "\n\n").trimEnd();
924
949
  } catch {
@@ -1106,6 +1131,67 @@ function mdToTelegram(text) {
1106
1131
  return out;
1107
1132
  }
1108
1133
 
1134
+ // src/utils/tool-label.ts
1135
+ var TOOL_LABELS = {
1136
+ fetch_url: { icon: "\u2197", label: "Fetching", argKey: "url", argTransform: extractDomain },
1137
+ read_file: { icon: "\u{1F4C4}", label: "Reading", argKey: "path", argTransform: basename },
1138
+ write_file: { icon: "\u270F\uFE0F", label: "Writing", argKey: "path", argTransform: basename },
1139
+ create_file: { icon: "\u2728", label: "Creating", argKey: "path", argTransform: basename },
1140
+ edit_file: { icon: "\u2702\uFE0F", label: "Editing", argKey: "path", argTransform: basename },
1141
+ delete_file: { icon: "\u{1F5D1}", label: "Deleting", argKey: "path", argTransform: basename },
1142
+ list_dir: { icon: "\u{1F4C2}", label: "Listing", argKey: "path", argTransform: basename },
1143
+ approve_scope: { icon: "\u{1F513}", label: "Approving scope" },
1144
+ run_command: { icon: "\u2328", label: "Running command", argKey: "command", argTransform: truncate(40) },
1145
+ cd: { icon: "\u{1F4C2}", label: "Changing dir to", argKey: "path" },
1146
+ approve_command: { icon: "\u2705", label: "Approving command" },
1147
+ send_message: { icon: "\u{1F4AC}", label: "Sending message" },
1148
+ send_file: { icon: "\u{1F4CE}", label: "Sending file", argKey: "path", argTransform: basename },
1149
+ git_status: { icon: "\u{1F4CA}", label: "Git status" },
1150
+ git_diff: { icon: "\u{1F4DD}", label: "Git diff" },
1151
+ git_log: { icon: "\u{1F4CB}", label: "Git log" },
1152
+ git_add: { icon: "\u2795", label: "Staging files" },
1153
+ git_commit: { icon: "\u{1F4BE}", label: "Committing" },
1154
+ git_push: { icon: "\u2B06", label: "Pushing" },
1155
+ create_pr: { icon: "\u{1F500}", label: "Creating PR" },
1156
+ review_pr: { icon: "\u{1F440}", label: "Reviewing PR" },
1157
+ list_issues: { icon: "\u{1F4CB}", label: "Listing issues" },
1158
+ create_issue: { icon: "\u{1F41B}", label: "Creating issue" },
1159
+ github_api: { icon: "\u{1F500}", label: "GitHub API", argKey: "path", argTransform: truncate(30) },
1160
+ schedule_task: { icon: "\u23F0", label: "Scheduling task" },
1161
+ cancel_task: { icon: "\u274C", label: "Cancelling task" },
1162
+ list_tasks: { icon: "\u{1F4CB}", label: "Listing tasks" },
1163
+ use_skill: { icon: "\u{1F9E0}", label: "Using skill", argKey: "name" },
1164
+ list_skills: { icon: "\u{1F4CB}", label: "Listing skills" },
1165
+ install_skill: { icon: "\u{1F4E5}", label: "Installing skill" },
1166
+ budget_status: { icon: "\u{1F4B0}", label: "Budget status" }
1167
+ };
1168
+ function extractDomain(url) {
1169
+ try {
1170
+ return new URL(url).hostname;
1171
+ } catch {
1172
+ return url;
1173
+ }
1174
+ }
1175
+ function basename(p) {
1176
+ const parts = p.replace(/\\/g, "/").split("/");
1177
+ return parts[parts.length - 1] || p;
1178
+ }
1179
+ function truncate(maxLen) {
1180
+ return (v) => v.length > maxLen ? v.slice(0, maxLen) + "\u2026" : v;
1181
+ }
1182
+ function formatToolStep(toolName, args) {
1183
+ const config = TOOL_LABELS[toolName];
1184
+ if (!config) {
1185
+ return toolName;
1186
+ }
1187
+ let detail = "";
1188
+ if (config.argKey && args[config.argKey] !== void 0) {
1189
+ const raw = String(args[config.argKey]);
1190
+ detail = config.argTransform ? config.argTransform(raw) : raw;
1191
+ }
1192
+ return detail ? `${config.icon} ${config.label} ${detail}` : `${config.icon} ${config.label}`;
1193
+ }
1194
+
1109
1195
  // src/utils/arrow-select.ts
1110
1196
  import readline from "readline";
1111
1197
  import chalk2 from "chalk";
@@ -1226,6 +1312,12 @@ async function selectWithArrowKeys(title, options, config = {}) {
1226
1312
  }
1227
1313
 
1228
1314
  // src/channels/cli.ts
1315
+ var USER_PROMPT = " You: ";
1316
+ var USER_PROMPT_VISIBLE_LEN = USER_PROMPT.length;
1317
+ function agentPrefix(name, suffix) {
1318
+ const time = suffix ?? "";
1319
+ return chalk3.cyan(` ${name}:`) + time;
1320
+ }
1229
1321
  var CLIChannel = class extends BaseChannel {
1230
1322
  type = "cli";
1231
1323
  rl = null;
@@ -1233,6 +1325,9 @@ var CLIChannel = class extends BaseChannel {
1233
1325
  menuDepth = 0;
1234
1326
  menuAbortController = null;
1235
1327
  outputInProgress = 0;
1328
+ streamActive = false;
1329
+ streamToolLines = 0;
1330
+ lastUserInput = "";
1236
1331
  constructor(agentName = "Mercury") {
1237
1332
  super();
1238
1333
  this.agentName = agentName;
@@ -1243,16 +1338,17 @@ var CLIChannel = class extends BaseChannel {
1243
1338
  async start() {
1244
1339
  this.createInterface();
1245
1340
  this.ready = true;
1246
- this.showPrompt();
1247
1341
  logger.info("CLI channel started");
1248
1342
  }
1249
1343
  createInterface() {
1250
1344
  this.rl = readline2.createInterface({
1251
1345
  input: process.stdin,
1252
- output: process.stdout,
1253
- prompt: " You: "
1346
+ output: process.stdout
1254
1347
  });
1348
+ this.rl.setPrompt(chalk3.yellow(USER_PROMPT));
1349
+ this.rl._promptLength = USER_PROMPT_VISIBLE_LEN;
1255
1350
  this.rl.on("line", (line) => {
1351
+ this.lastUserInput = line.trim();
1256
1352
  const trimmed = line.trim();
1257
1353
  if (!trimmed) {
1258
1354
  this.showPrompt();
@@ -1279,9 +1375,9 @@ var CLIChannel = class extends BaseChannel {
1279
1375
  this.beginOutput();
1280
1376
  const timeStr = elapsedMs != null ? chalk3.dim(` (${(elapsedMs / 1e3).toFixed(1)}s)`) : "";
1281
1377
  const rendered = renderMarkdown(content);
1378
+ const indented = this.indent(rendered);
1282
1379
  console.log("");
1283
- console.log(chalk3.cyan(` ${this.agentName}:`) + timeStr);
1284
- const indented = rendered.split("\n").map((line) => ` ${line}`).join("\n");
1380
+ console.log(agentPrefix(this.agentName, timeStr));
1285
1381
  console.log(indented);
1286
1382
  console.log("");
1287
1383
  this.endOutput();
@@ -1298,23 +1394,69 @@ var CLIChannel = class extends BaseChannel {
1298
1394
  const stat = fs.statSync(resolved);
1299
1395
  const sizeStr = stat.size > 1024 * 1024 ? `${(stat.size / (1024 * 1024)).toFixed(1)}MB` : stat.size > 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
1300
1396
  console.log("");
1301
- console.log(chalk3.cyan(` ${this.agentName}:`) + chalk3.dim(" (file)"));
1397
+ console.log(agentPrefix(this.agentName, chalk3.dim(" (file)")));
1302
1398
  console.log(chalk3.dim(` path: ${resolved}`));
1303
1399
  console.log(chalk3.dim(` size: ${sizeStr}`));
1304
1400
  console.log("");
1305
1401
  this.endOutput();
1306
1402
  }
1403
+ async sendToolFeedback(toolName, args) {
1404
+ const label = formatToolStep(toolName, args);
1405
+ if (this.streamActive) {
1406
+ this.streamToolLines++;
1407
+ process.stdout.write(chalk3.dim(`
1408
+ ${label}
1409
+ `));
1410
+ } else {
1411
+ console.log(chalk3.dim(` ${label}`));
1412
+ }
1413
+ }
1307
1414
  async stream(content, _targetId) {
1308
1415
  this.closeActiveMenu();
1309
1416
  this.beginOutput();
1310
- console.log("");
1311
- process.stdout.write(chalk3.cyan(` ${this.agentName}: `));
1417
+ this.streamActive = true;
1418
+ this.streamToolLines = 0;
1419
+ if (!process.stdout.isTTY) {
1420
+ process.stdout.write(chalk3.cyan(` ${this.agentName}: `));
1421
+ let full2 = "";
1422
+ for await (const chunk of content) {
1423
+ process.stdout.write(chunk);
1424
+ full2 += chunk;
1425
+ }
1426
+ this.streamActive = false;
1427
+ console.log("\n");
1428
+ this.endOutput();
1429
+ return full2;
1430
+ }
1431
+ console.log(chalk3.cyan(` ${this.agentName}:`));
1312
1432
  let full = "";
1313
1433
  for await (const chunk of content) {
1314
1434
  process.stdout.write(chunk);
1315
1435
  full += chunk;
1316
1436
  }
1317
- console.log("\n");
1437
+ this.streamActive = false;
1438
+ if (!full.trim()) {
1439
+ console.log("");
1440
+ this.endOutput();
1441
+ return full;
1442
+ }
1443
+ const termWidth = process.stdout.columns || 80;
1444
+ let textLineCount = 0;
1445
+ for (const line of full.split("\n")) {
1446
+ const visualLen = line.replace(/\x1b\[[0-9;]*m/g, "").length;
1447
+ textLineCount += Math.max(1, Math.ceil((visualLen + 2) / termWidth));
1448
+ }
1449
+ const totalLines = 1 + textLineCount + this.streamToolLines;
1450
+ process.stdout.write(`\x1B[${totalLines}A`);
1451
+ for (let i = 0; i < totalLines; i++) {
1452
+ process.stdout.write("\x1B[2K\x1B[1B");
1453
+ }
1454
+ process.stdout.write(`\x1B[${totalLines}A`);
1455
+ const rendered = renderMarkdown(full);
1456
+ const indented = this.indent(rendered);
1457
+ console.log(agentPrefix(this.agentName));
1458
+ console.log(indented);
1459
+ console.log("");
1318
1460
  this.endOutput();
1319
1461
  return full;
1320
1462
  }
@@ -1323,8 +1465,8 @@ var CLIChannel = class extends BaseChannel {
1323
1465
  }
1324
1466
  showPrompt() {
1325
1467
  if (this.rl) {
1326
- this.rl.setPrompt(" You: ");
1327
- this.rl.prompt();
1468
+ process.stdout.write("\x1B[2K\r");
1469
+ process.stdout.write(chalk3.yellow(USER_PROMPT));
1328
1470
  }
1329
1471
  }
1330
1472
  async withMenu(runner) {
@@ -1377,6 +1519,9 @@ var CLIChannel = class extends BaseChannel {
1377
1519
  if (!this.ready || this.rl) return;
1378
1520
  this.createInterface();
1379
1521
  }
1522
+ indent(text) {
1523
+ return text.split("\n").map((line) => ` ${line}`).join("\n");
1524
+ }
1380
1525
  async prompt(question) {
1381
1526
  return new Promise((resolve13) => {
1382
1527
  this.rl?.question(question, (answer) => resolve13(answer.trim()));
@@ -1391,13 +1536,37 @@ var CLIChannel = class extends BaseChannel {
1391
1536
  });
1392
1537
  });
1393
1538
  }
1539
+ async askToContinue(question, _targetId) {
1540
+ return new Promise((resolve13) => {
1541
+ console.log("");
1542
+ console.log(chalk3.yellow(` \u26A0 ${question}`));
1543
+ this.rl?.question(chalk3.yellow(" Continue? [y/N] "), (answer) => {
1544
+ const val = answer.trim().toLowerCase();
1545
+ resolve13(val === "y" || val === "yes");
1546
+ });
1547
+ });
1548
+ }
1394
1549
  };
1395
1550
 
1396
1551
  // src/core/agent.ts
1397
- var ToolCallLoopDetector = class {
1552
+ var ToolCallLoopDetector = class _ToolCallLoopDetector {
1398
1553
  recentCalls = [];
1399
- maxEntries = 16;
1400
- aborted = false;
1554
+ maxEntries = 20;
1555
+ hardAborted = false;
1556
+ static HIGH_TOLERANCE_TOOLS = /* @__PURE__ */ new Set([
1557
+ "fetch_url",
1558
+ "read_file",
1559
+ "list_dir",
1560
+ "web_search",
1561
+ "github_api",
1562
+ "run_command"
1563
+ ]);
1564
+ static getIdenticalThreshold() {
1565
+ return 3;
1566
+ }
1567
+ static getSameToolThreshold(toolName) {
1568
+ return _ToolCallLoopDetector.HIGH_TOLERANCE_TOOLS.has(toolName) ? 6 : 4;
1569
+ }
1401
1570
  record(toolName, params) {
1402
1571
  const paramsKey = JSON.stringify(params).slice(0, 100);
1403
1572
  this.recentCalls.push({ tool: toolName, params: paramsKey });
@@ -1405,7 +1574,7 @@ var ToolCallLoopDetector = class {
1405
1574
  this.recentCalls.shift();
1406
1575
  }
1407
1576
  }
1408
- detect() {
1577
+ detectIdentical() {
1409
1578
  if (this.recentCalls.length < 3) return null;
1410
1579
  const last = this.recentCalls[this.recentCalls.length - 1];
1411
1580
  let identicalCount = 0;
@@ -1416,39 +1585,42 @@ var ToolCallLoopDetector = class {
1416
1585
  break;
1417
1586
  }
1418
1587
  }
1419
- if (identicalCount >= 3) {
1420
- this.aborted = true;
1588
+ if (identicalCount >= _ToolCallLoopDetector.getIdenticalThreshold()) {
1589
+ this.hardAborted = true;
1421
1590
  return {
1422
1591
  tool: last.tool,
1423
1592
  count: identicalCount,
1424
- message: `You called "${last.tool}" ${identicalCount} times with identical parameters and got the same result. Stop repeating this call entirely.`
1593
+ message: `[SYSTEM] You called "${last.tool}" ${identicalCount} times with identical parameters and got the same result. This is a hard loop \u2014 stop immediately.`
1425
1594
  };
1426
1595
  }
1427
- const lastTool = last.tool;
1596
+ return null;
1597
+ }
1598
+ detectSameTool() {
1599
+ if (this.recentCalls.length < 4) return null;
1600
+ const last = this.recentCalls[this.recentCalls.length - 1];
1428
1601
  let sameToolCount = 0;
1429
1602
  for (let i = this.recentCalls.length - 1; i >= 0; i--) {
1430
- if (this.recentCalls[i].tool === lastTool) {
1603
+ if (this.recentCalls[i].tool === last.tool) {
1431
1604
  sameToolCount++;
1432
1605
  } else {
1433
1606
  break;
1434
1607
  }
1435
1608
  }
1436
- if (sameToolCount >= 3) {
1437
- this.aborted = true;
1609
+ const threshold = _ToolCallLoopDetector.getSameToolThreshold(last.tool);
1610
+ if (sameToolCount >= threshold) {
1438
1611
  return {
1439
- tool: lastTool,
1440
- count: sameToolCount,
1441
- message: `You called "${lastTool}" ${sameToolCount} times in a row with slightly different parameters and it isn't working. Stop \u2014 the approach is wrong. Step back, tell the user what you tried and what failed, and suggest alternatives instead of retrying.`
1612
+ tool: last.tool,
1613
+ count: sameToolCount
1442
1614
  };
1443
1615
  }
1444
1616
  return null;
1445
1617
  }
1446
- isAborted() {
1447
- return this.aborted;
1618
+ isHardAborted() {
1619
+ return this.hardAborted;
1448
1620
  }
1449
1621
  reset() {
1450
1622
  this.recentCalls = [];
1451
- this.aborted = false;
1623
+ this.hardAborted = false;
1452
1624
  }
1453
1625
  };
1454
1626
  var MAX_STEPS = 10;
@@ -1612,30 +1784,6 @@ You can override this:
1612
1784
  const recentMemory = this.shortTerm.getRecent(msg.channelId, 10);
1613
1785
  const relevantFacts = this.longTerm.search(msg.content, 3);
1614
1786
  const messages = [];
1615
- const recentSteps = this.shortTerm.getRecent(msg.channelId, 4);
1616
- let loopWarning = null;
1617
- if (recentSteps.length >= 3) {
1618
- const toolCallPattern = /\[Using: (.+?)\]/g;
1619
- const toolCalls = [];
1620
- for (const m of recentSteps) {
1621
- if (m.role === "assistant") {
1622
- let match;
1623
- while ((match = toolCallPattern.exec(m.content)) !== null) {
1624
- toolCalls.push(match[1]);
1625
- }
1626
- }
1627
- }
1628
- if (toolCalls.length >= 3) {
1629
- const last3 = toolCalls.slice(-3);
1630
- if (last3[0] === last3[1] && last3[1] === last3[2]) {
1631
- loopWarning = `[SYSTEM WARNING] In previous turns you called ${last3[0]} repeatedly. Do NOT call it again. If something failed, explain the failure to the user and suggest alternatives.`;
1632
- }
1633
- }
1634
- }
1635
- if (loopWarning) {
1636
- messages.push({ role: "user", content: loopWarning });
1637
- messages.push({ role: "assistant", content: "Understood. I will try a different approach." });
1638
- }
1639
1787
  if (relevantFacts.length > 0) {
1640
1788
  messages.push({
1641
1789
  role: "user",
@@ -1687,18 +1835,44 @@ You can override this:
1687
1835
  for (const tc of toolCalls) {
1688
1836
  loopDetector.record(tc.toolName, tc.args);
1689
1837
  }
1690
- const loop = loopDetector.detect();
1691
- if (loop) {
1692
- logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected \u2014 aborting generation");
1838
+ if (toolCalls.some((tc) => tc.toolName === "use_skill")) {
1839
+ loopDetector.reset();
1840
+ }
1841
+ const hardLoop = loopDetector.detectIdentical();
1842
+ if (hardLoop) {
1843
+ logger.warn({ tool: hardLoop.tool, count: hardLoop.count }, "Hard loop detected \u2014 aborting");
1693
1844
  if (!loopWarningSent && channel && msg.channelType !== "internal") {
1694
1845
  loopWarningSent = true;
1695
- await channel.send(`\u26A0 Loop detected \u2014 ${loop.tool} called ${loop.count}x in a row. Stopping to save tokens.`, msg.channelId).catch(() => {
1846
+ await channel.send(`\u26A0 Repeated call detected \u2014 ${hardLoop.tool} called ${hardLoop.count}x with same params. Stopping.`, msg.channelId).catch(() => {
1696
1847
  });
1697
1848
  }
1698
1849
  loopAbortController.abort();
1699
1850
  }
1700
- await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
1701
- });
1851
+ const softLoop = loopDetector.detectSameTool();
1852
+ if (softLoop && !loopWarningSent && channel && msg.channelType !== "internal") {
1853
+ loopWarningSent = true;
1854
+ const shouldContinue = await channel.askToContinue(
1855
+ `${softLoop.tool} has been called ${softLoop.count}x in a row. This might be a loop.`,
1856
+ msg.channelId
1857
+ ).catch(() => false);
1858
+ if (shouldContinue) {
1859
+ loopDetector.reset();
1860
+ loopWarningSent = false;
1861
+ } else {
1862
+ loopAbortController.abort();
1863
+ }
1864
+ }
1865
+ if (channel && msg.channelType !== "internal") {
1866
+ if (channel instanceof CLIChannel) {
1867
+ for (const tc of toolCalls) {
1868
+ await channel.sendToolFeedback(tc.toolName, tc.args).catch(() => {
1869
+ });
1870
+ }
1871
+ } else {
1872
+ await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
1873
+ });
1874
+ }
1875
+ }
1702
1876
  }
1703
1877
  }
1704
1878
  });
@@ -1738,19 +1912,43 @@ You can override this:
1738
1912
  for (const tc of toolCalls) {
1739
1913
  loopDetector.record(tc.toolName, tc.args);
1740
1914
  }
1741
- const loop = loopDetector.detect();
1742
- if (loop) {
1743
- logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected \u2014 aborting generation");
1915
+ if (toolCalls.some((tc) => tc.toolName === "use_skill")) {
1916
+ loopDetector.reset();
1917
+ }
1918
+ const hardLoop = loopDetector.detectIdentical();
1919
+ if (hardLoop) {
1920
+ logger.warn({ tool: hardLoop.tool, count: hardLoop.count }, "Hard loop detected \u2014 aborting");
1744
1921
  if (!loopWarningSent && channel && msg.channelType !== "internal") {
1745
1922
  loopWarningSent = true;
1746
- await channel.send(`\u26A0 Loop detected \u2014 ${loop.tool} called ${loop.count}x in a row. Stopping to save tokens.`, msg.channelId).catch(() => {
1923
+ await channel.send(`\u26A0 Repeated call detected \u2014 ${hardLoop.tool} called ${hardLoop.count}x with same params. Stopping.`, msg.channelId).catch(() => {
1747
1924
  });
1748
1925
  }
1749
1926
  loopAbortController.abort();
1750
1927
  }
1928
+ const softLoop = loopDetector.detectSameTool();
1929
+ if (softLoop && !loopWarningSent && channel && msg.channelType !== "internal") {
1930
+ loopWarningSent = true;
1931
+ const shouldContinue = await channel.askToContinue(
1932
+ `${softLoop.tool} has been called ${softLoop.count}x in a row. This might be a loop.`,
1933
+ msg.channelId
1934
+ ).catch(() => false);
1935
+ if (shouldContinue) {
1936
+ loopDetector.reset();
1937
+ loopWarningSent = false;
1938
+ } else {
1939
+ loopAbortController.abort();
1940
+ }
1941
+ }
1751
1942
  if (channel && msg.channelType !== "internal") {
1752
- await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
1753
- });
1943
+ if (channel instanceof CLIChannel) {
1944
+ for (const tc of toolCalls) {
1945
+ await channel.sendToolFeedback(tc.toolName, tc.args).catch(() => {
1946
+ });
1947
+ }
1948
+ } else {
1949
+ await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
1950
+ });
1951
+ }
1754
1952
  }
1755
1953
  }
1756
1954
  }
@@ -1760,11 +1958,14 @@ You can override this:
1760
1958
  this.providers.markSuccess(provider.name);
1761
1959
  break;
1762
1960
  } catch (err) {
1763
- if (loopDetector.isAborted()) {
1961
+ if (loopDetector.isHardAborted() || loopAbortController.signal.aborted) {
1764
1962
  logger.info("Generation aborted due to loop detection \u2014 using partial response");
1765
1963
  if (!result && streamedText) {
1766
1964
  result = { text: streamedText, usage: void 0 };
1767
1965
  }
1966
+ if (!result) {
1967
+ result = { text: "I stopped because I was repeating the same tool calls. What would you like me to do differently?", usage: void 0 };
1968
+ }
1768
1969
  if (usedProvider) {
1769
1970
  this.providers.markSuccess(usedProvider.name);
1770
1971
  }
@@ -2989,6 +3190,32 @@ var TelegramChannel = class extends BaseChannel {
2989
3190
  }, 12e4);
2990
3191
  });
2991
3192
  }
3193
+ async askToContinue(question, targetId) {
3194
+ const chatIds = this.resolveTargetChatIds(targetId);
3195
+ const chatId = chatIds[0];
3196
+ if (!chatId || !this.bot) return false;
3197
+ const id = `loop_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
3198
+ const keyboard = new InlineKeyboard().text("Continue", `${id}:yes`).text("Stop", `${id}:no`);
3199
+ try {
3200
+ await this.bot.api.sendMessage(chatId, mdToTelegram(question), {
3201
+ parse_mode: "HTML",
3202
+ reply_markup: keyboard
3203
+ });
3204
+ } catch {
3205
+ await this.bot.api.sendMessage(chatId, question, {
3206
+ reply_markup: keyboard
3207
+ });
3208
+ }
3209
+ return new Promise((resolve13) => {
3210
+ this.pendingApprovals.set(`${id}:yes`, () => resolve13(true));
3211
+ this.pendingApprovals.set(`${id}:no`, () => resolve13(false));
3212
+ setTimeout(() => {
3213
+ this.pendingApprovals.delete(`${id}:yes`);
3214
+ this.pendingApprovals.delete(`${id}:no`);
3215
+ resolve13(false);
3216
+ }, 12e4);
3217
+ });
3218
+ }
2992
3219
  async handleAccessRequest(userId, chatId, username, firstName) {
2993
3220
  const approvedUser = findTelegramApprovedUser(this.config, userId);
2994
3221
  if (approvedUser) {
@@ -4003,7 +4230,7 @@ function createEditFileTool(permissions, getCwd) {
4003
4230
  import { tool as tool7 } from "ai";
4004
4231
  import { z as z7 } from "zod";
4005
4232
  import { existsSync as existsSync12, statSync as statSync2 } from "fs";
4006
- import { resolve as resolve9, basename, isAbsolute as isAbsolute7 } from "path";
4233
+ import { resolve as resolve9, basename as basename2, isAbsolute as isAbsolute7 } from "path";
4007
4234
  function createSendFileTool(permissions, getCwd, sendFile) {
4008
4235
  return tool7({
4009
4236
  description: "Send a file to the user. On Telegram the file is uploaded as an attachment to the relevant approved recipients. On CLI the file path and size are displayed. The path must be within an allowed read scope.",
@@ -4029,7 +4256,7 @@ function createSendFileTool(permissions, getCwd, sendFile) {
4029
4256
  }
4030
4257
  try {
4031
4258
  await sendFile(resolved);
4032
- const filename = basename(resolved);
4259
+ const filename = basename2(resolved);
4033
4260
  const sizeStr = stat.size > 1024 * 1024 ? `${(stat.size / (1024 * 1024)).toFixed(1)}MB` : `${Math.round(stat.size / 1024)}KB`;
4034
4261
  return `File sent: ${filename} (${sizeStr})`;
4035
4262
  } catch (err) {
@@ -6851,6 +7078,7 @@ async function runAgent(isDaemon = false) {
6851
7078
  console.log(chalk7.green(` ${name} is live. Type a message and press Enter.`));
6852
7079
  console.log(chalk7.dim(" Ctrl+C to exit \xB7 /help for commands"));
6853
7080
  console.log("");
7081
+ cliChannel?.showPrompt();
6854
7082
  } else {
6855
7083
  logger.info({ channels: activeCh, tools: toolNames }, "Mercury is live (daemon mode)");
6856
7084
  }
@@ -7123,5 +7351,60 @@ serviceCmd.command("uninstall").description("Uninstall the system service").acti
7123
7351
  serviceCmd.command("status").description("Show system service status").action(() => {
7124
7352
  showServiceStatus();
7125
7353
  });
7354
+ program.command("upgrade").description("Upgrade Mercury to the latest version from npm").action(async () => {
7355
+ console.log("");
7356
+ console.log(chalk7.cyan(` Mercury ${chalk7.white(`v${pkgVersion}`)}`));
7357
+ console.log("");
7358
+ const daemon = getDaemonStatus();
7359
+ if (daemon.running) {
7360
+ console.log(chalk7.dim(" Stopping background daemon..."));
7361
+ stopDaemon();
7362
+ await new Promise((r) => setTimeout(r, 1e3));
7363
+ console.log(chalk7.green(" \u2713 Daemon stopped"));
7364
+ }
7365
+ console.log(chalk7.dim(" Checking for latest version..."));
7366
+ const { execSync: execSync9 } = await import("child_process");
7367
+ let latestVersion = "";
7368
+ try {
7369
+ latestVersion = execSync9("npm view @cosmicstack/mercury-agent version", { encoding: "utf-8" }).trim();
7370
+ } catch {
7371
+ console.log(chalk7.red(" \u2717 Failed to fetch latest version from npm"));
7372
+ console.log("");
7373
+ return;
7374
+ }
7375
+ console.log(chalk7.dim(` Latest: v${latestVersion}`));
7376
+ if (latestVersion === pkgVersion) {
7377
+ console.log(chalk7.green(` \u2713 Already on the latest version (v${pkgVersion})`));
7378
+ console.log("");
7379
+ return;
7380
+ }
7381
+ console.log(chalk7.dim(` Upgrading v${pkgVersion} \u2192 v${latestVersion}...`));
7382
+ console.log("");
7383
+ try {
7384
+ execSync9("npm rm -g @cosmicstack/mercury-agent", { stdio: "pipe" });
7385
+ } catch {
7386
+ try {
7387
+ const globalDir = execSync9("npm root -g", { encoding: "utf-8" }).trim();
7388
+ const pkgDir = join11(globalDir, "@cosmicstack", "mercury-agent");
7389
+ const { rmSync } = await import("fs");
7390
+ try {
7391
+ rmSync(pkgDir, { recursive: true, force: true });
7392
+ } catch {
7393
+ }
7394
+ } catch {
7395
+ }
7396
+ }
7397
+ try {
7398
+ execSync9("npm i -g @cosmicstack/mercury-agent@latest", { stdio: "inherit" });
7399
+ console.log("");
7400
+ console.log(chalk7.green(` \u2713 Upgraded to v${latestVersion}`));
7401
+ console.log(chalk7.dim(" Run `mercury` to start the new version."));
7402
+ } catch {
7403
+ console.log("");
7404
+ console.log(chalk7.red(" \u2717 Upgrade failed. Try manually:"));
7405
+ console.log(chalk7.dim(" npm rm -g @cosmicstack/mercury-agent && npm i -g @cosmicstack/mercury-agent"));
7406
+ }
7407
+ console.log("");
7408
+ });
7126
7409
  program.parse();
7127
7410
  //# sourceMappingURL=index.js.map