@cosmicstack/mercury-agent 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +331 -110
- package/dist/index.js.map +1 -1
- package/package.json +10 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as readFileSync12, writeFileSync as
|
|
4
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync14, existsSync as existsSync19 } from "fs";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { dirname as dirname4, join as
|
|
6
|
+
import { dirname as dirname4, join as join14 } from "path";
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import readline3 from "readline";
|
|
9
9
|
import chalk7 from "chalk";
|
|
@@ -599,7 +599,7 @@ var LongTermMemory = class {
|
|
|
599
599
|
}
|
|
600
600
|
load() {
|
|
601
601
|
if (!existsSync3(this.filepath)) return;
|
|
602
|
-
const lines = readFileSync3(this.filepath, "utf-8").split(
|
|
602
|
+
const lines = readFileSync3(this.filepath, "utf-8").split(/\r?\n/).filter(Boolean);
|
|
603
603
|
this.facts = lines.map((line) => {
|
|
604
604
|
try {
|
|
605
605
|
return JSON.parse(line);
|
|
@@ -641,7 +641,7 @@ var EpisodicMemory = class {
|
|
|
641
641
|
}
|
|
642
642
|
load() {
|
|
643
643
|
if (!existsSync3(this.filepath)) return;
|
|
644
|
-
const lines = readFileSync3(this.filepath, "utf-8").split(
|
|
644
|
+
const lines = readFileSync3(this.filepath, "utf-8").split(/\r?\n/).filter(Boolean);
|
|
645
645
|
this.events = lines.map((line) => {
|
|
646
646
|
try {
|
|
647
647
|
return JSON.parse(line);
|
|
@@ -656,17 +656,43 @@ function generateId() {
|
|
|
656
656
|
}
|
|
657
657
|
|
|
658
658
|
// src/memory/second-brain-db.ts
|
|
659
|
-
import
|
|
660
|
-
import {
|
|
661
|
-
import {
|
|
659
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, rmSync as rmSync2 } from "fs";
|
|
660
|
+
import { dirname, join as join4 } from "path";
|
|
661
|
+
import { tmpdir } from "os";
|
|
662
|
+
import { createRequire } from "module";
|
|
663
|
+
var require2 = createRequire(import.meta.url);
|
|
664
|
+
var syncDatabaseClass = null;
|
|
665
|
+
try {
|
|
666
|
+
const mod = require2("better-sqlite3");
|
|
667
|
+
const probeDir = join4(tmpdir(), `mercury-sqlite3-probe-${process.pid}`);
|
|
668
|
+
try {
|
|
669
|
+
mkdirSync4(probeDir, { recursive: true });
|
|
670
|
+
const probeDb = new mod(join4(probeDir, "probe.db"));
|
|
671
|
+
probeDb.close();
|
|
672
|
+
rmSync2(probeDir, { recursive: true, force: true });
|
|
673
|
+
syncDatabaseClass = mod;
|
|
674
|
+
} catch {
|
|
675
|
+
syncDatabaseClass = null;
|
|
676
|
+
}
|
|
677
|
+
} catch {
|
|
678
|
+
syncDatabaseClass = null;
|
|
679
|
+
}
|
|
680
|
+
function isBetterSqlite3Available() {
|
|
681
|
+
return syncDatabaseClass !== null;
|
|
682
|
+
}
|
|
662
683
|
var SecondBrainDB = class {
|
|
663
684
|
db;
|
|
664
685
|
constructor(dbPath) {
|
|
686
|
+
if (!syncDatabaseClass) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
"better-sqlite3 is not available \u2014 second brain memory requires it. Install build tools (make, gcc/g++, python3) or upgrade to Node >= 20. See: https://github.com/WiseLibs/better-sqlite3/blob/master/docs/compilation.md"
|
|
689
|
+
);
|
|
690
|
+
}
|
|
665
691
|
const dir = dirname(dbPath);
|
|
666
692
|
if (!existsSync4(dir)) {
|
|
667
693
|
mkdirSync4(dir, { recursive: true });
|
|
668
694
|
}
|
|
669
|
-
this.db = new
|
|
695
|
+
this.db = new syncDatabaseClass(dbPath);
|
|
670
696
|
this.db.pragma("journal_mode = WAL");
|
|
671
697
|
this.db.pragma("synchronous = NORMAL");
|
|
672
698
|
}
|
|
@@ -954,7 +980,7 @@ var SecondBrainDB = class {
|
|
|
954
980
|
};
|
|
955
981
|
|
|
956
982
|
// src/memory/user-memory.ts
|
|
957
|
-
import { join as
|
|
983
|
+
import { join as join5 } from "path";
|
|
958
984
|
var MIN_CONFIDENCE = 0.55;
|
|
959
985
|
var UserMemoryStore = class {
|
|
960
986
|
db;
|
|
@@ -966,7 +992,7 @@ var UserMemoryStore = class {
|
|
|
966
992
|
this.userKey = userKey;
|
|
967
993
|
this.maxRecords = config.memory.secondBrain?.maxRecords ?? 50;
|
|
968
994
|
this.consolidateThrottleMs = 5 * 60 * 1e3;
|
|
969
|
-
const resolvedDbPath = dbPath ??
|
|
995
|
+
const resolvedDbPath = dbPath ?? join5(getMemoryDir(), "second-brain", "second-brain.db");
|
|
970
996
|
this.db = new SecondBrainDB(resolvedDbPath);
|
|
971
997
|
this.db.init();
|
|
972
998
|
}
|
|
@@ -3411,31 +3437,52 @@ var ToolCallLoopDetector = class _ToolCallLoopDetector {
|
|
|
3411
3437
|
recentCalls = [];
|
|
3412
3438
|
totalCalls = 0;
|
|
3413
3439
|
hardAborted = false;
|
|
3440
|
+
recentStepTexts = [];
|
|
3414
3441
|
static ABSOLUTE_MAX = 25;
|
|
3442
|
+
static FAILED_ABSOLUTE_MAX = 12;
|
|
3415
3443
|
static HIGH_TOLERANCE_TOOLS = /* @__PURE__ */ new Set([
|
|
3416
3444
|
"fetch_url",
|
|
3417
3445
|
"read_file",
|
|
3418
3446
|
"list_dir",
|
|
3419
3447
|
"web_search",
|
|
3420
|
-
"github_api"
|
|
3421
|
-
"run_command"
|
|
3448
|
+
"github_api"
|
|
3422
3449
|
]);
|
|
3423
|
-
static
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
static
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3450
|
+
static IDENTICAL_THRESHOLD = 3;
|
|
3451
|
+
static SIMILAR_THRESHOLD = 4;
|
|
3452
|
+
static TEXT_REPEAT_THRESHOLD = 3;
|
|
3453
|
+
static MAX_STEP_TEXTS = 12;
|
|
3454
|
+
static getSameToolThreshold(toolName, failingCount) {
|
|
3455
|
+
const baseHigh = 5;
|
|
3456
|
+
const baseNormal = 3;
|
|
3457
|
+
const isHigh = _ToolCallLoopDetector.HIGH_TOLERANCE_TOOLS.has(toolName);
|
|
3458
|
+
let threshold = isHigh ? baseHigh : baseNormal;
|
|
3459
|
+
if (failingCount >= 3) {
|
|
3460
|
+
threshold = Math.min(threshold, isHigh ? 3 : 2);
|
|
3461
|
+
}
|
|
3462
|
+
return threshold;
|
|
3463
|
+
}
|
|
3464
|
+
record(toolName, params, failed = false) {
|
|
3430
3465
|
const paramsKey = JSON.stringify(params).slice(0, 200);
|
|
3431
|
-
this.recentCalls.push({ tool: toolName, params: paramsKey });
|
|
3466
|
+
this.recentCalls.push({ tool: toolName, params: paramsKey, failed });
|
|
3432
3467
|
this.totalCalls++;
|
|
3433
3468
|
if (this.recentCalls.length > 30) {
|
|
3434
3469
|
this.recentCalls.shift();
|
|
3435
3470
|
}
|
|
3436
3471
|
}
|
|
3472
|
+
recordStepText(text) {
|
|
3473
|
+
if (!text || text.length < 10) return;
|
|
3474
|
+
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 200);
|
|
3475
|
+
if (!normalized) return;
|
|
3476
|
+
this.recentStepTexts.push(normalized);
|
|
3477
|
+
if (this.recentStepTexts.length > _ToolCallLoopDetector.MAX_STEP_TEXTS) {
|
|
3478
|
+
this.recentStepTexts.shift();
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3437
3481
|
detectAbsoluteLimit() {
|
|
3438
|
-
|
|
3482
|
+
if (this.totalCalls >= _ToolCallLoopDetector.ABSOLUTE_MAX) return true;
|
|
3483
|
+
const failCount = this.recentCalls.filter((c) => c.failed).length;
|
|
3484
|
+
if (failCount >= _ToolCallLoopDetector.FAILED_ABSOLUTE_MAX) return true;
|
|
3485
|
+
return false;
|
|
3439
3486
|
}
|
|
3440
3487
|
detectIdentical() {
|
|
3441
3488
|
if (this.recentCalls.length < 3) return null;
|
|
@@ -3448,7 +3495,7 @@ var ToolCallLoopDetector = class _ToolCallLoopDetector {
|
|
|
3448
3495
|
break;
|
|
3449
3496
|
}
|
|
3450
3497
|
}
|
|
3451
|
-
if (identicalCount >= _ToolCallLoopDetector.
|
|
3498
|
+
if (identicalCount >= _ToolCallLoopDetector.IDENTICAL_THRESHOLD) {
|
|
3452
3499
|
this.hardAborted = true;
|
|
3453
3500
|
return {
|
|
3454
3501
|
tool: last.tool,
|
|
@@ -3458,18 +3505,73 @@ var ToolCallLoopDetector = class _ToolCallLoopDetector {
|
|
|
3458
3505
|
}
|
|
3459
3506
|
return null;
|
|
3460
3507
|
}
|
|
3508
|
+
detectSimilarLoop() {
|
|
3509
|
+
if (this.recentCalls.length < 4) return null;
|
|
3510
|
+
const last = this.recentCalls[this.recentCalls.length - 1];
|
|
3511
|
+
let similarCount = 0;
|
|
3512
|
+
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
3513
|
+
const call = this.recentCalls[i];
|
|
3514
|
+
if (call.tool !== last.tool) break;
|
|
3515
|
+
if (call.failed || last.failed) {
|
|
3516
|
+
similarCount++;
|
|
3517
|
+
} else {
|
|
3518
|
+
break;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
if (similarCount >= _ToolCallLoopDetector.SIMILAR_THRESHOLD) {
|
|
3522
|
+
this.hardAborted = true;
|
|
3523
|
+
return {
|
|
3524
|
+
tool: last.tool,
|
|
3525
|
+
count: similarCount,
|
|
3526
|
+
message: `[SYSTEM] You called "${last.tool}" ${similarCount} times with different params but all are failing. This is a failing loop \u2014 stop immediately. Tell the user you cannot complete this task.`
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
return null;
|
|
3530
|
+
}
|
|
3531
|
+
detectTextRepetition() {
|
|
3532
|
+
if (this.recentStepTexts.length < _ToolCallLoopDetector.TEXT_REPEAT_THRESHOLD) return null;
|
|
3533
|
+
const texts = this.recentStepTexts;
|
|
3534
|
+
const last = texts[texts.length - 1];
|
|
3535
|
+
let repeatCount = 0;
|
|
3536
|
+
for (let i = texts.length - 1; i >= 0; i--) {
|
|
3537
|
+
const similarity = this.textSimilarity(last, texts[i]);
|
|
3538
|
+
if (similarity >= 0.7) {
|
|
3539
|
+
repeatCount++;
|
|
3540
|
+
} else {
|
|
3541
|
+
break;
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
if (repeatCount >= _ToolCallLoopDetector.TEXT_REPEAT_THRESHOLD) {
|
|
3545
|
+
return {
|
|
3546
|
+
pattern: last.slice(0, 60),
|
|
3547
|
+
count: repeatCount
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
return null;
|
|
3551
|
+
}
|
|
3552
|
+
textSimilarity(a, b) {
|
|
3553
|
+
if (a === b) return 1;
|
|
3554
|
+
if (!a || !b) return 0;
|
|
3555
|
+
const setA = new Set(a.split(" "));
|
|
3556
|
+
const setB = new Set(b.split(" "));
|
|
3557
|
+
const intersection = [...setA].filter((w) => setB.has(w)).length;
|
|
3558
|
+
const union = (/* @__PURE__ */ new Set([...setA, ...setB])).size;
|
|
3559
|
+
return union === 0 ? 0 : intersection / union;
|
|
3560
|
+
}
|
|
3461
3561
|
detectSameTool() {
|
|
3462
3562
|
if (this.recentCalls.length < 3) return null;
|
|
3463
3563
|
const last = this.recentCalls[this.recentCalls.length - 1];
|
|
3464
3564
|
let consecutiveCount = 0;
|
|
3565
|
+
let failingConsecutive = 0;
|
|
3465
3566
|
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
3466
3567
|
if (this.recentCalls[i].tool === last.tool) {
|
|
3467
3568
|
consecutiveCount++;
|
|
3569
|
+
if (this.recentCalls[i].failed) failingConsecutive++;
|
|
3468
3570
|
} else {
|
|
3469
3571
|
break;
|
|
3470
3572
|
}
|
|
3471
3573
|
}
|
|
3472
|
-
const threshold = _ToolCallLoopDetector.getSameToolThreshold(last.tool);
|
|
3574
|
+
const threshold = _ToolCallLoopDetector.getSameToolThreshold(last.tool, failingConsecutive);
|
|
3473
3575
|
if (consecutiveCount >= threshold) {
|
|
3474
3576
|
return { tool: last.tool, count: consecutiveCount };
|
|
3475
3577
|
}
|
|
@@ -3494,6 +3596,7 @@ var ToolCallLoopDetector = class _ToolCallLoopDetector {
|
|
|
3494
3596
|
this.recentCalls = [];
|
|
3495
3597
|
this.totalCalls = 0;
|
|
3496
3598
|
this.hardAborted = false;
|
|
3599
|
+
this.recentStepTexts = [];
|
|
3497
3600
|
}
|
|
3498
3601
|
};
|
|
3499
3602
|
var MAX_STEPS = 10;
|
|
@@ -3659,7 +3762,7 @@ You can override this:
|
|
|
3659
3762
|
const systemPrompt = this.buildSystemPrompt();
|
|
3660
3763
|
const recentMemory = this.shortTerm.getRecent(msg.channelId, 10);
|
|
3661
3764
|
const messages = [];
|
|
3662
|
-
const recentSteps = this.shortTerm.getRecent(msg.channelId,
|
|
3765
|
+
const recentSteps = this.shortTerm.getRecent(msg.channelId, 6);
|
|
3663
3766
|
let loopWarning = null;
|
|
3664
3767
|
if (recentSteps.length >= 3) {
|
|
3665
3768
|
const toolCallPattern = /\[Using: (.+?)\]/g;
|
|
@@ -3678,10 +3781,24 @@ You can override this:
|
|
|
3678
3781
|
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.`;
|
|
3679
3782
|
}
|
|
3680
3783
|
}
|
|
3784
|
+
if (!loopWarning) {
|
|
3785
|
+
const assistantMessages = recentSteps.filter((m) => m.role === "assistant" && m.content.length > 20);
|
|
3786
|
+
if (assistantMessages.length >= 3) {
|
|
3787
|
+
const last3 = assistantMessages.slice(-3);
|
|
3788
|
+
const normalizeText = (t) => t.toLowerCase().replace(/[^\w\s]/g, "").replace(/\s+/g, " ").trim().slice(0, 150);
|
|
3789
|
+
const normalized = last3.map((m) => normalizeText(m.content));
|
|
3790
|
+
const words0 = new Set(normalized[0].split(" "));
|
|
3791
|
+
const overlap01 = normalized[0] && normalized[1] ? [...words0].filter((w) => new Set(normalized[1].split(" ")).has(w)).length / Math.max(words0.size, 1) : 0;
|
|
3792
|
+
const overlap12 = normalized[1] && normalized[2] ? [...new Set(normalized[1].split(" "))].filter((w) => new Set(normalized[2].split(" ")).has(w)).length / Math.max(new Set(normalized[1].split(" ")).size, 1) : 0;
|
|
3793
|
+
if (overlap01 > 0.75 && overlap12 > 0.75) {
|
|
3794
|
+
loopWarning = `[SYSTEM WARNING] Your last 3 responses are nearly identical. You are stuck in a text repetition loop. Stop immediately and give a completely different response. If you cannot complete the task, tell the user clearly why.`;
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3681
3798
|
}
|
|
3682
3799
|
if (loopWarning) {
|
|
3683
3800
|
messages.push({ role: "user", content: loopWarning });
|
|
3684
|
-
messages.push({ role: "assistant", content: "
|
|
3801
|
+
messages.push({ role: "assistant", content: "Acknowledged. I will stop repeating and respond differently, or clearly state if the task cannot be completed." });
|
|
3685
3802
|
}
|
|
3686
3803
|
if (this.userMemory) {
|
|
3687
3804
|
const memoryContext = this.userMemory.retrieveRelevant(msg.content, { maxRecords: 5, maxChars: 900 });
|
|
@@ -3744,11 +3861,15 @@ You can override this:
|
|
|
3744
3861
|
maxSteps: MAX_STEPS,
|
|
3745
3862
|
abortSignal: loopAbortController.signal,
|
|
3746
3863
|
onStepFinish: async ({ toolCalls, toolResults }) => {
|
|
3747
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
3864
|
+
if (toolCalls && toolResults && toolCalls.length > 0) {
|
|
3748
3865
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
3749
3866
|
logger.info({ tools: names }, "Tool call step");
|
|
3750
|
-
for (
|
|
3751
|
-
|
|
3867
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
3868
|
+
const tc = toolCalls[i];
|
|
3869
|
+
const tr = toolResults[i];
|
|
3870
|
+
const resultStr = typeof tr?.result === "string" ? tr.result : JSON.stringify(tr?.result ?? "");
|
|
3871
|
+
const failed = resultStr.length < 5e3 && (resultStr.startsWith("Error:") || resultStr.startsWith("\u26A0") || resultStr.includes("exited with code") || resultStr.includes("Command failed") || resultStr.startsWith("Command exited with code"));
|
|
3872
|
+
loopDetector.record(tc.toolName, tc.args, failed);
|
|
3752
3873
|
}
|
|
3753
3874
|
if (loopDetector.detectAbsoluteLimit()) {
|
|
3754
3875
|
logger.warn("Absolute tool call limit reached \u2014 aborting");
|
|
@@ -3773,6 +3894,17 @@ You can override this:
|
|
|
3773
3894
|
loopAbortController.abort();
|
|
3774
3895
|
return;
|
|
3775
3896
|
}
|
|
3897
|
+
const similarLoop = loopDetector.detectSimilarLoop();
|
|
3898
|
+
if (similarLoop) {
|
|
3899
|
+
logger.warn({ tool: similarLoop.tool, count: similarLoop.count }, "Failing loop detected \u2014 aborting");
|
|
3900
|
+
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
3901
|
+
loopWarningSent = true;
|
|
3902
|
+
await channel.send(`\u26A0 Failing loop detected \u2014 ${similarLoop.tool} called ${similarLoop.count}x, all failing. Stopping.`, msg.channelId).catch(() => {
|
|
3903
|
+
});
|
|
3904
|
+
}
|
|
3905
|
+
loopAbortController.abort();
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3776
3908
|
const softLoop = loopDetector.detectSameTool();
|
|
3777
3909
|
if (softLoop && !loopWarningSent && channel && msg.channelType !== "internal") {
|
|
3778
3910
|
if (this.capabilities.permissions.isAutoApproveAll()) {
|
|
@@ -3828,6 +3960,21 @@ You can override this:
|
|
|
3828
3960
|
});
|
|
3829
3961
|
}
|
|
3830
3962
|
}
|
|
3963
|
+
} else if (toolResults === void 0 || toolCalls === void 0) {
|
|
3964
|
+
const stepText = toolResults?.text ?? "";
|
|
3965
|
+
if (stepText) {
|
|
3966
|
+
loopDetector.recordStepText(String(stepText));
|
|
3967
|
+
}
|
|
3968
|
+
const textRepeat = loopDetector.detectTextRepetition();
|
|
3969
|
+
if (textRepeat) {
|
|
3970
|
+
logger.warn({ pattern: textRepeat.pattern, count: textRepeat.count }, "Text repetition loop detected \u2014 aborting");
|
|
3971
|
+
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
3972
|
+
loopWarningSent = true;
|
|
3973
|
+
await channel.send("\u26A0 I keep generating the same response. Stopping to prevent repetition.", msg.channelId).catch(() => {
|
|
3974
|
+
});
|
|
3975
|
+
}
|
|
3976
|
+
loopAbortController.abort();
|
|
3977
|
+
}
|
|
3831
3978
|
}
|
|
3832
3979
|
}
|
|
3833
3980
|
});
|
|
@@ -3852,6 +3999,7 @@ You can override this:
|
|
|
3852
3999
|
]);
|
|
3853
4000
|
result = { text: fullText, usage };
|
|
3854
4001
|
streamedText = fullText;
|
|
4002
|
+
loopDetector.recordStepText(fullText);
|
|
3855
4003
|
} else {
|
|
3856
4004
|
result = await generateText3({
|
|
3857
4005
|
model: provider.getModelInstance(),
|
|
@@ -3861,11 +4009,15 @@ You can override this:
|
|
|
3861
4009
|
maxSteps: MAX_STEPS,
|
|
3862
4010
|
abortSignal: loopAbortController.signal,
|
|
3863
4011
|
onStepFinish: async ({ toolCalls, toolResults }) => {
|
|
3864
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
4012
|
+
if (toolCalls && toolResults && toolCalls.length > 0) {
|
|
3865
4013
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
3866
4014
|
logger.info({ tools: names }, "Tool call step");
|
|
3867
|
-
for (
|
|
3868
|
-
|
|
4015
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
4016
|
+
const tc = toolCalls[i];
|
|
4017
|
+
const tr = toolResults[i];
|
|
4018
|
+
const resultStr = typeof tr?.result === "string" ? tr.result : JSON.stringify(tr?.result ?? "");
|
|
4019
|
+
const failed = resultStr.length < 5e3 && (resultStr.startsWith("Error:") || resultStr.startsWith("\u26A0") || resultStr.includes("exited with code") || resultStr.includes("Command failed") || resultStr.startsWith("Command exited with code"));
|
|
4020
|
+
loopDetector.record(tc.toolName, tc.args, failed);
|
|
3869
4021
|
}
|
|
3870
4022
|
if (loopDetector.detectAbsoluteLimit()) {
|
|
3871
4023
|
logger.warn("Absolute tool call limit reached \u2014 aborting");
|
|
@@ -3890,6 +4042,17 @@ You can override this:
|
|
|
3890
4042
|
loopAbortController.abort();
|
|
3891
4043
|
return;
|
|
3892
4044
|
}
|
|
4045
|
+
const similarLoop = loopDetector.detectSimilarLoop();
|
|
4046
|
+
if (similarLoop) {
|
|
4047
|
+
logger.warn({ tool: similarLoop.tool, count: similarLoop.count }, "Failing loop detected \u2014 aborting");
|
|
4048
|
+
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
4049
|
+
loopWarningSent = true;
|
|
4050
|
+
await channel.send(`\u26A0 Failing loop detected \u2014 ${similarLoop.tool} called ${similarLoop.count}x, all failing. Stopping.`, msg.channelId).catch(() => {
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
loopAbortController.abort();
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
3893
4056
|
const softLoop = loopDetector.detectSameTool();
|
|
3894
4057
|
if (softLoop && !loopWarningSent && channel && msg.channelType !== "internal") {
|
|
3895
4058
|
if (this.capabilities.permissions.isAutoApproveAll()) {
|
|
@@ -3945,6 +4108,21 @@ You can override this:
|
|
|
3945
4108
|
});
|
|
3946
4109
|
}
|
|
3947
4110
|
}
|
|
4111
|
+
} else if (toolResults === void 0 || toolCalls === void 0) {
|
|
4112
|
+
const stepText = toolResults?.text ?? "";
|
|
4113
|
+
if (stepText) {
|
|
4114
|
+
loopDetector.recordStepText(String(stepText));
|
|
4115
|
+
}
|
|
4116
|
+
const textRepeat = loopDetector.detectTextRepetition();
|
|
4117
|
+
if (textRepeat) {
|
|
4118
|
+
logger.warn({ pattern: textRepeat.pattern, count: textRepeat.count }, "Text repetition loop detected \u2014 aborting");
|
|
4119
|
+
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
4120
|
+
loopWarningSent = true;
|
|
4121
|
+
await channel.send("\u26A0 I keep generating the same response. Stopping to prevent repetition.", msg.channelId).catch(() => {
|
|
4122
|
+
});
|
|
4123
|
+
}
|
|
4124
|
+
loopAbortController.abort();
|
|
4125
|
+
}
|
|
3948
4126
|
}
|
|
3949
4127
|
}
|
|
3950
4128
|
});
|
|
@@ -3959,7 +4137,7 @@ You can override this:
|
|
|
3959
4137
|
result = { text: streamedText, usage: void 0 };
|
|
3960
4138
|
}
|
|
3961
4139
|
if (!result) {
|
|
3962
|
-
result = { text: "I stopped because I was repeating the same
|
|
4140
|
+
result = { text: "I stopped because I detected I was stuck in a loop (repeating the same action without progress). I cannot complete this task as requested. Please let me know if you'd like me to try a completely different approach, or if there's something else I can help with.", usage: void 0 };
|
|
3963
4141
|
}
|
|
3964
4142
|
if (usedProvider) {
|
|
3965
4143
|
this.providers.markSuccess(usedProvider.name);
|
|
@@ -4862,11 +5040,11 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
|
|
|
4862
5040
|
// src/core/scheduler.ts
|
|
4863
5041
|
import cron from "node-cron";
|
|
4864
5042
|
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
|
|
4865
|
-
import { join as
|
|
5043
|
+
import { join as join6 } from "path";
|
|
4866
5044
|
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
4867
5045
|
var SCHEDULES_FILE = "schedules.yaml";
|
|
4868
5046
|
function getSchedulesPath() {
|
|
4869
|
-
return
|
|
5047
|
+
return join6(getMercuryHome(), SCHEDULES_FILE);
|
|
4870
5048
|
}
|
|
4871
5049
|
function loadSchedules() {
|
|
4872
5050
|
const path3 = getSchedulesPath();
|
|
@@ -5085,7 +5263,7 @@ var ChannelRegistry = class {
|
|
|
5085
5263
|
|
|
5086
5264
|
// src/utils/tokens.ts
|
|
5087
5265
|
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
5088
|
-
import { join as
|
|
5266
|
+
import { join as join7 } from "path";
|
|
5089
5267
|
var TOKEN_FILE = "token-usage.json";
|
|
5090
5268
|
var TokenBudget = class {
|
|
5091
5269
|
constructor(config) {
|
|
@@ -5167,7 +5345,7 @@ var TokenBudget = class {
|
|
|
5167
5345
|
}
|
|
5168
5346
|
}
|
|
5169
5347
|
persist() {
|
|
5170
|
-
const path3 =
|
|
5348
|
+
const path3 = join7(getMercuryHome(), TOKEN_FILE);
|
|
5171
5349
|
try {
|
|
5172
5350
|
const data = {
|
|
5173
5351
|
dailyUsed: this.dailyUsed,
|
|
@@ -5181,7 +5359,7 @@ var TokenBudget = class {
|
|
|
5181
5359
|
}
|
|
5182
5360
|
}
|
|
5183
5361
|
restore() {
|
|
5184
|
-
const path3 =
|
|
5362
|
+
const path3 = join7(getMercuryHome(), TOKEN_FILE);
|
|
5185
5363
|
if (!existsSync6(path3)) return;
|
|
5186
5364
|
try {
|
|
5187
5365
|
const raw = readFileSync5(path3, "utf-8");
|
|
@@ -5200,7 +5378,7 @@ var TokenBudget = class {
|
|
|
5200
5378
|
|
|
5201
5379
|
// src/capabilities/permissions.ts
|
|
5202
5380
|
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
|
|
5203
|
-
import { join as
|
|
5381
|
+
import { join as join8, resolve as resolve3, sep } from "path";
|
|
5204
5382
|
import { homedir as homedir2 } from "os";
|
|
5205
5383
|
import { parse as parseYaml3, stringify as stringifyYaml3 } from "yaml";
|
|
5206
5384
|
var DEFAULT_MANIFEST = {
|
|
@@ -5230,7 +5408,15 @@ var DEFAULT_MANIFEST = {
|
|
|
5230
5408
|
"init 6",
|
|
5231
5409
|
"kill -9 1",
|
|
5232
5410
|
"> /dev/sda",
|
|
5233
|
-
"mv /* /dev/null"
|
|
5411
|
+
"mv /* /dev/null",
|
|
5412
|
+
"del /s /q C:\\*",
|
|
5413
|
+
"rmdir /s /q C:\\*",
|
|
5414
|
+
"format *",
|
|
5415
|
+
"icacls * C:\\* /grant",
|
|
5416
|
+
"net user *",
|
|
5417
|
+
"netsh *",
|
|
5418
|
+
"reg delete *",
|
|
5419
|
+
"cmd /c rd /s /q *"
|
|
5234
5420
|
],
|
|
5235
5421
|
autoApproved: [
|
|
5236
5422
|
"ls *",
|
|
@@ -5257,7 +5443,15 @@ var DEFAULT_MANIFEST = {
|
|
|
5257
5443
|
"du *",
|
|
5258
5444
|
"uname *",
|
|
5259
5445
|
"curl *",
|
|
5260
|
-
"wget *"
|
|
5446
|
+
"wget *",
|
|
5447
|
+
"dir *",
|
|
5448
|
+
"type *",
|
|
5449
|
+
"cd *",
|
|
5450
|
+
"where *",
|
|
5451
|
+
"tree *",
|
|
5452
|
+
"findstr *",
|
|
5453
|
+
"tasklist *",
|
|
5454
|
+
"systeminfo *"
|
|
5261
5455
|
],
|
|
5262
5456
|
needsApproval: [
|
|
5263
5457
|
"npm publish *",
|
|
@@ -5274,7 +5468,13 @@ var DEFAULT_MANIFEST = {
|
|
|
5274
5468
|
"cp -r *",
|
|
5275
5469
|
"chmod *",
|
|
5276
5470
|
"mkdir *",
|
|
5277
|
-
"rmdir *"
|
|
5471
|
+
"rmdir *",
|
|
5472
|
+
"xcopy *",
|
|
5473
|
+
"robocopy *",
|
|
5474
|
+
"del *",
|
|
5475
|
+
"rd /s *",
|
|
5476
|
+
"powershell *",
|
|
5477
|
+
"cmd /c *"
|
|
5278
5478
|
],
|
|
5279
5479
|
cwdOnly: true
|
|
5280
5480
|
},
|
|
@@ -5285,7 +5485,7 @@ var DEFAULT_MANIFEST = {
|
|
|
5285
5485
|
}
|
|
5286
5486
|
}
|
|
5287
5487
|
};
|
|
5288
|
-
var PERMISSIONS_FILE =
|
|
5488
|
+
var PERMISSIONS_FILE = join8(getMercuryHome(), "permissions.yaml");
|
|
5289
5489
|
var PermissionManager = class {
|
|
5290
5490
|
manifest;
|
|
5291
5491
|
cwd;
|
|
@@ -5694,7 +5894,7 @@ function createCreateFileTool(permissions, getCwd) {
|
|
|
5694
5894
|
import { tool as tool4 } from "ai";
|
|
5695
5895
|
import { z as z4 } from "zod";
|
|
5696
5896
|
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync } from "fs";
|
|
5697
|
-
import { resolve as resolve7, isAbsolute as isAbsolute4 } from "path";
|
|
5897
|
+
import { resolve as resolve7, isAbsolute as isAbsolute4, join as join9 } from "path";
|
|
5698
5898
|
function createListDirTool(permissions, getCwd) {
|
|
5699
5899
|
return tool4({
|
|
5700
5900
|
description: "List the contents of a directory. Shows file names, types, and sizes.",
|
|
@@ -5718,7 +5918,7 @@ function createListDirTool(permissions, getCwd) {
|
|
|
5718
5918
|
const entries = readdirSync2(resolved, { withFileTypes: true });
|
|
5719
5919
|
const lines = entries.map((entry) => {
|
|
5720
5920
|
const isDir = entry.isDirectory();
|
|
5721
|
-
const fullPath =
|
|
5921
|
+
const fullPath = join9(resolved, entry.name);
|
|
5722
5922
|
let size = "";
|
|
5723
5923
|
try {
|
|
5724
5924
|
if (!isDir) {
|
|
@@ -5739,9 +5939,6 @@ ${lines.join("\n")}`;
|
|
|
5739
5939
|
}
|
|
5740
5940
|
});
|
|
5741
5941
|
}
|
|
5742
|
-
function join8(base, name) {
|
|
5743
|
-
return base.endsWith("/") ? base + name : base + "/" + name;
|
|
5744
|
-
}
|
|
5745
5942
|
function formatSize(bytes) {
|
|
5746
5943
|
if (bytes < 1024) return `${bytes}B`;
|
|
5747
5944
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
@@ -5919,6 +6116,7 @@ import { z as z10 } from "zod";
|
|
|
5919
6116
|
import { execSync } from "child_process";
|
|
5920
6117
|
import { resolve as resolve12, isAbsolute as isAbsolute9 } from "path";
|
|
5921
6118
|
import { existsSync as existsSync14 } from "fs";
|
|
6119
|
+
import { homedir as homedir3 } from "os";
|
|
5922
6120
|
function createRunCommandTool(permissions, getCwd, setCwd) {
|
|
5923
6121
|
return tool10({
|
|
5924
6122
|
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.
|
|
@@ -5973,7 +6171,7 @@ function detectCd(command, currentCwd, setCwd) {
|
|
|
5973
6171
|
const trimmed = command.trim();
|
|
5974
6172
|
const cdOnly = trimmed.match(/^cd\s+(.+)$/);
|
|
5975
6173
|
if (cdOnly) {
|
|
5976
|
-
const target = cdOnly[1].replace(/^["']|["']$/g, "").replace(
|
|
6174
|
+
const target = cdOnly[1].replace(/^["']|["']$/g, "").replace(/^~/, homedir3());
|
|
5977
6175
|
const resolved = isAbsolute9(target) ? target : resolve12(currentCwd, target);
|
|
5978
6176
|
if (existsSync14(resolved)) {
|
|
5979
6177
|
setCwd(resolved);
|
|
@@ -5982,7 +6180,7 @@ function detectCd(command, currentCwd, setCwd) {
|
|
|
5982
6180
|
}
|
|
5983
6181
|
const cdChain = trimmed.match(/cd\s+(.+?)\s*&&/);
|
|
5984
6182
|
if (cdChain) {
|
|
5985
|
-
const target = cdChain[1].replace(/^["']|["']$/g, "").replace(
|
|
6183
|
+
const target = cdChain[1].replace(/^["']|["']$/g, "").replace(/^~/, homedir3());
|
|
5986
6184
|
const resolved = isAbsolute9(target) ? target : resolve12(currentCwd, target);
|
|
5987
6185
|
if (existsSync14(resolved)) {
|
|
5988
6186
|
setCwd(resolved);
|
|
@@ -6357,6 +6555,8 @@ function createGitAddTool(getCwd) {
|
|
|
6357
6555
|
import { tool as tool24 } from "ai";
|
|
6358
6556
|
import { z as z24 } from "zod";
|
|
6359
6557
|
import { execSync as execSync6 } from "child_process";
|
|
6558
|
+
import { writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
|
|
6559
|
+
import { join as join10 } from "path";
|
|
6360
6560
|
var CO_AUTHOR = "Mercury <mercury@cosmicstack.org>";
|
|
6361
6561
|
function createGitCommitTool(getCwd) {
|
|
6362
6562
|
return tool24({
|
|
@@ -6369,10 +6569,24 @@ function createGitCommitTool(getCwd) {
|
|
|
6369
6569
|
const fullMessage = `${message}
|
|
6370
6570
|
|
|
6371
6571
|
Co-authored-by: ${CO_AUTHOR}`;
|
|
6372
|
-
const
|
|
6373
|
-
const
|
|
6572
|
+
const cwd = getCwd();
|
|
6573
|
+
const msgFilePath = join10(cwd, ".git", "MERCU_MSG");
|
|
6574
|
+
writeFileSync10(msgFilePath, fullMessage, "utf-8");
|
|
6575
|
+
const result = execSync6(`git commit -F "${msgFilePath}"`, {
|
|
6576
|
+
encoding: "utf-8",
|
|
6577
|
+
timeout: 1e4,
|
|
6578
|
+
cwd
|
|
6579
|
+
});
|
|
6580
|
+
try {
|
|
6581
|
+
unlinkSync3(msgFilePath);
|
|
6582
|
+
} catch {
|
|
6583
|
+
}
|
|
6374
6584
|
return result.trim() || "Committed successfully.";
|
|
6375
6585
|
} catch (err) {
|
|
6586
|
+
try {
|
|
6587
|
+
unlinkSync3(join10(getCwd(), ".git", "MERCU_MSG"));
|
|
6588
|
+
} catch {
|
|
6589
|
+
}
|
|
6376
6590
|
const stderr = err.stderr?.trim() || "";
|
|
6377
6591
|
if (stderr.includes("nothing to commit")) {
|
|
6378
6592
|
return "Nothing to commit \u2014 no staged changes.";
|
|
@@ -6912,8 +7126,8 @@ var CapabilityRegistry = class {
|
|
|
6912
7126
|
};
|
|
6913
7127
|
|
|
6914
7128
|
// src/skills/loader.ts
|
|
6915
|
-
import { existsSync as existsSync16, readFileSync as readFileSync10, readdirSync as readdirSync3, mkdirSync as mkdirSync9, writeFileSync as
|
|
6916
|
-
import { join as
|
|
7129
|
+
import { existsSync as existsSync16, readFileSync as readFileSync10, readdirSync as readdirSync3, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
7130
|
+
import { join as join11 } from "path";
|
|
6917
7131
|
import { parse as parseYaml5 } from "yaml";
|
|
6918
7132
|
var SKILL_FILE = "SKILL.md";
|
|
6919
7133
|
function parseSkillMd(content) {
|
|
@@ -6937,7 +7151,7 @@ var SkillLoader = class {
|
|
|
6937
7151
|
discovered = /* @__PURE__ */ new Map();
|
|
6938
7152
|
loaded = /* @__PURE__ */ new Map();
|
|
6939
7153
|
constructor(skillsDir) {
|
|
6940
|
-
this.skillsDir = skillsDir ||
|
|
7154
|
+
this.skillsDir = skillsDir || join11(getMercuryHome(), "skills");
|
|
6941
7155
|
}
|
|
6942
7156
|
discover() {
|
|
6943
7157
|
this.discovered.clear();
|
|
@@ -6950,7 +7164,7 @@ var SkillLoader = class {
|
|
|
6950
7164
|
const entries = readdirSync3(this.skillsDir, { withFileTypes: true });
|
|
6951
7165
|
for (const entry of entries) {
|
|
6952
7166
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
6953
|
-
const skillPath =
|
|
7167
|
+
const skillPath = join11(this.skillsDir, entry.name, SKILL_FILE);
|
|
6954
7168
|
if (!existsSync16(skillPath)) continue;
|
|
6955
7169
|
try {
|
|
6956
7170
|
const raw = readFileSync10(skillPath, "utf-8");
|
|
@@ -6972,18 +7186,18 @@ var SkillLoader = class {
|
|
|
6972
7186
|
if (cached) return cached;
|
|
6973
7187
|
for (const entry of readdirSync3(this.skillsDir, { withFileTypes: true })) {
|
|
6974
7188
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
6975
|
-
const skillPath =
|
|
7189
|
+
const skillPath = join11(this.skillsDir, entry.name, SKILL_FILE);
|
|
6976
7190
|
if (!existsSync16(skillPath)) continue;
|
|
6977
7191
|
try {
|
|
6978
7192
|
const raw = readFileSync10(skillPath, "utf-8");
|
|
6979
7193
|
const parsed = parseSkillMd(raw);
|
|
6980
7194
|
if (!parsed || parsed.meta.name !== name) continue;
|
|
6981
|
-
const skillDir =
|
|
7195
|
+
const skillDir = join11(this.skillsDir, entry.name);
|
|
6982
7196
|
const skill = {
|
|
6983
7197
|
...parsed.meta,
|
|
6984
7198
|
instructions: parsed.instructions,
|
|
6985
|
-
scriptsDir: existsSync16(
|
|
6986
|
-
referencesDir: existsSync16(
|
|
7199
|
+
scriptsDir: existsSync16(join11(skillDir, "scripts")) ? join11(skillDir, "scripts") : void 0,
|
|
7200
|
+
referencesDir: existsSync16(join11(skillDir, "references")) ? join11(skillDir, "references") : void 0
|
|
6987
7201
|
};
|
|
6988
7202
|
this.loaded.set(name, skill);
|
|
6989
7203
|
return skill;
|
|
@@ -7003,17 +7217,17 @@ var SkillLoader = class {
|
|
|
7003
7217
|
return "Available skills:\n" + skills.map((s) => `- ${s.name}: ${s.description}`).join("\n");
|
|
7004
7218
|
}
|
|
7005
7219
|
saveSkill(name, content) {
|
|
7006
|
-
const skillDir =
|
|
7220
|
+
const skillDir = join11(this.skillsDir, name);
|
|
7007
7221
|
if (!existsSync16(skillDir)) {
|
|
7008
7222
|
mkdirSync9(skillDir, { recursive: true });
|
|
7009
7223
|
}
|
|
7010
|
-
|
|
7224
|
+
writeFileSync11(join11(skillDir, SKILL_FILE), content, "utf-8");
|
|
7011
7225
|
logger.info({ skill: name }, "Skill saved");
|
|
7012
7226
|
this.discover();
|
|
7013
7227
|
return skillDir;
|
|
7014
7228
|
}
|
|
7015
7229
|
seedTemplate() {
|
|
7016
|
-
const templateDir =
|
|
7230
|
+
const templateDir = join11(this.skillsDir, "_template");
|
|
7017
7231
|
mkdirSync9(templateDir, { recursive: true });
|
|
7018
7232
|
const content = `---
|
|
7019
7233
|
name: template-skill
|
|
@@ -7044,7 +7258,7 @@ Describe what this skill enables Mercury to do. When invoked via the use_skill t
|
|
|
7044
7258
|
- List only the tools you need in allowed-tools
|
|
7045
7259
|
- The skill name must be unique among installed skills
|
|
7046
7260
|
`;
|
|
7047
|
-
|
|
7261
|
+
writeFileSync11(join11(templateDir, SKILL_FILE), content, "utf-8");
|
|
7048
7262
|
logger.info("Seeded template skill");
|
|
7049
7263
|
}
|
|
7050
7264
|
};
|
|
@@ -7213,17 +7427,17 @@ function getManual() {
|
|
|
7213
7427
|
|
|
7214
7428
|
// src/cli/daemon.ts
|
|
7215
7429
|
import { spawn } from "child_process";
|
|
7216
|
-
import { existsSync as existsSync17, readFileSync as readFileSync11, writeFileSync as
|
|
7217
|
-
import { join as
|
|
7430
|
+
import { existsSync as existsSync17, readFileSync as readFileSync11, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, mkdirSync as mkdirSync10, openSync } from "fs";
|
|
7431
|
+
import { join as join12 } from "path";
|
|
7218
7432
|
import process2 from "process";
|
|
7219
7433
|
import chalk5 from "chalk";
|
|
7220
7434
|
var PID_FILE = "daemon.pid";
|
|
7221
7435
|
var LOG_FILE = "daemon.log";
|
|
7222
7436
|
function pidPath() {
|
|
7223
|
-
return
|
|
7437
|
+
return join12(getMercuryHome(), PID_FILE);
|
|
7224
7438
|
}
|
|
7225
7439
|
function logPath() {
|
|
7226
|
-
return
|
|
7440
|
+
return join12(getMercuryHome(), LOG_FILE);
|
|
7227
7441
|
}
|
|
7228
7442
|
function readPid() {
|
|
7229
7443
|
const path3 = pidPath();
|
|
@@ -7259,7 +7473,7 @@ function startBackground() {
|
|
|
7259
7473
|
}
|
|
7260
7474
|
if (status.pid && !status.running) {
|
|
7261
7475
|
try {
|
|
7262
|
-
|
|
7476
|
+
unlinkSync4(pidPath());
|
|
7263
7477
|
} catch {
|
|
7264
7478
|
}
|
|
7265
7479
|
}
|
|
@@ -7277,7 +7491,7 @@ function startBackground() {
|
|
|
7277
7491
|
windowsHide: isWin
|
|
7278
7492
|
});
|
|
7279
7493
|
child.unref();
|
|
7280
|
-
|
|
7494
|
+
writeFileSync12(pidPath(), String(child.pid));
|
|
7281
7495
|
console.log("");
|
|
7282
7496
|
console.log(chalk5.green(` Mercury started in background (PID: ${child.pid})`));
|
|
7283
7497
|
console.log(chalk5.dim(` Logs: ${logFile}`));
|
|
@@ -7295,7 +7509,7 @@ function stopDaemon() {
|
|
|
7295
7509
|
if (!status.running) {
|
|
7296
7510
|
console.log(chalk5.yellow(` Stale PID file found (PID: ${status.pid} is not running). Cleaning up.`));
|
|
7297
7511
|
try {
|
|
7298
|
-
|
|
7512
|
+
unlinkSync4(pidPath());
|
|
7299
7513
|
} catch {
|
|
7300
7514
|
}
|
|
7301
7515
|
console.log("");
|
|
@@ -7312,7 +7526,7 @@ function stopDaemon() {
|
|
|
7312
7526
|
console.log(chalk5.red(` Failed to stop PID ${status.pid}. You may need to kill it manually.`));
|
|
7313
7527
|
}
|
|
7314
7528
|
try {
|
|
7315
|
-
|
|
7529
|
+
unlinkSync4(pidPath());
|
|
7316
7530
|
} catch {
|
|
7317
7531
|
}
|
|
7318
7532
|
console.log("");
|
|
@@ -7330,13 +7544,13 @@ function restartDaemon() {
|
|
|
7330
7544
|
} catch {
|
|
7331
7545
|
}
|
|
7332
7546
|
try {
|
|
7333
|
-
|
|
7547
|
+
unlinkSync4(pidPath());
|
|
7334
7548
|
} catch {
|
|
7335
7549
|
}
|
|
7336
7550
|
console.log(chalk5.green(" Mercury stopped."));
|
|
7337
7551
|
} else if (status.pid) {
|
|
7338
7552
|
try {
|
|
7339
|
-
|
|
7553
|
+
unlinkSync4(pidPath());
|
|
7340
7554
|
} catch {
|
|
7341
7555
|
}
|
|
7342
7556
|
}
|
|
@@ -7362,7 +7576,7 @@ function tryAutoDaemonize() {
|
|
|
7362
7576
|
}
|
|
7363
7577
|
if (status.pid && !status.running) {
|
|
7364
7578
|
try {
|
|
7365
|
-
|
|
7579
|
+
unlinkSync4(pidPath());
|
|
7366
7580
|
} catch {
|
|
7367
7581
|
}
|
|
7368
7582
|
}
|
|
@@ -7383,7 +7597,7 @@ function tryAutoDaemonize() {
|
|
|
7383
7597
|
if (!child.pid) {
|
|
7384
7598
|
return false;
|
|
7385
7599
|
}
|
|
7386
|
-
|
|
7600
|
+
writeFileSync12(pidPath(), String(child.pid));
|
|
7387
7601
|
return true;
|
|
7388
7602
|
} catch {
|
|
7389
7603
|
return false;
|
|
@@ -7391,9 +7605,9 @@ function tryAutoDaemonize() {
|
|
|
7391
7605
|
}
|
|
7392
7606
|
|
|
7393
7607
|
// src/cli/service.ts
|
|
7394
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync11, writeFileSync as
|
|
7395
|
-
import { join as
|
|
7396
|
-
import { homedir as
|
|
7608
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync11, writeFileSync as writeFileSync13, unlinkSync as unlinkSync5 } from "fs";
|
|
7609
|
+
import { join as join13 } from "path";
|
|
7610
|
+
import { homedir as homedir4 } from "os";
|
|
7397
7611
|
import chalk6 from "chalk";
|
|
7398
7612
|
import { execSync as execSync8 } from "child_process";
|
|
7399
7613
|
var SERVICE_DESC = "Mercury \u2014 Soul-Driven AI Agent";
|
|
@@ -7401,9 +7615,9 @@ var WIN_TASK_NAME = "MercuryAgent";
|
|
|
7401
7615
|
function isServiceInstalled() {
|
|
7402
7616
|
const platform = process.platform;
|
|
7403
7617
|
if (platform === "darwin") {
|
|
7404
|
-
return existsSync18(
|
|
7618
|
+
return existsSync18(join13(homedir4(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist"));
|
|
7405
7619
|
} else if (platform === "linux") {
|
|
7406
|
-
return existsSync18(
|
|
7620
|
+
return existsSync18(join13(homedir4(), ".config", "systemd", "user", "mercury.service"));
|
|
7407
7621
|
} else if (platform === "win32") {
|
|
7408
7622
|
try {
|
|
7409
7623
|
execSync8(`schtasks /query /tn "${WIN_TASK_NAME}"`, { stdio: "pipe", shell: "cmd.exe" });
|
|
@@ -7418,7 +7632,10 @@ function getNodeBinPath() {
|
|
|
7418
7632
|
return process.execPath;
|
|
7419
7633
|
}
|
|
7420
7634
|
function getDistPath() {
|
|
7421
|
-
|
|
7635
|
+
if (!process.argv[1]) {
|
|
7636
|
+
return join13(homedir4(), ".nvm", "versions", "node", `v${process.version.slice(1)}`, "lib", "node_modules", "@cosmicstack", "mercury-agent", "dist", "index.js");
|
|
7637
|
+
}
|
|
7638
|
+
return join13(process.argv[1], "..", "..", "lib", "node_modules", "@cosmicstack", "mercury-agent", "dist", "index.js");
|
|
7422
7639
|
}
|
|
7423
7640
|
function installService() {
|
|
7424
7641
|
const platform = process.platform;
|
|
@@ -7457,16 +7674,16 @@ function showServiceStatus() {
|
|
|
7457
7674
|
}
|
|
7458
7675
|
}
|
|
7459
7676
|
function installMac() {
|
|
7460
|
-
const plistDir =
|
|
7461
|
-
const plistPath =
|
|
7677
|
+
const plistDir = join13(homedir4(), "Library", "LaunchAgents");
|
|
7678
|
+
const plistPath = join13(plistDir, "com.cosmicstack.mercury.plist");
|
|
7462
7679
|
if (!existsSync18(plistDir)) {
|
|
7463
7680
|
mkdirSync11(plistDir, { recursive: true });
|
|
7464
7681
|
}
|
|
7465
7682
|
const nodeBin = getNodeBinPath();
|
|
7466
7683
|
const scriptPath = getDistPath();
|
|
7467
7684
|
const home = getMercuryHome();
|
|
7468
|
-
const logPath2 =
|
|
7469
|
-
const errPath =
|
|
7685
|
+
const logPath2 = join13(home, "daemon.log");
|
|
7686
|
+
const errPath = join13(home, "daemon-error.log");
|
|
7470
7687
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
7471
7688
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
7472
7689
|
<plist version="1.0">
|
|
@@ -7496,13 +7713,13 @@ function installMac() {
|
|
|
7496
7713
|
<key>PATH</key>
|
|
7497
7714
|
<string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
|
|
7498
7715
|
<key>HOME</key>
|
|
7499
|
-
<string>${
|
|
7716
|
+
<string>${homedir4()}</string>
|
|
7500
7717
|
</dict>
|
|
7501
7718
|
<key>WorkingDirectory</key>
|
|
7502
|
-
<string>${
|
|
7719
|
+
<string>${homedir4()}</string>
|
|
7503
7720
|
</dict>
|
|
7504
7721
|
</plist>`;
|
|
7505
|
-
|
|
7722
|
+
writeFileSync13(plistPath, plist, "utf-8");
|
|
7506
7723
|
try {
|
|
7507
7724
|
execSync8(`launchctl load ${plistPath}`, { stdio: "inherit" });
|
|
7508
7725
|
} catch {
|
|
@@ -7519,7 +7736,7 @@ function installMac() {
|
|
|
7519
7736
|
console.log("");
|
|
7520
7737
|
}
|
|
7521
7738
|
function uninstallMac() {
|
|
7522
|
-
const plistPath =
|
|
7739
|
+
const plistPath = join13(homedir4(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
7523
7740
|
if (!existsSync18(plistPath)) {
|
|
7524
7741
|
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
7525
7742
|
console.log("");
|
|
@@ -7530,7 +7747,7 @@ function uninstallMac() {
|
|
|
7530
7747
|
} catch {
|
|
7531
7748
|
}
|
|
7532
7749
|
try {
|
|
7533
|
-
|
|
7750
|
+
unlinkSync5(plistPath);
|
|
7534
7751
|
} catch {
|
|
7535
7752
|
console.log(chalk6.yellow(" Failed to remove plist file. Remove manually:"));
|
|
7536
7753
|
console.log(chalk6.dim(` rm ${plistPath}`));
|
|
@@ -7540,7 +7757,7 @@ function uninstallMac() {
|
|
|
7540
7757
|
console.log("");
|
|
7541
7758
|
}
|
|
7542
7759
|
function showMacStatus() {
|
|
7543
|
-
const plistPath =
|
|
7760
|
+
const plistPath = join13(homedir4(), "Library", "LaunchAgents", "com.cosmicstack.mercury.plist");
|
|
7544
7761
|
if (!existsSync18(plistPath)) {
|
|
7545
7762
|
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
7546
7763
|
console.log(chalk6.dim(" Run `mercury service install` to set it up."));
|
|
@@ -7558,11 +7775,11 @@ function showMacStatus() {
|
|
|
7558
7775
|
console.log("");
|
|
7559
7776
|
}
|
|
7560
7777
|
function installLinux() {
|
|
7561
|
-
const systemdDir =
|
|
7778
|
+
const systemdDir = join13(homedir4(), ".config", "systemd", "user");
|
|
7562
7779
|
if (!existsSync18(systemdDir)) {
|
|
7563
7780
|
mkdirSync11(systemdDir, { recursive: true });
|
|
7564
7781
|
}
|
|
7565
|
-
const servicePath =
|
|
7782
|
+
const servicePath = join13(systemdDir, "mercury.service");
|
|
7566
7783
|
const nodeBin = getNodeBinPath();
|
|
7567
7784
|
const scriptPath = getDistPath();
|
|
7568
7785
|
const home = getMercuryHome();
|
|
@@ -7576,14 +7793,14 @@ ExecStart=${nodeBin} ${scriptPath} start --daemon
|
|
|
7576
7793
|
Restart=on-failure
|
|
7577
7794
|
RestartSec=5
|
|
7578
7795
|
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
7579
|
-
Environment=HOME=${
|
|
7580
|
-
WorkingDirectory=${
|
|
7581
|
-
StandardOutput=append:${
|
|
7582
|
-
StandardError=append:${
|
|
7796
|
+
Environment=HOME=${homedir4()}
|
|
7797
|
+
WorkingDirectory=${homedir4()}
|
|
7798
|
+
StandardOutput=append:${join13(home, "daemon.log")}
|
|
7799
|
+
StandardError=append:${join13(home, "daemon-error.log")}
|
|
7583
7800
|
|
|
7584
7801
|
[Install]
|
|
7585
7802
|
WantedBy=default.target`;
|
|
7586
|
-
|
|
7803
|
+
writeFileSync13(servicePath, service, "utf-8");
|
|
7587
7804
|
try {
|
|
7588
7805
|
execSync8("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
7589
7806
|
execSync8("systemctl --user enable mercury.service", { stdio: "inherit" });
|
|
@@ -7603,14 +7820,14 @@ WantedBy=default.target`;
|
|
|
7603
7820
|
console.log("");
|
|
7604
7821
|
console.log(chalk6.green(" Mercury service installed (systemd --user)"));
|
|
7605
7822
|
console.log(chalk6.dim(` Service: ${servicePath}`));
|
|
7606
|
-
console.log(chalk6.dim(` Logs: ${
|
|
7823
|
+
console.log(chalk6.dim(` Logs: ${join13(home, "daemon.log")}`));
|
|
7607
7824
|
console.log(chalk6.dim(" Auto-starts on login. Auto-restarts on crash (5s delay)."));
|
|
7608
7825
|
console.log("");
|
|
7609
7826
|
console.log(chalk6.dim(" Uninstall: mercury service uninstall"));
|
|
7610
7827
|
console.log("");
|
|
7611
7828
|
}
|
|
7612
7829
|
function uninstallLinux() {
|
|
7613
|
-
const servicePath =
|
|
7830
|
+
const servicePath = join13(homedir4(), ".config", "systemd", "user", "mercury.service");
|
|
7614
7831
|
if (!existsSync18(servicePath)) {
|
|
7615
7832
|
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
7616
7833
|
console.log("");
|
|
@@ -7622,7 +7839,7 @@ function uninstallLinux() {
|
|
|
7622
7839
|
} catch {
|
|
7623
7840
|
}
|
|
7624
7841
|
try {
|
|
7625
|
-
|
|
7842
|
+
unlinkSync5(servicePath);
|
|
7626
7843
|
} catch {
|
|
7627
7844
|
console.log(chalk6.yellow(" Failed to remove service file. Remove manually:"));
|
|
7628
7845
|
console.log(chalk6.dim(` rm ${servicePath}`));
|
|
@@ -7636,7 +7853,7 @@ function uninstallLinux() {
|
|
|
7636
7853
|
console.log("");
|
|
7637
7854
|
}
|
|
7638
7855
|
function showLinuxStatus() {
|
|
7639
|
-
const servicePath =
|
|
7856
|
+
const servicePath = join13(homedir4(), ".config", "systemd", "user", "mercury.service");
|
|
7640
7857
|
if (!existsSync18(servicePath)) {
|
|
7641
7858
|
console.log(chalk6.yellow(" Mercury service is not installed."));
|
|
7642
7859
|
console.log(chalk6.dim(" Run `mercury service install` to set it up."));
|
|
@@ -7656,7 +7873,7 @@ function installWindows() {
|
|
|
7656
7873
|
const nodeBin = getNodeBinPath();
|
|
7657
7874
|
const scriptPath = getDistPath();
|
|
7658
7875
|
const home = getMercuryHome();
|
|
7659
|
-
const logPath2 =
|
|
7876
|
+
const logPath2 = join13(home, "daemon.log");
|
|
7660
7877
|
const cmd = `"${nodeBin}" "${scriptPath}" start --daemon`;
|
|
7661
7878
|
try {
|
|
7662
7879
|
execSync8(
|
|
@@ -7938,7 +8155,7 @@ async function fetchProviderModelCatalog(provider, config) {
|
|
|
7938
8155
|
|
|
7939
8156
|
// src/index.ts
|
|
7940
8157
|
var __dirname = dirname4(fileURLToPath(import.meta.url));
|
|
7941
|
-
var pkgVersion = JSON.parse(readFileSync12(
|
|
8158
|
+
var pkgVersion = JSON.parse(readFileSync12(join14(__dirname, "..", "package.json"), "utf8")).version;
|
|
7942
8159
|
function hr() {
|
|
7943
8160
|
console.log(chalk7.dim("\u2500".repeat(50)));
|
|
7944
8161
|
}
|
|
@@ -8223,14 +8440,14 @@ async function promptValidatedValue(prompt, validator, existingValue, options) {
|
|
|
8223
8440
|
}
|
|
8224
8441
|
}
|
|
8225
8442
|
function appendToEnv(key, value) {
|
|
8226
|
-
const envPath =
|
|
8443
|
+
const envPath = join14(getMercuryHome(), ".env");
|
|
8227
8444
|
let envContent = "";
|
|
8228
8445
|
if (existsSync19(envPath)) {
|
|
8229
8446
|
envContent = readFileSync12(envPath, "utf-8");
|
|
8230
8447
|
}
|
|
8231
8448
|
const lines = envContent.split("\n").filter((l) => !l.startsWith(`${key}=`) && l.trim() !== "");
|
|
8232
8449
|
lines.push(`${key}=${value}`);
|
|
8233
|
-
|
|
8450
|
+
writeFileSync14(envPath, lines.join("\n") + "\n", "utf-8");
|
|
8234
8451
|
process.env[key] = value;
|
|
8235
8452
|
}
|
|
8236
8453
|
function parseGithubRepo(input) {
|
|
@@ -8618,7 +8835,7 @@ async function runAgent(isDaemon = false) {
|
|
|
8618
8835
|
const longTerm = new LongTermMemory(config);
|
|
8619
8836
|
const episodic = new EpisodicMemory(config);
|
|
8620
8837
|
let userMemory = null;
|
|
8621
|
-
if (config.memory.secondBrain?.enabled !== false) {
|
|
8838
|
+
if (config.memory.secondBrain?.enabled !== false && isBetterSqlite3Available()) {
|
|
8622
8839
|
try {
|
|
8623
8840
|
userMemory = new UserMemoryStore(config);
|
|
8624
8841
|
if (!isDaemon) {
|
|
@@ -8630,6 +8847,10 @@ async function runAgent(isDaemon = false) {
|
|
|
8630
8847
|
logger.warn({ err }, "Second brain initialization failed, continuing without it");
|
|
8631
8848
|
userMemory = null;
|
|
8632
8849
|
}
|
|
8850
|
+
} else if (config.memory.secondBrain?.enabled !== false && !isBetterSqlite3Available()) {
|
|
8851
|
+
logger.warn(
|
|
8852
|
+
"better-sqlite3 is not available \u2014 second brain memory is disabled. To enable it, install build tools (make, gcc/g++, python3) and ensure Node >= 20, then reinstall."
|
|
8853
|
+
);
|
|
8633
8854
|
}
|
|
8634
8855
|
const channels = new ChannelRegistry(config);
|
|
8635
8856
|
const capabilities = new CapabilityRegistry(skillLoader, scheduler, tokenBudget);
|
|
@@ -9046,10 +9267,10 @@ program.command("upgrade").description("Upgrade Mercury to the latest version fr
|
|
|
9046
9267
|
} catch {
|
|
9047
9268
|
try {
|
|
9048
9269
|
const globalDir = execSync9("npm root -g", { encoding: "utf-8" }).trim();
|
|
9049
|
-
const pkgDir =
|
|
9050
|
-
const { rmSync:
|
|
9270
|
+
const pkgDir = join14(globalDir, "@cosmicstack", "mercury-agent");
|
|
9271
|
+
const { rmSync: rmSync3 } = await import("fs");
|
|
9051
9272
|
try {
|
|
9052
|
-
|
|
9273
|
+
rmSync3(pkgDir, { recursive: true, force: true });
|
|
9053
9274
|
} catch {
|
|
9054
9275
|
}
|
|
9055
9276
|
} catch {
|