@plumpslabs/kuma 2.0.5 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -15
- package/dist/index.js +1696 -243
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
|
+
import * as readline from "readline";
|
|
5
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
8
|
|
|
@@ -899,7 +900,11 @@ async function handleSmartFilePicker(params) {
|
|
|
899
900
|
const content = fs4.readFileSync(resolvedPath, "utf-8");
|
|
900
901
|
const lines = content.split("\n");
|
|
901
902
|
const totalLines = lines.length;
|
|
902
|
-
sessionMemory.recordToolCall("smart_file_picker", {
|
|
903
|
+
sessionMemory.recordToolCall("smart_file_picker", {
|
|
904
|
+
filePath,
|
|
905
|
+
chunkStrategy,
|
|
906
|
+
totalLines
|
|
907
|
+
});
|
|
903
908
|
if (startLine !== void 0 || endLine !== void 0) {
|
|
904
909
|
const start = startLine ?? 1;
|
|
905
910
|
const end = endLine ?? totalLines;
|
|
@@ -915,7 +920,13 @@ async function handleSmartFilePicker(params) {
|
|
|
915
920
|
case "smart":
|
|
916
921
|
return handleSmartStrategy(filePath, lines, totalLines);
|
|
917
922
|
default:
|
|
918
|
-
return formatOutput(
|
|
923
|
+
return formatOutput(
|
|
924
|
+
filePath,
|
|
925
|
+
lines.slice(0, CHUNK_THRESHOLD),
|
|
926
|
+
1,
|
|
927
|
+
totalLines,
|
|
928
|
+
true
|
|
929
|
+
);
|
|
919
930
|
}
|
|
920
931
|
} catch (err) {
|
|
921
932
|
return 'Error reading file "' + filePath + '": ' + (err instanceof Error ? err.message : String(err));
|
|
@@ -940,7 +951,8 @@ async function handleOutlineStrategy(filePath, lines, totalLines) {
|
|
|
940
951
|
const exportLines = [];
|
|
941
952
|
for (let i = 0; i < lines.length; i++) {
|
|
942
953
|
const line = lines[i].trim();
|
|
943
|
-
if (line.startsWith("//") || line.startsWith("#") || line.startsWith("/*") || line.startsWith("*"))
|
|
954
|
+
if (line.startsWith("//") || line.startsWith("#") || line.startsWith("/*") || line.startsWith("*"))
|
|
955
|
+
continue;
|
|
944
956
|
if (line.startsWith("import ") || line.startsWith("from ") || line.startsWith("require(")) {
|
|
945
957
|
importLines.push(line);
|
|
946
958
|
continue;
|
|
@@ -974,10 +986,15 @@ async function handleSmartStrategy(filePath, lines, totalLines) {
|
|
|
974
986
|
}
|
|
975
987
|
if (headerEnd < tailStart) {
|
|
976
988
|
smartLines.push({ line: -1, text: "" });
|
|
977
|
-
smartLines.push({
|
|
989
|
+
smartLines.push({
|
|
990
|
+
line: -1,
|
|
991
|
+
text: " ... " + (tailStart - headerEnd) + " lines hidden ..."
|
|
992
|
+
});
|
|
978
993
|
smartLines.push({ line: -1, text: "" });
|
|
979
994
|
}
|
|
980
|
-
const keyDeclarations = extractKeyDeclarations(
|
|
995
|
+
const keyDeclarations = extractKeyDeclarations(
|
|
996
|
+
lines.slice(headerEnd, tailStart)
|
|
997
|
+
);
|
|
981
998
|
for (const decl of keyDeclarations) {
|
|
982
999
|
smartLines.push({ line: decl.line + 1 + headerEnd, text: decl.text });
|
|
983
1000
|
}
|
|
@@ -1020,7 +1037,9 @@ function extractKeyDeclarations(lines) {
|
|
|
1020
1037
|
for (let i = 0; i < lines.length; i++) {
|
|
1021
1038
|
const line = lines[i].trim();
|
|
1022
1039
|
if (!line || line.startsWith("//") || line.startsWith("#")) continue;
|
|
1023
|
-
if (/^(export\s+)?(async\s+)?(function|class|interface|type|enum|const|let|var|def)\s/.test(
|
|
1040
|
+
if (/^(export\s+)?(async\s+)?(function|class|interface|type|enum|const|let|var|def)\s/.test(
|
|
1041
|
+
line
|
|
1042
|
+
)) {
|
|
1024
1043
|
decls.push({ line: i, text: line.substring(0, 150) });
|
|
1025
1044
|
}
|
|
1026
1045
|
}
|
|
@@ -1575,8 +1594,80 @@ function formatBatchResult(results, totalRequested) {
|
|
|
1575
1594
|
return lines.join("\n");
|
|
1576
1595
|
}
|
|
1577
1596
|
|
|
1578
|
-
// src/
|
|
1597
|
+
// src/utils/processRunner.ts
|
|
1579
1598
|
import { spawn } from "child_process";
|
|
1599
|
+
var DEFAULT_MAX_STDOUT = 5e3;
|
|
1600
|
+
var DEFAULT_MAX_STDERR = 2e3;
|
|
1601
|
+
var DEFAULT_TIMEOUT = 60;
|
|
1602
|
+
function spawnProcess(command, options) {
|
|
1603
|
+
const {
|
|
1604
|
+
cwd,
|
|
1605
|
+
timeoutSeconds = DEFAULT_TIMEOUT,
|
|
1606
|
+
useShell = false,
|
|
1607
|
+
maxStdout = DEFAULT_MAX_STDOUT,
|
|
1608
|
+
maxStderr = DEFAULT_MAX_STDERR
|
|
1609
|
+
} = options;
|
|
1610
|
+
return new Promise((resolve) => {
|
|
1611
|
+
const parts = command.split(" ");
|
|
1612
|
+
const cmd = parts[0];
|
|
1613
|
+
const args = parts.slice(1);
|
|
1614
|
+
const proc = spawn(cmd, args, {
|
|
1615
|
+
cwd,
|
|
1616
|
+
shell: useShell,
|
|
1617
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1618
|
+
});
|
|
1619
|
+
let stdout = "";
|
|
1620
|
+
let stderr = "";
|
|
1621
|
+
let timedOut = false;
|
|
1622
|
+
const timeoutId = setTimeout(() => {
|
|
1623
|
+
timedOut = true;
|
|
1624
|
+
proc.kill("SIGTERM");
|
|
1625
|
+
if (process.platform === "win32") {
|
|
1626
|
+
try {
|
|
1627
|
+
spawn("taskkill", ["/pid", String(proc.pid), "/f", "/t"], {
|
|
1628
|
+
stdio: "ignore"
|
|
1629
|
+
});
|
|
1630
|
+
} catch {
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}, timeoutSeconds * 1e3);
|
|
1634
|
+
proc.stdout?.on("data", (data) => {
|
|
1635
|
+
stdout += data.toString();
|
|
1636
|
+
});
|
|
1637
|
+
proc.stderr?.on("data", (data) => {
|
|
1638
|
+
stderr += data.toString();
|
|
1639
|
+
});
|
|
1640
|
+
proc.on("close", (code) => {
|
|
1641
|
+
clearTimeout(timeoutId);
|
|
1642
|
+
resolve({
|
|
1643
|
+
stdout: truncateOutput(stdout, maxStdout),
|
|
1644
|
+
stderr: truncateOutput(stderr, maxStderr),
|
|
1645
|
+
exitCode: code ?? -1,
|
|
1646
|
+
timedOut
|
|
1647
|
+
});
|
|
1648
|
+
});
|
|
1649
|
+
proc.on("error", () => {
|
|
1650
|
+
clearTimeout(timeoutId);
|
|
1651
|
+
resolve({
|
|
1652
|
+
stdout,
|
|
1653
|
+
stderr: `Failed to spawn process: ${cmd}`,
|
|
1654
|
+
exitCode: -1,
|
|
1655
|
+
timedOut: false
|
|
1656
|
+
});
|
|
1657
|
+
});
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
function spawnShell(command, options) {
|
|
1661
|
+
return spawnProcess(command, { ...options, useShell: true });
|
|
1662
|
+
}
|
|
1663
|
+
function truncateOutput(output, maxChars) {
|
|
1664
|
+
if (output.length <= maxChars) return output;
|
|
1665
|
+
return output.slice(0, maxChars) + `
|
|
1666
|
+
|
|
1667
|
+
[...truncated, ${output.length - maxChars} more characters]`;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// src/tools/safeTerminalExec.ts
|
|
1580
1671
|
var TASK_COMMANDS = {
|
|
1581
1672
|
test: "npm test",
|
|
1582
1673
|
build: "npm run build",
|
|
@@ -1600,7 +1691,6 @@ var DANGEROUS_PATTERNS = [
|
|
|
1600
1691
|
"mkfs",
|
|
1601
1692
|
"dd if=",
|
|
1602
1693
|
":(){ :|:& };:",
|
|
1603
|
-
// fork bomb
|
|
1604
1694
|
"curl ",
|
|
1605
1695
|
"wget "
|
|
1606
1696
|
];
|
|
@@ -1609,12 +1699,7 @@ async function handleSafeTerminalExec(params) {
|
|
|
1609
1699
|
if (task === "custom" && !customCommand) {
|
|
1610
1700
|
return "Error: Task 'custom' requires the 'customCommand' parameter.";
|
|
1611
1701
|
}
|
|
1612
|
-
|
|
1613
|
-
if (task === "custom") {
|
|
1614
|
-
command = customCommand;
|
|
1615
|
-
} else {
|
|
1616
|
-
command = TASK_COMMANDS[task];
|
|
1617
|
-
}
|
|
1702
|
+
const command = task === "custom" ? customCommand : TASK_COMMANDS[task];
|
|
1618
1703
|
const cbResult = circuitBreaker.check("safe_terminal_exec", { task, command });
|
|
1619
1704
|
if (!cbResult.allowed) {
|
|
1620
1705
|
return `\u26A0\uFE0F Circuit breaker: ${cbResult.reason}
|
|
@@ -1629,7 +1714,10 @@ This command is not permitted.`;
|
|
|
1629
1714
|
const projectRoot = getProjectRoot();
|
|
1630
1715
|
try {
|
|
1631
1716
|
sessionMemory.recordToolCall("execute_safe_test", { task, command });
|
|
1632
|
-
const result = await
|
|
1717
|
+
const result = await spawnShell(command, {
|
|
1718
|
+
cwd: projectRoot,
|
|
1719
|
+
timeoutSeconds: timeout
|
|
1720
|
+
});
|
|
1633
1721
|
const output = formatExecResult(result, command, task);
|
|
1634
1722
|
if (result.exitCode !== 0) {
|
|
1635
1723
|
sessionMemory.addFailedFile(task, result.stderr || result.stdout);
|
|
@@ -1637,65 +1725,12 @@ This command is not permitted.`;
|
|
|
1637
1725
|
return output;
|
|
1638
1726
|
} catch (err) {
|
|
1639
1727
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1640
|
-
|
|
1641
|
-
if (isTimeout) {
|
|
1728
|
+
if (errorMsg.toLowerCase().includes("timeout")) {
|
|
1642
1729
|
return formatTimeoutResult(command, timeout);
|
|
1643
1730
|
}
|
|
1644
1731
|
return `Error running "${command}": ${errorMsg}`;
|
|
1645
1732
|
}
|
|
1646
1733
|
}
|
|
1647
|
-
async function executeWithTimeout(command, cwd, timeoutSeconds) {
|
|
1648
|
-
return new Promise((resolve, reject) => {
|
|
1649
|
-
const parts = command.split(" ");
|
|
1650
|
-
const cmd = parts[0];
|
|
1651
|
-
const args = parts.slice(1);
|
|
1652
|
-
const proc = spawn(cmd, args, {
|
|
1653
|
-
cwd,
|
|
1654
|
-
shell: process.platform === "win32",
|
|
1655
|
-
// Use shell on Windows
|
|
1656
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1657
|
-
timeout: timeoutSeconds * 1e3
|
|
1658
|
-
});
|
|
1659
|
-
let stdout = "";
|
|
1660
|
-
let stderr = "";
|
|
1661
|
-
let timedOut = false;
|
|
1662
|
-
const timeoutId = setTimeout(() => {
|
|
1663
|
-
timedOut = true;
|
|
1664
|
-
proc.kill("SIGTERM");
|
|
1665
|
-
if (process.platform === "win32") {
|
|
1666
|
-
try {
|
|
1667
|
-
spawn("taskkill", ["/pid", String(proc.pid), "/f", "/t"], { stdio: "ignore" });
|
|
1668
|
-
} catch {
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
}, timeoutSeconds * 1e3);
|
|
1672
|
-
proc.stdout?.on("data", (data) => {
|
|
1673
|
-
stdout += data.toString();
|
|
1674
|
-
});
|
|
1675
|
-
proc.stderr?.on("data", (data) => {
|
|
1676
|
-
stderr += data.toString();
|
|
1677
|
-
});
|
|
1678
|
-
proc.on("close", (code) => {
|
|
1679
|
-
clearTimeout(timeoutId);
|
|
1680
|
-
resolve({
|
|
1681
|
-
stdout: truncateOutput(stdout, 5e3),
|
|
1682
|
-
stderr: truncateOutput(stderr, 2e3),
|
|
1683
|
-
exitCode: code ?? -1,
|
|
1684
|
-
timedOut
|
|
1685
|
-
});
|
|
1686
|
-
});
|
|
1687
|
-
proc.on("error", (err) => {
|
|
1688
|
-
clearTimeout(timeoutId);
|
|
1689
|
-
reject(err);
|
|
1690
|
-
});
|
|
1691
|
-
});
|
|
1692
|
-
}
|
|
1693
|
-
function truncateOutput(output, maxChars) {
|
|
1694
|
-
if (output.length <= maxChars) return output;
|
|
1695
|
-
return output.slice(0, maxChars) + `
|
|
1696
|
-
|
|
1697
|
-
[...truncated, ${output.length - maxChars} more characters]`;
|
|
1698
|
-
}
|
|
1699
1734
|
function formatExecResult(result, command, task) {
|
|
1700
1735
|
const status = result.exitCode === 0 ? "\u2705 PASS" : "\u274C FAIL";
|
|
1701
1736
|
const lines = [
|
|
@@ -2868,20 +2903,26 @@ function formatConventionsOutput(conventions) {
|
|
|
2868
2903
|
return lines.join("\n");
|
|
2869
2904
|
}
|
|
2870
2905
|
|
|
2906
|
+
// src/utils/gitUtils.ts
|
|
2907
|
+
import { execSync as execSync2 } from "child_process";
|
|
2908
|
+
function runGitCommand(command) {
|
|
2909
|
+
const root = getProjectRoot();
|
|
2910
|
+
return execSync2(command, {
|
|
2911
|
+
cwd: root,
|
|
2912
|
+
encoding: "utf-8",
|
|
2913
|
+
maxBuffer: 2 * 1024 * 1024
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2871
2917
|
// src/tools/gitLog.ts
|
|
2872
|
-
import child_process from "child_process";
|
|
2873
2918
|
async function handleGitLog(params) {
|
|
2874
2919
|
const { maxCount = 10, filePath } = params;
|
|
2875
|
-
const root = getProjectRoot();
|
|
2876
2920
|
try {
|
|
2877
2921
|
let command = `git log -n ${maxCount} --oneline`;
|
|
2878
2922
|
if (filePath) {
|
|
2879
2923
|
command += ` -- "${filePath}"`;
|
|
2880
2924
|
}
|
|
2881
|
-
const stdout =
|
|
2882
|
-
cwd: root,
|
|
2883
|
-
encoding: "utf-8"
|
|
2884
|
-
});
|
|
2925
|
+
const stdout = runGitCommand(command);
|
|
2885
2926
|
sessionMemory.recordToolCall("git_log", { maxCount, filePath });
|
|
2886
2927
|
if (!stdout.trim()) {
|
|
2887
2928
|
return `\u2139\uFE0F No commit history found${filePath ? ` for file "${filePath}"` : ""}.`;
|
|
@@ -2895,10 +2936,8 @@ ${stdout}`;
|
|
|
2895
2936
|
}
|
|
2896
2937
|
|
|
2897
2938
|
// src/tools/gitDiff.ts
|
|
2898
|
-
import child_process2 from "child_process";
|
|
2899
2939
|
async function handleGitDiff(params) {
|
|
2900
2940
|
const { filePath, staged = false, contextLines = 3, baseRef, targetRef } = params;
|
|
2901
|
-
const root = getProjectRoot();
|
|
2902
2941
|
try {
|
|
2903
2942
|
let command = "git diff";
|
|
2904
2943
|
if (staged) {
|
|
@@ -2914,11 +2953,7 @@ async function handleGitDiff(params) {
|
|
|
2914
2953
|
if (filePath) {
|
|
2915
2954
|
command += ' -- "' + filePath + '"';
|
|
2916
2955
|
}
|
|
2917
|
-
const stdout =
|
|
2918
|
-
cwd: root,
|
|
2919
|
-
encoding: "utf-8",
|
|
2920
|
-
maxBuffer: 2 * 1024 * 1024
|
|
2921
|
-
});
|
|
2956
|
+
const stdout = runGitCommand(command);
|
|
2922
2957
|
sessionMemory.recordToolCall("git_diff", { filePath, staged, baseRef, targetRef });
|
|
2923
2958
|
if (!stdout.trim()) {
|
|
2924
2959
|
if (staged) {
|
|
@@ -3018,6 +3053,8 @@ var DEFAULT_IGNORE = [
|
|
|
3018
3053
|
".DS_Store",
|
|
3019
3054
|
"*.log"
|
|
3020
3055
|
];
|
|
3056
|
+
var treeCache = /* @__PURE__ */ new Map();
|
|
3057
|
+
var CACHE_TTL_MS2 = 6e4;
|
|
3021
3058
|
async function handleProjectStructure(params) {
|
|
3022
3059
|
const {
|
|
3023
3060
|
depth = 3,
|
|
@@ -3027,6 +3064,12 @@ async function handleProjectStructure(params) {
|
|
|
3027
3064
|
} = params;
|
|
3028
3065
|
const root = getProjectRoot();
|
|
3029
3066
|
const clampedDepth = Math.max(1, Math.min(depth, 6));
|
|
3067
|
+
const cacheKey = `${root}:${clampedDepth}:${folderOnly}:${includePattern ?? ""}:${excludePattern ?? ""}`;
|
|
3068
|
+
const cached = treeCache.get(cacheKey);
|
|
3069
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS2) {
|
|
3070
|
+
sessionMemory.recordToolCall("project_structure", { depth: clampedDepth, folderOnly, cached: true });
|
|
3071
|
+
return cached.result;
|
|
3072
|
+
}
|
|
3030
3073
|
sessionMemory.recordToolCall("project_structure", { depth: clampedDepth, folderOnly });
|
|
3031
3074
|
try {
|
|
3032
3075
|
const tree = buildTree(root, root, clampedDepth, 0, folderOnly, includePattern, excludePattern);
|
|
@@ -3041,7 +3084,9 @@ async function handleProjectStructure(params) {
|
|
|
3041
3084
|
"Increase depth (max 6) for deeper structure.",
|
|
3042
3085
|
"Use folderOnly: true for high-level overview."
|
|
3043
3086
|
];
|
|
3044
|
-
|
|
3087
|
+
const result = lines.join("\n");
|
|
3088
|
+
treeCache.set(cacheKey, { result, timestamp: Date.now() });
|
|
3089
|
+
return result;
|
|
3045
3090
|
} catch (err) {
|
|
3046
3091
|
return "Error building project structure: " + (err instanceof Error ? err.message : String(err));
|
|
3047
3092
|
}
|
|
@@ -3122,7 +3167,6 @@ function getFileSize(fullPath) {
|
|
|
3122
3167
|
}
|
|
3123
3168
|
|
|
3124
3169
|
// src/tools/staticAnalysis.ts
|
|
3125
|
-
import { spawn as spawn2 } from "child_process";
|
|
3126
3170
|
import fs10 from "fs";
|
|
3127
3171
|
import path10 from "path";
|
|
3128
3172
|
async function handleStaticAnalysis(params) {
|
|
@@ -3216,45 +3260,14 @@ function readPackageJson(root) {
|
|
|
3216
3260
|
async function runTool(tool, cwd, files, autoFix, timeoutSeconds) {
|
|
3217
3261
|
const cmd = buildToolCommand(tool, files, autoFix);
|
|
3218
3262
|
if (!cmd) return null;
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
shell: process.platform === "win32",
|
|
3226
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
3227
|
-
});
|
|
3228
|
-
let stdout = "";
|
|
3229
|
-
let stderr = "";
|
|
3230
|
-
const timeoutId = setTimeout(() => {
|
|
3231
|
-
proc.kill("SIGTERM");
|
|
3232
|
-
if (process.platform === "win32") {
|
|
3233
|
-
try {
|
|
3234
|
-
spawn2("taskkill", ["/pid", String(proc.pid), "/f", "/t"], { stdio: "ignore" });
|
|
3235
|
-
} catch {
|
|
3236
|
-
}
|
|
3237
|
-
}
|
|
3238
|
-
}, (timeoutSeconds ?? 60) * 1e3);
|
|
3239
|
-
proc.stdout?.on("data", (data) => {
|
|
3240
|
-
stdout += data.toString();
|
|
3241
|
-
});
|
|
3242
|
-
proc.stderr?.on("data", (data) => {
|
|
3243
|
-
stderr += data.toString();
|
|
3244
|
-
});
|
|
3245
|
-
proc.on("close", (code) => {
|
|
3246
|
-
clearTimeout(timeoutId);
|
|
3247
|
-
resolve({
|
|
3248
|
-
stdout,
|
|
3249
|
-
stderr,
|
|
3250
|
-
exitCode: code ?? -1
|
|
3251
|
-
});
|
|
3252
|
-
});
|
|
3253
|
-
proc.on("error", () => {
|
|
3254
|
-
clearTimeout(timeoutId);
|
|
3255
|
-
resolve(null);
|
|
3256
|
-
});
|
|
3263
|
+
const result = await spawnProcess(cmd, {
|
|
3264
|
+
cwd,
|
|
3265
|
+
timeoutSeconds: timeoutSeconds ?? 60,
|
|
3266
|
+
maxStdout: 1e5,
|
|
3267
|
+
// Tool output can be large
|
|
3268
|
+
maxStderr: 5e4
|
|
3257
3269
|
});
|
|
3270
|
+
return result;
|
|
3258
3271
|
}
|
|
3259
3272
|
function buildToolCommand(tool, files, autoFix) {
|
|
3260
3273
|
const pm = detectPackageManagerPrefix();
|
|
@@ -3483,7 +3496,7 @@ function formatToolNotAvailable(requested, available) {
|
|
|
3483
3496
|
}
|
|
3484
3497
|
|
|
3485
3498
|
// src/tools/kumaReflect.ts
|
|
3486
|
-
import { execSync as
|
|
3499
|
+
import { execSync as execSync3 } from "child_process";
|
|
3487
3500
|
async function handleReflect(params) {
|
|
3488
3501
|
const summary = sessionMemory.getSummary();
|
|
3489
3502
|
const goal = params.goal || summary.currentGoal || "";
|
|
@@ -3505,7 +3518,7 @@ async function handleReflect(params) {
|
|
|
3505
3518
|
let gitStat = "";
|
|
3506
3519
|
try {
|
|
3507
3520
|
const root = getProjectRoot();
|
|
3508
|
-
gitStat =
|
|
3521
|
+
gitStat = execSync3("git diff --stat", {
|
|
3509
3522
|
cwd: root,
|
|
3510
3523
|
encoding: "utf-8",
|
|
3511
3524
|
timeout: 5e3
|
|
@@ -3576,86 +3589,541 @@ async function handleReflect(params) {
|
|
|
3576
3589
|
);
|
|
3577
3590
|
}
|
|
3578
3591
|
|
|
3579
|
-
// src/
|
|
3580
|
-
import {
|
|
3581
|
-
|
|
3592
|
+
// src/tools/kumaGuard.ts
|
|
3593
|
+
import { execSync as execSync6 } from "child_process";
|
|
3594
|
+
|
|
3595
|
+
// src/guards/antiPatternDetector.ts
|
|
3582
3596
|
import fs11 from "fs";
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3597
|
+
import path11 from "path";
|
|
3598
|
+
import { execSync as execSync4 } from "child_process";
|
|
3599
|
+
var SCRIPT_PATCH_PATTERNS = [
|
|
3600
|
+
"writeFileSync",
|
|
3601
|
+
"writeFile",
|
|
3602
|
+
"replace(",
|
|
3603
|
+
"replaceAll(",
|
|
3604
|
+
"sed",
|
|
3605
|
+
"awk",
|
|
3606
|
+
"fs.write",
|
|
3607
|
+
"patch",
|
|
3608
|
+
"modify"
|
|
3609
|
+
];
|
|
3610
|
+
var BASH_GREP_PATTERNS = [
|
|
3611
|
+
"grep -rn",
|
|
3612
|
+
"grep -r",
|
|
3613
|
+
"grep -n",
|
|
3614
|
+
"| grep",
|
|
3615
|
+
"ripgrep",
|
|
3616
|
+
"rg ",
|
|
3617
|
+
"ag "
|
|
3618
|
+
];
|
|
3619
|
+
var SCRIPT_EXTENSIONS = [".py", ".js", ".mjs", ".cjs", ".ts"];
|
|
3620
|
+
function findPatchScripts(projectRoot) {
|
|
3621
|
+
const warnings = [];
|
|
3622
|
+
const recentFiles = scanRecentFiles(projectRoot);
|
|
3623
|
+
for (const file of recentFiles) {
|
|
3624
|
+
const ext = path11.extname(file).toLowerCase();
|
|
3625
|
+
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
3626
|
+
const relativePath = path11.relative(projectRoot, file);
|
|
3627
|
+
if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".")) {
|
|
3628
|
+
continue;
|
|
3629
|
+
}
|
|
3630
|
+
try {
|
|
3631
|
+
const content = fs11.readFileSync(file, "utf-8").toLowerCase();
|
|
3632
|
+
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
3633
|
+
if (matchedPattern) {
|
|
3634
|
+
warnings.push({
|
|
3635
|
+
severity: "high",
|
|
3636
|
+
pattern: "script-patching",
|
|
3637
|
+
message: `Created script file that modifies other files: ${path11.basename(file)}`,
|
|
3638
|
+
suggestion: "Use **precise_diff_editor** instead \u2014 it has fuzzy matching, auto-backup, and rollback support",
|
|
3639
|
+
evidence: `File: ${relativePath} contains '${matchedPattern}'`,
|
|
3640
|
+
filePath: relativePath
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
} catch {
|
|
3644
|
+
}
|
|
3595
3645
|
}
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3646
|
+
return warnings;
|
|
3647
|
+
}
|
|
3648
|
+
function detectBashGrepUsage() {
|
|
3649
|
+
const warnings = [];
|
|
3650
|
+
const toolCalls = sessionMemory.getToolCallHistory(100);
|
|
3651
|
+
for (const call of toolCalls) {
|
|
3652
|
+
if (call.toolName !== "execute_safe_test") continue;
|
|
3653
|
+
const cmd = call.params?.customCommand || call.params?.command || "";
|
|
3654
|
+
const matchedPattern = BASH_GREP_PATTERNS.find((p) => cmd.toLowerCase().includes(p));
|
|
3655
|
+
if (matchedPattern) {
|
|
3656
|
+
warnings.push({
|
|
3657
|
+
severity: "medium",
|
|
3658
|
+
pattern: "bash-grep",
|
|
3659
|
+
message: `Used bash grep instead of smart_grep`,
|
|
3660
|
+
suggestion: "Use **smart_grep** \u2014 it returns line numbers + context, caches results, respects .gitignore",
|
|
3661
|
+
evidence: `Command: ${cmd.substring(0, 120)}`
|
|
3662
|
+
});
|
|
3663
|
+
break;
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
return warnings;
|
|
3667
|
+
}
|
|
3668
|
+
function scanRecentFiles(projectRoot) {
|
|
3669
|
+
const recent = [];
|
|
3670
|
+
try {
|
|
3671
|
+
const entries = fs11.readdirSync(projectRoot, { withFileTypes: true });
|
|
3672
|
+
const now = Date.now();
|
|
3673
|
+
const recentThreshold = 30 * 60 * 1e3;
|
|
3674
|
+
for (const entry of entries) {
|
|
3675
|
+
if (!entry.isFile()) continue;
|
|
3600
3676
|
try {
|
|
3601
|
-
|
|
3602
|
-
|
|
3677
|
+
const stat = fs11.statSync(path11.join(projectRoot, entry.name));
|
|
3678
|
+
if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
|
|
3679
|
+
recent.push(path11.join(projectRoot, entry.name));
|
|
3680
|
+
}
|
|
3603
3681
|
} catch {
|
|
3604
|
-
|
|
3682
|
+
continue;
|
|
3605
3683
|
}
|
|
3606
3684
|
}
|
|
3607
|
-
|
|
3608
|
-
return this.initPromise;
|
|
3685
|
+
} catch {
|
|
3609
3686
|
}
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
}
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3687
|
+
return recent;
|
|
3688
|
+
}
|
|
3689
|
+
function detectGitPatchScripts(projectRoot) {
|
|
3690
|
+
const warnings = [];
|
|
3691
|
+
try {
|
|
3692
|
+
const statusStdout = execSync4("git status --porcelain", {
|
|
3693
|
+
cwd: projectRoot,
|
|
3694
|
+
encoding: "utf-8",
|
|
3695
|
+
timeout: 3e3
|
|
3696
|
+
}).trim();
|
|
3697
|
+
if (statusStdout) {
|
|
3698
|
+
const lines = statusStdout.split("\n").filter(Boolean);
|
|
3699
|
+
for (const line of lines) {
|
|
3700
|
+
const prefix = line.substring(0, 2);
|
|
3701
|
+
if (prefix !== "??" && prefix !== "A ") continue;
|
|
3702
|
+
const file = line.substring(3).trim();
|
|
3703
|
+
const ext = path11.extname(file).toLowerCase();
|
|
3704
|
+
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
3705
|
+
const isRootLevel = !file.includes("/");
|
|
3706
|
+
const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
|
|
3707
|
+
if (!isRootLevel && !isScriptsDir) continue;
|
|
3708
|
+
const fullPath = path11.join(projectRoot, file);
|
|
3709
|
+
if (fs11.existsSync(fullPath)) {
|
|
3710
|
+
const content = fs11.readFileSync(fullPath, "utf-8").toLowerCase();
|
|
3711
|
+
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
3712
|
+
if (matchedPattern) {
|
|
3713
|
+
warnings.push({
|
|
3714
|
+
severity: "high",
|
|
3715
|
+
pattern: "script-patching",
|
|
3716
|
+
message: `Patch script detected: ${file}`,
|
|
3717
|
+
suggestion: "Use **precise_diff_editor** instead \u2014 it has fuzzy matching, auto-backup, and rollback support",
|
|
3718
|
+
evidence: `File: ${file} (contains '${matchedPattern}')`,
|
|
3719
|
+
filePath: file
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3645
3723
|
}
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3724
|
+
}
|
|
3725
|
+
const deletedStdout = execSync4("git diff --name-only --diff-filter=D HEAD", {
|
|
3726
|
+
cwd: projectRoot,
|
|
3727
|
+
encoding: "utf-8",
|
|
3728
|
+
timeout: 3e3
|
|
3729
|
+
}).trim();
|
|
3730
|
+
if (deletedStdout) {
|
|
3731
|
+
const deletedFiles = deletedStdout.split("\n").filter(Boolean);
|
|
3732
|
+
for (const file of deletedFiles) {
|
|
3733
|
+
const ext = path11.extname(file).toLowerCase();
|
|
3734
|
+
if (SCRIPT_EXTENSIONS.includes(ext)) {
|
|
3735
|
+
warnings.push({
|
|
3736
|
+
severity: "high",
|
|
3737
|
+
pattern: "script-patching",
|
|
3738
|
+
message: `Patch script was tracked then deleted: ${file}`,
|
|
3739
|
+
suggestion: "Use **precise_diff_editor** instead of creating disposable scripts. It has auto-backup + rollback.",
|
|
3740
|
+
evidence: `Git shows ${file} was deleted`,
|
|
3741
|
+
filePath: file
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3657
3744
|
}
|
|
3658
|
-
|
|
3745
|
+
}
|
|
3746
|
+
} catch {
|
|
3747
|
+
}
|
|
3748
|
+
return warnings;
|
|
3749
|
+
}
|
|
3750
|
+
function detectBashSedUsage() {
|
|
3751
|
+
const warnings = [];
|
|
3752
|
+
const toolCalls = sessionMemory.getToolCallHistory(100);
|
|
3753
|
+
const SED_PATTERNS = [
|
|
3754
|
+
"sed -i",
|
|
3755
|
+
"sed \\'",
|
|
3756
|
+
"sed '",
|
|
3757
|
+
"awk '",
|
|
3758
|
+
"awk \\'",
|
|
3759
|
+
"cat <<",
|
|
3760
|
+
'>> "$file',
|
|
3761
|
+
">> '$file",
|
|
3762
|
+
'echo " >> ',
|
|
3763
|
+
"echo ' >> ",
|
|
3764
|
+
"printf '%s' >"
|
|
3765
|
+
];
|
|
3766
|
+
for (const call of toolCalls) {
|
|
3767
|
+
if (call.toolName !== "execute_safe_test") continue;
|
|
3768
|
+
const cmd = call.params?.customCommand || call.params?.command || "";
|
|
3769
|
+
if (SED_PATTERNS.some((p) => cmd.includes(p))) {
|
|
3770
|
+
warnings.push({
|
|
3771
|
+
severity: "high",
|
|
3772
|
+
pattern: "bash-sed-editing",
|
|
3773
|
+
message: "Used bash sed/awk to edit source files instead of precise_diff_editor",
|
|
3774
|
+
suggestion: 'Use **precise_diff_editor** for all file modifications \u2014 it has fuzzy matching, auto-backup, and rollback.\n\n\u2705 Correct format:\nprecise_diff_editor({\n filePath: "src/file.ts",\n edits: [\n { searchBlock: "old code", replaceBlock: "new code" }\n ]\n})',
|
|
3775
|
+
evidence: `Command: ${cmd.substring(0, 150)}`
|
|
3776
|
+
});
|
|
3777
|
+
break;
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
return warnings;
|
|
3781
|
+
}
|
|
3782
|
+
function detectAllAntiPatterns() {
|
|
3783
|
+
const projectRoot = getProjectRoot();
|
|
3784
|
+
const warnings = [];
|
|
3785
|
+
warnings.push(...findPatchScripts(projectRoot));
|
|
3786
|
+
warnings.push(...detectBashGrepUsage());
|
|
3787
|
+
warnings.push(...detectGitPatchScripts(projectRoot));
|
|
3788
|
+
warnings.push(...detectBashSedUsage());
|
|
3789
|
+
return warnings;
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
// src/engine/contextSnapshot.ts
|
|
3793
|
+
import fs12 from "fs";
|
|
3794
|
+
import path12 from "path";
|
|
3795
|
+
import { execSync as execSync5 } from "child_process";
|
|
3796
|
+
var SNAPSHOT_DIR = "context-snapshots";
|
|
3797
|
+
function snapshotDir() {
|
|
3798
|
+
return path12.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
|
|
3799
|
+
}
|
|
3800
|
+
function ensureSnapshotDir() {
|
|
3801
|
+
const dir = snapshotDir();
|
|
3802
|
+
if (!fs12.existsSync(dir)) {
|
|
3803
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
function snapshotFilePath(timestamp) {
|
|
3807
|
+
return path12.join(snapshotDir(), `${timestamp}.json`);
|
|
3808
|
+
}
|
|
3809
|
+
function saveSnapshot(goal) {
|
|
3810
|
+
try {
|
|
3811
|
+
ensureSnapshotDir();
|
|
3812
|
+
} catch {
|
|
3813
|
+
return null;
|
|
3814
|
+
}
|
|
3815
|
+
const summary = sessionMemory.getSummary();
|
|
3816
|
+
const snapshot = {
|
|
3817
|
+
version: 1,
|
|
3818
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3819
|
+
goal: goal || summary.currentGoal || "",
|
|
3820
|
+
modifiedFiles: summary.modifiedFiles || [],
|
|
3821
|
+
unresolvedFailures: summary.unresolvedFailures || [],
|
|
3822
|
+
toolCallCount: summary.toolCallCount || 0,
|
|
3823
|
+
gitDiffStat: getGitDiffStat(),
|
|
3824
|
+
completedSteps: summary.completedSteps || [],
|
|
3825
|
+
hasConventions: !!summary.hasConventions
|
|
3826
|
+
};
|
|
3827
|
+
try {
|
|
3828
|
+
fs12.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
|
|
3829
|
+
} catch {
|
|
3830
|
+
}
|
|
3831
|
+
sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
|
|
3832
|
+
return snapshot;
|
|
3833
|
+
}
|
|
3834
|
+
function listSnapshots() {
|
|
3835
|
+
const dir = snapshotDir();
|
|
3836
|
+
if (!fs12.existsSync(dir)) return [];
|
|
3837
|
+
try {
|
|
3838
|
+
const files = fs12.readdirSync(dir).filter((f) => f.endsWith(".json")).sort((a, b) => b.localeCompare(a));
|
|
3839
|
+
return files.map((f) => {
|
|
3840
|
+
try {
|
|
3841
|
+
const content = fs12.readFileSync(path12.join(dir, f), "utf-8");
|
|
3842
|
+
return JSON.parse(content);
|
|
3843
|
+
} catch {
|
|
3844
|
+
return null;
|
|
3845
|
+
}
|
|
3846
|
+
}).filter((s) => s !== null);
|
|
3847
|
+
} catch {
|
|
3848
|
+
return [];
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
function formatSnapshot(snapshot) {
|
|
3852
|
+
const lines = [
|
|
3853
|
+
`\u{1F4F8} **Context Snapshot** \u2014 ${snapshot.timestamp}`,
|
|
3854
|
+
"",
|
|
3855
|
+
`\u{1F3AF} **Goal:** ${snapshot.goal || "(not set)"}`,
|
|
3856
|
+
`\u{1F4C1} **Modified Files:** ${snapshot.modifiedFiles.length}`,
|
|
3857
|
+
`\u274C **Unresolved Failures:** ${snapshot.unresolvedFailures.length}`,
|
|
3858
|
+
`\u{1F527} **Tool Calls:** ${snapshot.toolCallCount}`,
|
|
3859
|
+
`\u{1F4CA} **Git Diff:** ${snapshot.gitDiffStat || "clean"}`,
|
|
3860
|
+
`\u2705 **Completed Steps:** ${snapshot.completedSteps.length}`,
|
|
3861
|
+
`\u{1F4D0} **Conventions:** ${snapshot.hasConventions ? "detected" : "not detected"}`,
|
|
3862
|
+
""
|
|
3863
|
+
];
|
|
3864
|
+
if (snapshot.modifiedFiles.length > 0) {
|
|
3865
|
+
lines.push("**Modified Files:**");
|
|
3866
|
+
for (const f of snapshot.modifiedFiles) {
|
|
3867
|
+
lines.push(` - ${f.filePath} (${f.status})`);
|
|
3868
|
+
}
|
|
3869
|
+
lines.push("");
|
|
3870
|
+
}
|
|
3871
|
+
if (snapshot.unresolvedFailures.length > 0) {
|
|
3872
|
+
lines.push("**Unresolved Failures:**");
|
|
3873
|
+
for (const f of snapshot.unresolvedFailures) {
|
|
3874
|
+
lines.push(` - [${f.task}] ${f.error.substring(0, 150)}`);
|
|
3875
|
+
}
|
|
3876
|
+
lines.push("");
|
|
3877
|
+
}
|
|
3878
|
+
lines.push('\u{1F4A1} Use kuma_guard({ check: "context" }) to create a new snapshot.');
|
|
3879
|
+
return lines.join("\n");
|
|
3880
|
+
}
|
|
3881
|
+
function getGitDiffStat() {
|
|
3882
|
+
try {
|
|
3883
|
+
const root = getProjectRoot();
|
|
3884
|
+
const stdout = execSync5("git diff --stat", {
|
|
3885
|
+
cwd: root,
|
|
3886
|
+
encoding: "utf-8",
|
|
3887
|
+
timeout: 3e3
|
|
3888
|
+
}).trim();
|
|
3889
|
+
return stdout || "clean";
|
|
3890
|
+
} catch {
|
|
3891
|
+
return "clean";
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
// src/tools/kumaGuard.ts
|
|
3896
|
+
async function handleKumaGuard(params) {
|
|
3897
|
+
const { check = "all", goal: inputGoal } = params;
|
|
3898
|
+
const summary = sessionMemory.getSummary();
|
|
3899
|
+
const goal = inputGoal || summary.currentGoal || "";
|
|
3900
|
+
sessionMemory.recordToolCall("kuma_guard", { check, goal });
|
|
3901
|
+
const warnings = [];
|
|
3902
|
+
if (check === "all" || check === "anti-pattern") {
|
|
3903
|
+
warnings.push(...detectAllAntiPatterns());
|
|
3904
|
+
}
|
|
3905
|
+
const loop = check === "all" || check === "loop" ? sessionMemory.detectLoop() : { isLooping: false };
|
|
3906
|
+
if (loop.isLooping) {
|
|
3907
|
+
warnings.push({
|
|
3908
|
+
severity: "high",
|
|
3909
|
+
pattern: "tool-loop",
|
|
3910
|
+
message: loop.message ?? "Detected potential tool call loop",
|
|
3911
|
+
suggestion: "Switch approach \u2014 try reading the file first with smart_file_picker"
|
|
3912
|
+
});
|
|
3913
|
+
}
|
|
3914
|
+
const drifts = [];
|
|
3915
|
+
const toolCalls = sessionMemory.getToolCallHistory(50);
|
|
3916
|
+
const hasRunTests = toolCalls.some((c) => c.toolName === "execute_safe_test");
|
|
3917
|
+
if (check === "all" || check === "drift") {
|
|
3918
|
+
const modifiedFiles = sessionMemory.getModifiedFiles();
|
|
3919
|
+
const failedFiles = sessionMemory.getFailedFiles();
|
|
3920
|
+
let unresolvedCount = 0;
|
|
3921
|
+
for (const f of failedFiles) {
|
|
3922
|
+
for (const ff of f.failures) {
|
|
3923
|
+
if (!ff.resolved) unresolvedCount++;
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
if (modifiedFiles.length > 0 && !hasRunTests) {
|
|
3927
|
+
drifts.push(`${modifiedFiles.length} file(s) edited but no test run`);
|
|
3928
|
+
warnings.push({
|
|
3929
|
+
severity: "medium",
|
|
3930
|
+
pattern: "no-test-after-edit",
|
|
3931
|
+
message: `${modifiedFiles.length} file(s) modified without running tests`,
|
|
3932
|
+
suggestion: 'Run execute_safe_test({ task: "typecheck" }) to verify changes'
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
if (unresolvedCount > 0) {
|
|
3936
|
+
drifts.push(`${unresolvedCount} unresolved failure(s)`);
|
|
3937
|
+
}
|
|
3938
|
+
try {
|
|
3939
|
+
const root = getProjectRoot();
|
|
3940
|
+
const gitStat = execSync6("git diff --stat", {
|
|
3941
|
+
cwd: root,
|
|
3942
|
+
encoding: "utf-8",
|
|
3943
|
+
timeout: 3e3
|
|
3944
|
+
}).trim();
|
|
3945
|
+
if (gitStat) {
|
|
3946
|
+
drifts.push(`Git diff: ${gitStat}`);
|
|
3947
|
+
}
|
|
3948
|
+
} catch {
|
|
3949
|
+
}
|
|
3950
|
+
const editCalls = toolCalls.filter(
|
|
3951
|
+
(c) => c.toolName === "precise_diff_editor" || c.toolName === "batch_file_writer"
|
|
3952
|
+
).length;
|
|
3953
|
+
if (editCalls > 5) {
|
|
3954
|
+
warnings.push({
|
|
3955
|
+
severity: "low",
|
|
3956
|
+
pattern: "excessive-edits",
|
|
3957
|
+
message: `${editCalls} file operations in a row`,
|
|
3958
|
+
suggestion: "Consider if all edits are needed. Run tests before making more changes."
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
if (check === "context") {
|
|
3963
|
+
const snapshot = saveSnapshot(goal);
|
|
3964
|
+
if (!snapshot) {
|
|
3965
|
+
return "\u26A0\uFE0F Could not create context snapshot. The .kuma directory might not be accessible.";
|
|
3966
|
+
}
|
|
3967
|
+
return formatSnapshot(snapshot);
|
|
3968
|
+
}
|
|
3969
|
+
const hasWarnings = warnings.length > 0;
|
|
3970
|
+
const hasDrifts = drifts.length > 0;
|
|
3971
|
+
const onTrack = !hasWarnings && !hasDrifts;
|
|
3972
|
+
let suggestion;
|
|
3973
|
+
if (warnings.some((w) => w.severity === "high" && w.pattern === "script-patching")) {
|
|
3974
|
+
suggestion = "Remove patch scripts and use precise_diff_editor for all file modifications";
|
|
3975
|
+
} else if (warnings.some((w) => w.pattern === "tool-loop")) {
|
|
3976
|
+
suggestion = "Switch approach \u2014 current tool is not making progress";
|
|
3977
|
+
} else if (warnings.some((w) => w.pattern === "no-test-after-edit")) {
|
|
3978
|
+
suggestion = "Run tests to verify your changes before continuing";
|
|
3979
|
+
} else if (warnings.some((w) => w.pattern === "bash-grep")) {
|
|
3980
|
+
suggestion = "Use smart_grep for code search instead of bash grep";
|
|
3981
|
+
} else if (warnings.some((w) => w.pattern === "excessive-edits")) {
|
|
3982
|
+
suggestion = "Pause and review: are all these edits necessary?";
|
|
3983
|
+
} else if (!goal) {
|
|
3984
|
+
suggestion = "No goal set \u2014 use goal parameter or setGoal to track intent";
|
|
3985
|
+
} else {
|
|
3986
|
+
suggestion = "On track \u2014 continue with current approach";
|
|
3987
|
+
}
|
|
3988
|
+
const report = {
|
|
3989
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3990
|
+
onTrack,
|
|
3991
|
+
warnings,
|
|
3992
|
+
drifts,
|
|
3993
|
+
suggestion,
|
|
3994
|
+
stats: {
|
|
3995
|
+
goal,
|
|
3996
|
+
modifiedFiles: summary.modifiedFiles.length,
|
|
3997
|
+
toolCalls: summary.toolCallCount ?? 0,
|
|
3998
|
+
unresolvedFailures: summary.unresolvedFailures ? summary.unresolvedFailures.length : 0,
|
|
3999
|
+
hasLoop: loop.isLooping,
|
|
4000
|
+
hasRunTests
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
return JSON.stringify(report, null, 2);
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
// src/tools/kumaContext.ts
|
|
4007
|
+
async function handleKumaContext(params) {
|
|
4008
|
+
const { action, goal } = params;
|
|
4009
|
+
switch (action) {
|
|
4010
|
+
case "save": {
|
|
4011
|
+
const snapshot = saveSnapshot(goal);
|
|
4012
|
+
if (!snapshot) {
|
|
4013
|
+
return "\u26A0\uFE0F Could not create context snapshot. The .kuma directory might not be accessible.";
|
|
4014
|
+
}
|
|
4015
|
+
return formatSnapshot(snapshot);
|
|
4016
|
+
}
|
|
4017
|
+
case "list": {
|
|
4018
|
+
const snapshots = listSnapshots();
|
|
4019
|
+
if (snapshots.length === 0) {
|
|
4020
|
+
return '\u{1F4F8} **No snapshots found.**\n\nRun `kuma_guard({ check: "context" })` or `kuma_context({ action: "save" })` to create one.';
|
|
4021
|
+
}
|
|
4022
|
+
const lines = [
|
|
4023
|
+
`\u{1F4F8} **Context Snapshots** \u2014 ${snapshots.length} available`,
|
|
4024
|
+
""
|
|
4025
|
+
];
|
|
4026
|
+
for (let i = 0; i < snapshots.length; i++) {
|
|
4027
|
+
const s = snapshots[i];
|
|
4028
|
+
const fileCount = s.modifiedFiles.length;
|
|
4029
|
+
const failureCount = s.unresolvedFailures.length;
|
|
4030
|
+
lines.push(
|
|
4031
|
+
`[${i + 1}] ${s.timestamp}`,
|
|
4032
|
+
` \u{1F3AF} Goal: ${s.goal || "(not set)"}`,
|
|
4033
|
+
` \u{1F4C1} ${fileCount} files, \u274C ${failureCount} failures, \u{1F527} ${s.toolCallCount} tool calls`,
|
|
4034
|
+
` \u{1F4CA} Git: ${s.gitDiffStat || "clean"}`,
|
|
4035
|
+
""
|
|
4036
|
+
);
|
|
4037
|
+
}
|
|
4038
|
+
lines.push('\u{1F4A1} Use kuma_context({ action: "save" }) to create a new snapshot.');
|
|
4039
|
+
lines.push('\u{1F4A1} Or kuma_guard({ check: "context" }) to save and get full context.');
|
|
4040
|
+
return lines.join("\n");
|
|
4041
|
+
}
|
|
4042
|
+
default:
|
|
4043
|
+
return `Error: Action "${action}" not supported. Use "save" or "list".`;
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
// src/engine/lspClient.ts
|
|
4048
|
+
import { spawn as spawn2 } from "child_process";
|
|
4049
|
+
import path13 from "path";
|
|
4050
|
+
import fs13 from "fs";
|
|
4051
|
+
var LSPClient = class {
|
|
4052
|
+
process = null;
|
|
4053
|
+
requestId = 0;
|
|
4054
|
+
pending = /* @__PURE__ */ new Map();
|
|
4055
|
+
buffer = Buffer.alloc(0);
|
|
4056
|
+
initialized = false;
|
|
4057
|
+
initPromise = null;
|
|
4058
|
+
openDocuments = /* @__PURE__ */ new Set();
|
|
4059
|
+
_isAvailable = true;
|
|
4060
|
+
/** Check whether LSP is available (binary installed) */
|
|
4061
|
+
isAvailable() {
|
|
4062
|
+
return this._isAvailable;
|
|
4063
|
+
}
|
|
4064
|
+
async ensureInitialized() {
|
|
4065
|
+
if (this._isAvailable !== void 0 && !this._isAvailable) return;
|
|
4066
|
+
if (this.initialized && this.process) return;
|
|
4067
|
+
if (this.initPromise) {
|
|
4068
|
+
try {
|
|
4069
|
+
await this.initPromise;
|
|
4070
|
+
return;
|
|
4071
|
+
} catch {
|
|
4072
|
+
this.initPromise = null;
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
this.initPromise = this.initialize();
|
|
4076
|
+
return this.initPromise;
|
|
4077
|
+
}
|
|
4078
|
+
async initialize() {
|
|
4079
|
+
const root = getProjectRoot();
|
|
4080
|
+
const lspBinary = this.resolveLspBinary(root);
|
|
4081
|
+
if (!lspBinary) {
|
|
4082
|
+
this._isAvailable = false;
|
|
4083
|
+
this.initialized = false;
|
|
4084
|
+
this.initPromise = null;
|
|
4085
|
+
console.error(`[LSP] typescript-language-server not found. LSP features will fallback to regex.`);
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
const tsconfigPath = path13.join(root, "tsconfig.json");
|
|
4089
|
+
if (!fs13.existsSync(tsconfigPath)) {
|
|
4090
|
+
console.error(`[LSP] Warning: tsconfig.json not found at "${root}". Running in implicit project mode.`);
|
|
4091
|
+
}
|
|
4092
|
+
this.process = spawn2(lspBinary, ["--stdio", "--log-level=4"], {
|
|
4093
|
+
cwd: root,
|
|
4094
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4095
|
+
env: { ...process.env }
|
|
4096
|
+
});
|
|
4097
|
+
this.process.stdout?.on("data", (data) => {
|
|
4098
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
4099
|
+
this.processBuffer();
|
|
4100
|
+
});
|
|
4101
|
+
this.process.stderr?.on("data", (data) => {
|
|
4102
|
+
console.error(`[LSP] ${data.toString().trim()}`);
|
|
4103
|
+
});
|
|
4104
|
+
this.process.on("exit", (code) => {
|
|
4105
|
+
console.error(`[LSP] Process exited with code ${code}`);
|
|
4106
|
+
this.initialized = false;
|
|
4107
|
+
this.process = null;
|
|
4108
|
+
this.initPromise = null;
|
|
4109
|
+
this.openDocuments.clear();
|
|
4110
|
+
this.buffer = Buffer.alloc(0);
|
|
4111
|
+
for (const [, pending] of this.pending) {
|
|
4112
|
+
pending.reject(new Error(`LSP server exited (code ${code})`));
|
|
4113
|
+
}
|
|
4114
|
+
this.pending.clear();
|
|
4115
|
+
});
|
|
4116
|
+
this.process.on("error", (err) => {
|
|
4117
|
+
console.error(`[LSP] Process error: ${err.message}. LSP features will fallback to regex.`);
|
|
4118
|
+
this._isAvailable = false;
|
|
4119
|
+
this.process = null;
|
|
4120
|
+
this.initialized = false;
|
|
4121
|
+
this.initPromise = null;
|
|
4122
|
+
this.openDocuments.clear();
|
|
4123
|
+
for (const [, pending] of this.pending) {
|
|
4124
|
+
pending.reject(new Error(`LSP server error: ${err.message}`));
|
|
4125
|
+
}
|
|
4126
|
+
this.pending.clear();
|
|
3659
4127
|
});
|
|
3660
4128
|
const rootUri = this.toUri(root);
|
|
3661
4129
|
const initResult = await this.sendRequestRaw("initialize", {
|
|
@@ -3680,19 +4148,19 @@ var LSPClient = class {
|
|
|
3680
4148
|
/** Resolve the typescript-language-server binary from local or global install */
|
|
3681
4149
|
resolveLspBinary(projectRoot) {
|
|
3682
4150
|
const candidates = [
|
|
3683
|
-
|
|
3684
|
-
|
|
4151
|
+
path13.join(projectRoot, "node_modules", ".bin", "typescript-language-server"),
|
|
4152
|
+
path13.join(projectRoot, "..", "node_modules", ".bin", "typescript-language-server")
|
|
3685
4153
|
];
|
|
3686
4154
|
for (const candidate of candidates) {
|
|
3687
|
-
if (
|
|
4155
|
+
if (fs13.existsSync(candidate)) {
|
|
3688
4156
|
return candidate;
|
|
3689
4157
|
}
|
|
3690
4158
|
}
|
|
3691
4159
|
try {
|
|
3692
4160
|
const envPath = process.env.PATH ?? "";
|
|
3693
|
-
for (const dir of envPath.split(
|
|
3694
|
-
const binPath =
|
|
3695
|
-
if (
|
|
4161
|
+
for (const dir of envPath.split(path13.delimiter)) {
|
|
4162
|
+
const binPath = path13.join(dir, "typescript-language-server");
|
|
4163
|
+
if (fs13.existsSync(binPath)) {
|
|
3696
4164
|
return binPath;
|
|
3697
4165
|
}
|
|
3698
4166
|
}
|
|
@@ -3713,7 +4181,7 @@ var LSPClient = class {
|
|
|
3713
4181
|
this.openDocuments.add(filePath);
|
|
3714
4182
|
let content;
|
|
3715
4183
|
try {
|
|
3716
|
-
content =
|
|
4184
|
+
content = fs13.readFileSync(filePath, "utf-8");
|
|
3717
4185
|
} catch {
|
|
3718
4186
|
content = "";
|
|
3719
4187
|
}
|
|
@@ -3930,8 +4398,8 @@ var LSPClient = class {
|
|
|
3930
4398
|
var lspClient = new LSPClient();
|
|
3931
4399
|
|
|
3932
4400
|
// src/tools/lspTools.ts
|
|
3933
|
-
import
|
|
3934
|
-
import
|
|
4401
|
+
import fs14 from "fs";
|
|
4402
|
+
import path14 from "path";
|
|
3935
4403
|
async function handleFindReferences(params) {
|
|
3936
4404
|
const { filePath, line, character } = params;
|
|
3937
4405
|
const validation = validateFilePath(filePath);
|
|
@@ -3939,7 +4407,7 @@ async function handleFindReferences(params) {
|
|
|
3939
4407
|
return `Error: ${validation.error.message}`;
|
|
3940
4408
|
}
|
|
3941
4409
|
const resolvedPath = validation.resolvedPath;
|
|
3942
|
-
if (!
|
|
4410
|
+
if (!fs14.existsSync(resolvedPath)) {
|
|
3943
4411
|
return `Error: File not found: "${filePath}".`;
|
|
3944
4412
|
}
|
|
3945
4413
|
if (!lspClient.isAvailable()) {
|
|
@@ -3961,7 +4429,7 @@ async function handleFindReferences(params) {
|
|
|
3961
4429
|
const enrichedRefs = references.map((ref) => {
|
|
3962
4430
|
let lineContent = "";
|
|
3963
4431
|
try {
|
|
3964
|
-
const content =
|
|
4432
|
+
const content = fs14.readFileSync(ref.filePath, "utf-8");
|
|
3965
4433
|
const lines2 = content.split("\n");
|
|
3966
4434
|
lineContent = lines2[ref.line]?.trim() ?? "";
|
|
3967
4435
|
} catch {
|
|
@@ -3977,11 +4445,11 @@ async function handleFindReferences(params) {
|
|
|
3977
4445
|
const projectRoot = getProjectRoot();
|
|
3978
4446
|
const lines = [
|
|
3979
4447
|
`\u{1F50D} **Find References** \u2014 ${enrichedRefs.length} references found`,
|
|
3980
|
-
`\u{1F4CD} File: ${
|
|
4448
|
+
`\u{1F4CD} File: ${path14.relative(projectRoot, resolvedPath)}:${line + 1}:${character + 1}`,
|
|
3981
4449
|
""
|
|
3982
4450
|
];
|
|
3983
4451
|
for (const [file, refs] of grouped) {
|
|
3984
|
-
const relPath =
|
|
4452
|
+
const relPath = path14.relative(projectRoot, file);
|
|
3985
4453
|
lines.push(`**\u{1F4C4} ${relPath}:**`);
|
|
3986
4454
|
for (const ref of refs) {
|
|
3987
4455
|
const loc = `L${ref.line + 1}:${ref.character + 1}`;
|
|
@@ -4002,7 +4470,7 @@ async function handleGoToDefinition(params) {
|
|
|
4002
4470
|
return `Error: ${validation.error.message}`;
|
|
4003
4471
|
}
|
|
4004
4472
|
const resolvedPath = validation.resolvedPath;
|
|
4005
|
-
if (!
|
|
4473
|
+
if (!fs14.existsSync(resolvedPath)) {
|
|
4006
4474
|
return `Error: File not found: "${filePath}".`;
|
|
4007
4475
|
}
|
|
4008
4476
|
if (!lspClient.isAvailable()) {
|
|
@@ -4022,10 +4490,10 @@ async function handleGoToDefinition(params) {
|
|
|
4022
4490
|
\u26A0\uFE0F Cannot find definition for symbol at this position.`;
|
|
4023
4491
|
}
|
|
4024
4492
|
const projectRoot = getProjectRoot();
|
|
4025
|
-
const relPath =
|
|
4493
|
+
const relPath = path14.relative(projectRoot, definition.filePath);
|
|
4026
4494
|
let lineContent = "";
|
|
4027
4495
|
try {
|
|
4028
|
-
const content =
|
|
4496
|
+
const content = fs14.readFileSync(definition.filePath, "utf-8");
|
|
4029
4497
|
const lines2 = content.split("\n");
|
|
4030
4498
|
lineContent = lines2[definition.line]?.trim() ?? "";
|
|
4031
4499
|
} catch {
|
|
@@ -4057,7 +4525,7 @@ async function handleRenameSymbol(params) {
|
|
|
4057
4525
|
return `Error: ${validation.error.message}`;
|
|
4058
4526
|
}
|
|
4059
4527
|
const resolvedPath = validation.resolvedPath;
|
|
4060
|
-
if (!
|
|
4528
|
+
if (!fs14.existsSync(resolvedPath)) {
|
|
4061
4529
|
return `Error: File not found: "${filePath}".`;
|
|
4062
4530
|
}
|
|
4063
4531
|
if (!lspClient.isAvailable()) {
|
|
@@ -4086,7 +4554,7 @@ Make sure:
|
|
|
4086
4554
|
const fileChanges = [];
|
|
4087
4555
|
for (const change of result.changes) {
|
|
4088
4556
|
try {
|
|
4089
|
-
const content =
|
|
4557
|
+
const content = fs14.readFileSync(change.filePath, "utf-8");
|
|
4090
4558
|
const lines2 = content.split("\n");
|
|
4091
4559
|
const sortedEdits = [...change.edits].sort((a, b) => {
|
|
4092
4560
|
if (b.line !== a.line) return b.line - a.line;
|
|
@@ -4100,10 +4568,10 @@ Make sure:
|
|
|
4100
4568
|
lines2[edit.line] = before + edit.newText + after;
|
|
4101
4569
|
}
|
|
4102
4570
|
}
|
|
4103
|
-
|
|
4571
|
+
fs14.writeFileSync(change.filePath, lines2.join("\n"), "utf-8");
|
|
4104
4572
|
totalEdits += change.edits.length;
|
|
4105
4573
|
fileChanges.push({
|
|
4106
|
-
filePath:
|
|
4574
|
+
filePath: path14.relative(projectRoot, change.filePath),
|
|
4107
4575
|
editCount: change.edits.length
|
|
4108
4576
|
});
|
|
4109
4577
|
} catch (err) {
|
|
@@ -4130,13 +4598,13 @@ async function handleGetTypeInfo(params) {
|
|
|
4130
4598
|
return `Error: ${validation.error.message}`;
|
|
4131
4599
|
}
|
|
4132
4600
|
const resolvedPath = validation.resolvedPath;
|
|
4133
|
-
if (!
|
|
4601
|
+
if (!fs14.existsSync(resolvedPath)) {
|
|
4134
4602
|
return `Error: File not found: "${filePath}".`;
|
|
4135
4603
|
}
|
|
4136
4604
|
if (!lspClient.isAvailable()) {
|
|
4137
4605
|
sessionMemory.recordToolCall("lsp_query", { action: "type", filePath, line, character, fallback: "line-context" });
|
|
4138
4606
|
try {
|
|
4139
|
-
const fileContent =
|
|
4607
|
+
const fileContent = fs14.readFileSync(resolvedPath, "utf-8");
|
|
4140
4608
|
const fileLines = fileContent.split("\n");
|
|
4141
4609
|
const targetLine = fileLines[line];
|
|
4142
4610
|
if (!targetLine) {
|
|
@@ -4144,7 +4612,7 @@ async function handleGetTypeInfo(params) {
|
|
|
4144
4612
|
}
|
|
4145
4613
|
const symbolName = extractSymbolAtPosition(resolvedPath, line, character);
|
|
4146
4614
|
const projectRoot = getProjectRoot();
|
|
4147
|
-
const relPath =
|
|
4615
|
+
const relPath = path14.relative(projectRoot, resolvedPath);
|
|
4148
4616
|
const contextStart = Math.max(0, line - 3);
|
|
4149
4617
|
const contextEnd = Math.min(fileLines.length, line + 4);
|
|
4150
4618
|
const contextLines = [];
|
|
@@ -4182,7 +4650,7 @@ async function handleGetTypeInfo(params) {
|
|
|
4182
4650
|
\u26A0\uFE0F No type info for this position.`;
|
|
4183
4651
|
}
|
|
4184
4652
|
const projectRoot = getProjectRoot();
|
|
4185
|
-
const relPath =
|
|
4653
|
+
const relPath = path14.relative(projectRoot, resolvedPath);
|
|
4186
4654
|
const lines = [
|
|
4187
4655
|
`\u{1F4CB} **Type Info** \u2014 \`${relPath}:${line + 1}:${character + 1}\``,
|
|
4188
4656
|
"",
|
|
@@ -4223,7 +4691,7 @@ async function handleLspQuery(params) {
|
|
|
4223
4691
|
}
|
|
4224
4692
|
function extractSymbolAtPosition(filePath, line, character) {
|
|
4225
4693
|
try {
|
|
4226
|
-
const content =
|
|
4694
|
+
const content = fs14.readFileSync(filePath, "utf-8");
|
|
4227
4695
|
const lines = content.split("\n");
|
|
4228
4696
|
const targetLine = lines[line];
|
|
4229
4697
|
if (!targetLine) return null;
|
|
@@ -4254,7 +4722,7 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4254
4722
|
const regex = new RegExp(`\\b${escapedSymbol}\\b`, "g");
|
|
4255
4723
|
for (const file of tsFiles.slice(0, 100)) {
|
|
4256
4724
|
try {
|
|
4257
|
-
const content =
|
|
4725
|
+
const content = fs14.readFileSync(file, "utf-8");
|
|
4258
4726
|
const lines2 = content.split("\n");
|
|
4259
4727
|
for (let i = 0; i < lines2.length; i++) {
|
|
4260
4728
|
if (regex.test(lines2[i])) {
|
|
@@ -4284,7 +4752,7 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4284
4752
|
""
|
|
4285
4753
|
];
|
|
4286
4754
|
for (const [file, refs] of grouped) {
|
|
4287
|
-
const relPath =
|
|
4755
|
+
const relPath = path14.relative(projectRoot, file);
|
|
4288
4756
|
lines.push(`**\u{1F4C4} ${relPath}:**`);
|
|
4289
4757
|
for (const ref of refs) {
|
|
4290
4758
|
lines.push(` \u2514 L${ref.line} \u2014 ${ref.content}`);
|
|
@@ -4318,14 +4786,14 @@ async function fallbackGrepDefinition(symbolName) {
|
|
|
4318
4786
|
];
|
|
4319
4787
|
for (const file of tsFiles) {
|
|
4320
4788
|
try {
|
|
4321
|
-
const content =
|
|
4789
|
+
const content = fs14.readFileSync(file, "utf-8");
|
|
4322
4790
|
const lines = content.split("\n");
|
|
4323
4791
|
for (let i = 0; i < lines.length; i++) {
|
|
4324
4792
|
const trimmed = lines[i].trim();
|
|
4325
4793
|
for (const pattern of declPatterns) {
|
|
4326
4794
|
if (pattern.test(trimmed)) {
|
|
4327
4795
|
const projectRoot = getProjectRoot();
|
|
4328
|
-
const relPath =
|
|
4796
|
+
const relPath = path14.relative(projectRoot, file);
|
|
4329
4797
|
return [
|
|
4330
4798
|
`\u{1F4CD} **Go to Definition** (regex fallback)`,
|
|
4331
4799
|
`\u{1F4C4} File: \`${relPath}\``,
|
|
@@ -4356,7 +4824,7 @@ function registerAllTools(server) {
|
|
|
4356
4824
|
"smart_grep",
|
|
4357
4825
|
"Narrow down to the specific code you need. Locates functions or text patterns \u2014 returns filename, line number, and 3 lines of context.",
|
|
4358
4826
|
{
|
|
4359
|
-
query: z.string().min(1).describe("Regex pattern to search for"),
|
|
4827
|
+
query: z.string({ invalid_type_error: 'smart_grep: "query" must be a string regex pattern.\n\n\u2705 Example: { query: "function handleUser" }' }).min(1).describe("Regex pattern to search for. Example: 'function handleAuth' or 'console\\.log'"),
|
|
4360
4828
|
targetFolder: z.string().optional().describe("Target folder (default: project root, max depth 3)"),
|
|
4361
4829
|
maxResults: z.number().min(1).max(100).optional().default(30).describe("Max results to return"),
|
|
4362
4830
|
extensions: z.array(z.string()).optional().describe("Filter results by file extensions (e.g. ['ts', 'js'])")
|
|
@@ -4392,15 +4860,17 @@ function registerAllTools(server) {
|
|
|
4392
4860
|
"precise_diff_editor",
|
|
4393
4861
|
"Edit code with safety net. Search-and-replace with fuzzy fallback + automatic versioned backup. Use action:'rollback' to undo edits.",
|
|
4394
4862
|
{
|
|
4395
|
-
filePath: z.string().min(1).describe("Path to the file to edit"),
|
|
4396
|
-
action: z.enum(["rollback"]).optional().describe("Set to 'rollback' to restore from backup"),
|
|
4863
|
+
filePath: z.string({ invalid_type_error: 'precise_diff_editor: "filePath" must be a string path relative to project root.\n\n\u2705 Example: "src/example.ts"' }).min(1).describe("Path to the file to edit"),
|
|
4864
|
+
action: z.enum(["rollback"]).optional().describe("Set to 'rollback' to restore from backup. Only use when restoring from backup."),
|
|
4397
4865
|
edits: z.array(z.object({
|
|
4398
|
-
searchBlock: z.string().min(1).describe("Code block to replace (MUST be exact or fuzzy match)"),
|
|
4399
|
-
replaceBlock: z.string().describe("Replacement code block"),
|
|
4866
|
+
searchBlock: z.string({ invalid_type_error: '"searchBlock" must be a string of the exact code to replace' }).min(1).describe("Code block to replace (MUST be exact or fuzzy match)"),
|
|
4867
|
+
replaceBlock: z.string({ invalid_type_error: '"replaceBlock" must be a string of the replacement code' }).describe("Replacement code block"),
|
|
4400
4868
|
allowMultiple: z.boolean().optional().default(false).describe("Allow multiple replacements"),
|
|
4401
4869
|
fuzzyThreshold: z.number().min(0).max(1).optional().default(0.85).describe("Fuzzy match threshold (0.0-1.0)")
|
|
4402
|
-
})
|
|
4403
|
-
|
|
4870
|
+
}), {
|
|
4871
|
+
invalid_type_error: 'precise_diff_editor: "edits" must be an ARRAY of { searchBlock, replaceBlock } objects.\n\n\u2705 Correct format:\n{\n edits: [\n { searchBlock: "old code", replaceBlock: "new code" }\n ]\n}'
|
|
4872
|
+
}).min(1).max(10).optional().describe("Array of edits (max 10). Each edit has: searchBlock (code to find), replaceBlock (new code)"),
|
|
4873
|
+
dryRun: z.boolean().optional().default(false).describe("Preview changes without writing to disk. Set dryRun: true to preview first."),
|
|
4404
4874
|
version: z.union([z.number().min(1), z.literal("list")]).optional().describe("Backup version to restore (1=newest, omit=latest, 'list'=show versions)")
|
|
4405
4875
|
},
|
|
4406
4876
|
async (params) => {
|
|
@@ -4420,7 +4890,10 @@ function registerAllTools(server) {
|
|
|
4420
4890
|
filePath: z.string().min(1).describe("File path to create"),
|
|
4421
4891
|
content: z.string().describe("File content"),
|
|
4422
4892
|
instructions: z.string().min(1).describe("Reason for creating the file")
|
|
4423
|
-
})
|
|
4893
|
+
}), {
|
|
4894
|
+
invalid_type_error: 'batch_file_writer: "files" must be an ARRAY of objects.\n\n\u2705 Correct format:\n{\n files: [\n { filePath: "src/example.ts", content: "// code", instructions: "reason" }\n ]\n}',
|
|
4895
|
+
required_error: 'batch_file_writer: "files" is required.\n\n\u2705 Correct format:\n{\n files: [\n { filePath: "src/example.ts", content: "// code", instructions: "reason" }\n ]\n}'
|
|
4896
|
+
}).min(1).max(15).describe("Array of files to create (max 15). Example: [{ filePath: 'src/x.ts', content: '...', instructions: 'why' }]")
|
|
4424
4897
|
},
|
|
4425
4898
|
async (params) => {
|
|
4426
4899
|
try {
|
|
@@ -4435,9 +4908,9 @@ function registerAllTools(server) {
|
|
|
4435
4908
|
"execute_safe_test",
|
|
4436
4909
|
"Run tests, lint, or typecheck with timeout protection and circuit breaker. Use after every edit to verify you didn't break anything.",
|
|
4437
4910
|
{
|
|
4438
|
-
task: z.enum(["test", "build", "lint", "typecheck", "custom"]).describe("Task to execute"),
|
|
4439
|
-
customCommand: z.string().optional().describe("Custom command (only if task='custom')"),
|
|
4440
|
-
timeout: z.number().min(5).max(180).optional().default(60).describe("Timeout in seconds (default: 60s, max: 180s)")
|
|
4911
|
+
task: z.enum(["test", "build", "lint", "typecheck", "custom"]).describe("Task to execute: test, build, lint, typecheck, or custom"),
|
|
4912
|
+
customCommand: z.string({ invalid_type_error: '"customCommand" must be a string command like "npm run my-script"' }).optional().describe("Custom command (required only if task='custom')"),
|
|
4913
|
+
timeout: z.number({ invalid_type_error: '"timeout" must be a number in seconds (5-180)' }).min(5).max(180).optional().default(60).describe("Timeout in seconds (default: 60s, max: 180s)")
|
|
4441
4914
|
},
|
|
4442
4915
|
async (params) => {
|
|
4443
4916
|
try {
|
|
@@ -4485,7 +4958,7 @@ function registerAllTools(server) {
|
|
|
4485
4958
|
"get_session_memory",
|
|
4486
4959
|
"Check what you've done this session: modified files, unresolved failures, tool history, memories. Accepts optional topic to load a specific memory file.",
|
|
4487
4960
|
{
|
|
4488
|
-
topic: z.enum(MEMORY_TOPICS).optional().describe("Load a specific memory topic
|
|
4961
|
+
topic: z.enum(MEMORY_TOPICS).optional().describe("Load a specific memory topic: decisions, glossary, architecture, conventions, known-issues. Example: { topic: 'decisions' }")
|
|
4489
4962
|
},
|
|
4490
4963
|
async (params) => {
|
|
4491
4964
|
try {
|
|
@@ -4500,11 +4973,11 @@ function registerAllTools(server) {
|
|
|
4500
4973
|
"lsp_query",
|
|
4501
4974
|
"Jump to definition, find references, inspect types, or rename symbols. Uses TypeScript Language Server \u2014 falls back to regex when LSP is unavailable.",
|
|
4502
4975
|
{
|
|
4503
|
-
filePath: z.string().min(1).describe("Path to the file containing the symbol"),
|
|
4504
|
-
line: z.number().min(0).describe("Line number (0-indexed)"),
|
|
4505
|
-
character: z.number().min(0).describe("Character position (0-indexed)"),
|
|
4506
|
-
action: z.enum(["def", "refs", "type", "rename"]).describe("Action: def
|
|
4507
|
-
newName: z.string().optional().describe("New name (required for action:'rename')")
|
|
4976
|
+
filePath: z.string({ invalid_type_error: 'lsp_query: "filePath" must be a string path.\n\n\u2705 Example: { filePath: "src/index.ts", line: 10, character: 5, action: "def" }' }).min(1).describe("Path to the file containing the symbol"),
|
|
4977
|
+
line: z.number({ invalid_type_error: '"line" must be a number (0-indexed). Line 0 = first line.' }).min(0).describe("Line number (0-indexed)"),
|
|
4978
|
+
character: z.number({ invalid_type_error: '"character" must be a number (0-indexed). Position within the line.' }).min(0).describe("Character position (0-indexed)"),
|
|
4979
|
+
action: z.enum(["def", "refs", "type", "rename"]).describe("Action: def=go to definition, refs=find references, type=get type info, rename=rename symbol"),
|
|
4980
|
+
newName: z.string().optional().describe("New name (required for action:'rename'). Example: { action: 'rename', newName: 'newFunctionName' }")
|
|
4508
4981
|
},
|
|
4509
4982
|
async (params) => {
|
|
4510
4983
|
try {
|
|
@@ -4616,6 +5089,22 @@ function registerAllTools(server) {
|
|
|
4616
5089
|
}
|
|
4617
5090
|
}
|
|
4618
5091
|
);
|
|
5092
|
+
server.tool(
|
|
5093
|
+
"kuma_guard",
|
|
5094
|
+
"Context safety net. Checks for anti-patterns (script patching, bash grep), loops, drift (edits without tests, unresolved failures). Run this after every few edits to stay on track.",
|
|
5095
|
+
{
|
|
5096
|
+
check: z.enum(["all", "anti-pattern", "loop", "drift", "context"]).optional().default("all").describe("Check type: all=everything, anti-pattern=script/grep detection, loop=loop detection, drift=edit vs test balance"),
|
|
5097
|
+
goal: z.string({ invalid_type_error: 'kuma_guard: "goal" must be a string describing what you are working on.\n\n\u2705 Example: { goal: "refactor auth module" }' }).optional().describe("Optional goal to check against. Example: 'refactor auth module'")
|
|
5098
|
+
},
|
|
5099
|
+
async (params) => {
|
|
5100
|
+
try {
|
|
5101
|
+
const result = await handleKumaGuard(params);
|
|
5102
|
+
return { content: [{ type: "text", text: result }] };
|
|
5103
|
+
} catch (err) {
|
|
5104
|
+
return { content: [{ type: "text", text: `Error in kuma_guard: ${err}` }], isError: true };
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
);
|
|
4619
5108
|
server.tool(
|
|
4620
5109
|
"static_analysis",
|
|
4621
5110
|
"Runs available linters/checkers (ESLint, TypeScript, Prettier, Ruff) and parses output into structured results. Auto-detects tools from project config.",
|
|
@@ -4634,7 +5123,827 @@ function registerAllTools(server) {
|
|
|
4634
5123
|
}
|
|
4635
5124
|
}
|
|
4636
5125
|
);
|
|
4637
|
-
|
|
5126
|
+
server.tool(
|
|
5127
|
+
"kuma_context",
|
|
5128
|
+
"Context snapshot manager. Save a snapshot of current project state (modified files, errors, git diff) or list previous snapshots. Run this before risky operations to have a restore point.",
|
|
5129
|
+
{
|
|
5130
|
+
action: z.enum(["save", "list"]).describe("Action: save=create a snapshot, list=show all snapshots"),
|
|
5131
|
+
goal: z.string().optional().describe("Optional goal to associate with the snapshot")
|
|
5132
|
+
},
|
|
5133
|
+
async (params) => {
|
|
5134
|
+
try {
|
|
5135
|
+
const result = await handleKumaContext(params);
|
|
5136
|
+
return { content: [{ type: "text", text: result }] };
|
|
5137
|
+
} catch (err) {
|
|
5138
|
+
return { content: [{ type: "text", text: `Error in kuma_context: ${err}` }], isError: true };
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
);
|
|
5142
|
+
console.error("[Manifest] Registered 17 tools.");
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
// src/cli/init.ts
|
|
5146
|
+
import fs15 from "fs";
|
|
5147
|
+
import path15 from "path";
|
|
5148
|
+
var ALL_CONFIG_TYPES = [
|
|
5149
|
+
"claude",
|
|
5150
|
+
"cursor",
|
|
5151
|
+
"windsurf",
|
|
5152
|
+
"copilot",
|
|
5153
|
+
"cline",
|
|
5154
|
+
"aider",
|
|
5155
|
+
"antigravity",
|
|
5156
|
+
"opencode",
|
|
5157
|
+
"codex",
|
|
5158
|
+
"qwen",
|
|
5159
|
+
"kiro",
|
|
5160
|
+
"openclaw",
|
|
5161
|
+
"codewhale"
|
|
5162
|
+
];
|
|
5163
|
+
var CONFIG_LABELS = {
|
|
5164
|
+
claude: "Claude Code (CLAUDE.md / plugin)",
|
|
5165
|
+
cursor: "Cursor (.cursor/rules/*.mdc)",
|
|
5166
|
+
windsurf: "Windsurf (.windsurfrules)",
|
|
5167
|
+
copilot: "GitHub Copilot Editor (AGENTS.md + Skill)",
|
|
5168
|
+
cline: "Cline (.clinerules/*.md)",
|
|
5169
|
+
aider: "Aider (CONVENTIONS.md via .aider.conf.yml)",
|
|
5170
|
+
antigravity: "Antigravity CLI (.agents/skills/)",
|
|
5171
|
+
opencode: "OpenCode (opencode.json)",
|
|
5172
|
+
codex: "Codex CLI (AGENTS.md + .codex/config.toml)",
|
|
5173
|
+
qwen: "Qwen Code (AGENTS.md + settings.json)",
|
|
5174
|
+
kiro: "Kiro (.kiro/steering/*.md)",
|
|
5175
|
+
openclaw: "OpenClaw (skills/)",
|
|
5176
|
+
codewhale: "CodeWhale (skills/ + .codewhale/mcp.json)"
|
|
5177
|
+
};
|
|
5178
|
+
function configFilePath(type) {
|
|
5179
|
+
switch (type) {
|
|
5180
|
+
case "claude":
|
|
5181
|
+
return "CLAUDE.md";
|
|
5182
|
+
case "cursor":
|
|
5183
|
+
return ".cursor/rules/kuma.mdc";
|
|
5184
|
+
case "windsurf":
|
|
5185
|
+
return ".windsurfrules";
|
|
5186
|
+
case "copilot":
|
|
5187
|
+
return "AGENTS.md";
|
|
5188
|
+
case "cline":
|
|
5189
|
+
return ".clinerules/kuma.md";
|
|
5190
|
+
case "aider":
|
|
5191
|
+
return "CONVENTIONS.md";
|
|
5192
|
+
case "antigravity":
|
|
5193
|
+
return ".agents/skills/kuma/SKILL.md";
|
|
5194
|
+
case "opencode":
|
|
5195
|
+
return "opencode.json";
|
|
5196
|
+
case "codex":
|
|
5197
|
+
return "AGENTS.md";
|
|
5198
|
+
case "qwen":
|
|
5199
|
+
return "AGENTS.md";
|
|
5200
|
+
case "kiro":
|
|
5201
|
+
return ".kiro/steering/kuma.md";
|
|
5202
|
+
case "openclaw":
|
|
5203
|
+
return "skills/kuma/SKILL.md";
|
|
5204
|
+
case "codewhale":
|
|
5205
|
+
return "skills/kuma/SKILL.md";
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
var CORE_RULES = [
|
|
5209
|
+
"## AI Agent Usage Guidelines",
|
|
5210
|
+
"",
|
|
5211
|
+
"Kuma MCP tools are available. Use them correctly:",
|
|
5212
|
+
"",
|
|
5213
|
+
"### Code Search",
|
|
5214
|
+
"- Use the **smart_grep** tool to search code - NOT bash grep/ripgrep manually",
|
|
5215
|
+
"- smart_grep returns line numbers + context, caches results, respects .gitignore",
|
|
5216
|
+
`- **Example:** smart_grep({ query: "function handleAuth", extensions: ['ts'] })`,
|
|
5217
|
+
"",
|
|
5218
|
+
"### Reading Code",
|
|
5219
|
+
"- Use the **smart_file_picker** tool to read files with smart chunking",
|
|
5220
|
+
"- For large files, use startLine/endLine to read specific ranges",
|
|
5221
|
+
'- **Example:** smart_file_picker({ filePath: "src/index.ts", chunkStrategy: "outline" })',
|
|
5222
|
+
'- **Example:** smart_file_picker({ filePath: "src/index.ts", startLine: 10, endLine: 30 })',
|
|
5223
|
+
"",
|
|
5224
|
+
"### Editing Code",
|
|
5225
|
+
"- Use the **precise_diff_editor** tool to edit files (fuzzy matching + auto-backup)",
|
|
5226
|
+
"- DO NOT create Python/Node scripts to patch files; use precise_diff_editor directly",
|
|
5227
|
+
"- DO NOT use bash sed/cat/awk to modify source files",
|
|
5228
|
+
'- **Example:** precise_diff_editor({ filePath: "src/app.ts", edits: [{ searchBlock: "old code", replaceBlock: "new code" }] })',
|
|
5229
|
+
'- **Example:** precise_diff_editor({ filePath: "src/app.ts", dryRun: true, edits: [...] })',
|
|
5230
|
+
'- **Example:** precise_diff_editor({ filePath: "src/app.ts", action: "rollback" })',
|
|
5231
|
+
"",
|
|
5232
|
+
"### Creating Files",
|
|
5233
|
+
"- Use the **batch_file_writer** tool to create new files (up to 15 at once)",
|
|
5234
|
+
'- **Example:** batch_file_writer({ files: [{ filePath: "src/util.ts", content: "// code", instructions: "reason for creating" }] })',
|
|
5235
|
+
"",
|
|
5236
|
+
"### Running Tasks",
|
|
5237
|
+
"- Use the **execute_safe_test** tool for test/build/lint/typecheck",
|
|
5238
|
+
"- Always run typecheck after editing TypeScript files",
|
|
5239
|
+
'- **Example:** execute_safe_test({ task: "typecheck" })',
|
|
5240
|
+
'- **Example:** execute_safe_test({ task: "custom", customCommand: "npm run lint" })',
|
|
5241
|
+
"",
|
|
5242
|
+
"### Code Review",
|
|
5243
|
+
"- Use the **code_reviewer** tool after changes",
|
|
5244
|
+
"- Supports focus: correctness, security, performance, over-engineering",
|
|
5245
|
+
'- **Example:** code_reviewer({ focus: "security" })',
|
|
5246
|
+
'- **Example:** code_reviewer({ files: ["src/auth.ts"], format: "json" })',
|
|
5247
|
+
"",
|
|
5248
|
+
"### Git Operations",
|
|
5249
|
+
"- Use the **git_diff** tool for structured diff output",
|
|
5250
|
+
"- Use the **git_log** tool for commit history",
|
|
5251
|
+
"- **Example:** git_log({ maxCount: 5 })",
|
|
5252
|
+
"- **Example:** git_diff({ staged: true })",
|
|
5253
|
+
"",
|
|
5254
|
+
"### Session Awareness",
|
|
5255
|
+
"- Use the **kuma_reflect** tool to check on-track/drift/loops",
|
|
5256
|
+
"- Use the **kuma_guard** tool for deeper safety checks (anti-patterns, auto-detection)",
|
|
5257
|
+
"- Use the **get_session_memory** tool to recall session state",
|
|
5258
|
+
'- **Example:** kuma_reflect({ goal: "refactor auth" })',
|
|
5259
|
+
'- **Example:** kuma_guard({ check: "all", goal: "refactor auth" })',
|
|
5260
|
+
"",
|
|
5261
|
+
"### LSP / Code Intelligence",
|
|
5262
|
+
"- Use the **lsp_query** tool for go-to-definition, find references, type info",
|
|
5263
|
+
'- **Example:** lsp_query({ filePath: "src/index.ts", line: 5, character: 10, action: "def" })',
|
|
5264
|
+
'- **Example:** lsp_query({ filePath: "src/index.ts", line: 5, character: 10, action: "refs" })',
|
|
5265
|
+
"",
|
|
5266
|
+
"### Static Analysis",
|
|
5267
|
+
"- Use the **static_analysis** tool to run ESLint/TSC/Prettier/Ruff",
|
|
5268
|
+
'- **Example:** static_analysis({ tool: "eslint", autoFix: true })',
|
|
5269
|
+
"",
|
|
5270
|
+
"### Project Structure",
|
|
5271
|
+
"- Use the **project_structure** tool to see project layout",
|
|
5272
|
+
"- **Example:** project_structure({ depth: 2, folderOnly: true })",
|
|
5273
|
+
"",
|
|
5274
|
+
"### Write Memory",
|
|
5275
|
+
"- Use the **write_memory** tool to persist decisions and glossary",
|
|
5276
|
+
'- **Example:** write_memory({ topic: "decisions", content: "## Reason for using X" })',
|
|
5277
|
+
"",
|
|
5278
|
+
"### General Rules",
|
|
5279
|
+
"- When you error, READ the error carefully before acting",
|
|
5280
|
+
"- After 3+ edits without running tests, stop and verify",
|
|
5281
|
+
"- If a tool fails, check the message - don't retry blindly",
|
|
5282
|
+
"- Detect conventions first with the **project_conventions** tool",
|
|
5283
|
+
"- **Example:** project_conventions({ forceRescan: true })"
|
|
5284
|
+
].join("\n");
|
|
5285
|
+
var KUMA_CORE_INSTRUCTIONS = CORE_RULES;
|
|
5286
|
+
function claudeTemplate() {
|
|
5287
|
+
return [
|
|
5288
|
+
"# Kuma AI Agent Guidelines",
|
|
5289
|
+
"",
|
|
5290
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5291
|
+
"",
|
|
5292
|
+
"## Workflow Pipeline",
|
|
5293
|
+
"",
|
|
5294
|
+
"For best results:",
|
|
5295
|
+
"1. **project_conventions** - detect stack",
|
|
5296
|
+
"2. **smart_grep** / **smart_file_picker** - understand code",
|
|
5297
|
+
"3. **precise_diff_editor** / **batch_file_writer** - make changes",
|
|
5298
|
+
"4. **execute_safe_test** - verify (typecheck + test)",
|
|
5299
|
+
"5. **code_reviewer** - review changes"
|
|
5300
|
+
].join("\n");
|
|
5301
|
+
}
|
|
5302
|
+
function cursorRulesTemplate() {
|
|
5303
|
+
return [
|
|
5304
|
+
"---",
|
|
5305
|
+
"description: Kuma MCP tool usage rules for AI coding agents",
|
|
5306
|
+
"alwaysApply: true",
|
|
5307
|
+
"---",
|
|
5308
|
+
"",
|
|
5309
|
+
"You are an expert engineer. Kuma MCP tools are available.",
|
|
5310
|
+
"",
|
|
5311
|
+
"## Critical Rules",
|
|
5312
|
+
"",
|
|
5313
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5314
|
+
"",
|
|
5315
|
+
"## NEVER",
|
|
5316
|
+
"- Never create Python/Node scripts to patch code",
|
|
5317
|
+
"- Never use bash sed/cat/awk to edit source files",
|
|
5318
|
+
"- Never run git push/git commit through bash"
|
|
5319
|
+
].join("\n");
|
|
5320
|
+
}
|
|
5321
|
+
function windsurfRulesTemplate() {
|
|
5322
|
+
return [
|
|
5323
|
+
"# Windsurf Cascade Rules with Kuma",
|
|
5324
|
+
"",
|
|
5325
|
+
KUMA_CORE_INSTRUCTIONS
|
|
5326
|
+
].join("\n");
|
|
5327
|
+
}
|
|
5328
|
+
function copilotTemplate() {
|
|
5329
|
+
return [
|
|
5330
|
+
"## GitHub Copilot Editor",
|
|
5331
|
+
"",
|
|
5332
|
+
"Kuma MCP tools are available. Use them correctly:",
|
|
5333
|
+
"",
|
|
5334
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5335
|
+
"",
|
|
5336
|
+
"### Copilot Editor-Specific",
|
|
5337
|
+
"- Copilot Editor reads AGENTS.md at project root for persistent instructions",
|
|
5338
|
+
"- Configure MCP servers via VS Code settings (cmd+shift+P \u2192 Developer: Reload Window after adding Kuma)",
|
|
5339
|
+
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5340
|
+
].join("\n");
|
|
5341
|
+
}
|
|
5342
|
+
function clineRulesTemplate() {
|
|
5343
|
+
return [
|
|
5344
|
+
"---",
|
|
5345
|
+
"description: Kuma MCP tool usage rules for AI coding agents",
|
|
5346
|
+
"paths:",
|
|
5347
|
+
' - "*"',
|
|
5348
|
+
"---",
|
|
5349
|
+
"",
|
|
5350
|
+
KUMA_CORE_INSTRUCTIONS
|
|
5351
|
+
].join("\n");
|
|
5352
|
+
}
|
|
5353
|
+
function aiderTemplate() {
|
|
5354
|
+
return [
|
|
5355
|
+
"# Kuma MCP - Aider Coding Conventions",
|
|
5356
|
+
"",
|
|
5357
|
+
"These conventions are loaded by Aider via the `read:` field in .aider.conf.yml",
|
|
5358
|
+
"",
|
|
5359
|
+
KUMA_CORE_INSTRUCTIONS
|
|
5360
|
+
].join("\n");
|
|
5361
|
+
}
|
|
5362
|
+
function opencodeTemplate() {
|
|
5363
|
+
const config = {
|
|
5364
|
+
mcp: {
|
|
5365
|
+
kuma: {
|
|
5366
|
+
type: "local",
|
|
5367
|
+
command: ["npx", "-y", "@plumpslabs/kuma"],
|
|
5368
|
+
enabled: true
|
|
5369
|
+
}
|
|
5370
|
+
},
|
|
5371
|
+
instructions: ["CLAUDE.md"]
|
|
5372
|
+
};
|
|
5373
|
+
const header = [
|
|
5374
|
+
"// Generated by Kuma MCP - https://github.com/plumpslabs/kuma",
|
|
5375
|
+
"// OpenCode config with Kuma MCP tools. Edit opencode.json to customize.",
|
|
5376
|
+
""
|
|
5377
|
+
].join("\n");
|
|
5378
|
+
return header + JSON.stringify(config, null, 2) + "\n";
|
|
5379
|
+
}
|
|
5380
|
+
function codexTemplate() {
|
|
5381
|
+
return [
|
|
5382
|
+
"## Codex CLI (OpenAI)",
|
|
5383
|
+
"",
|
|
5384
|
+
"Kuma MCP tools are available. Use them correctly:",
|
|
5385
|
+
"",
|
|
5386
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5387
|
+
"",
|
|
5388
|
+
"### Codex-Specific",
|
|
5389
|
+
"- Codex uses cascading AGENTS.md files (global ~/.codex/AGENTS.md -> project AGENTS.md)",
|
|
5390
|
+
"- MCP config is in .codex/config.toml (auto-generated by kuma init)",
|
|
5391
|
+
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5392
|
+
].join("\n");
|
|
5393
|
+
}
|
|
5394
|
+
function codexConfigTomlTemplate() {
|
|
5395
|
+
return [
|
|
5396
|
+
"# Generated by Kuma MCP - https://github.com/plumpslabs/kuma",
|
|
5397
|
+
"# Kuma MCP server config for Codex CLI",
|
|
5398
|
+
"",
|
|
5399
|
+
"[mcp_servers.kuma]",
|
|
5400
|
+
'command = "npx"',
|
|
5401
|
+
'args = ["-y", "@plumpslabs/kuma"]',
|
|
5402
|
+
""
|
|
5403
|
+
].join("\n");
|
|
5404
|
+
}
|
|
5405
|
+
function qwenTemplate() {
|
|
5406
|
+
return [
|
|
5407
|
+
"## Qwen Code",
|
|
5408
|
+
"",
|
|
5409
|
+
"Kuma MCP tools are available. Use them correctly:",
|
|
5410
|
+
"",
|
|
5411
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5412
|
+
"",
|
|
5413
|
+
"### Qwen-Specific",
|
|
5414
|
+
"- Qwen reads AGENTS.md at project root for persistent instructions",
|
|
5415
|
+
"- MCP config is in settings.json (auto-generated by kuma init)",
|
|
5416
|
+
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5417
|
+
].join("\n");
|
|
5418
|
+
}
|
|
5419
|
+
function qwenSettingsTemplate() {
|
|
5420
|
+
const config = {
|
|
5421
|
+
mcpServers: {
|
|
5422
|
+
kuma: {
|
|
5423
|
+
command: "npx",
|
|
5424
|
+
args: ["-y", "@plumpslabs/kuma"],
|
|
5425
|
+
env: {}
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
};
|
|
5429
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
5430
|
+
}
|
|
5431
|
+
function kiroRulesTemplate() {
|
|
5432
|
+
return [
|
|
5433
|
+
"---",
|
|
5434
|
+
"name: kuma-mcp",
|
|
5435
|
+
"description: Kuma safety toolkit - use smart_grep for search, precise_diff_editor for edits",
|
|
5436
|
+
"inclusion: always",
|
|
5437
|
+
"---",
|
|
5438
|
+
"",
|
|
5439
|
+
"# Kuma MCP - Kiro Steering",
|
|
5440
|
+
"",
|
|
5441
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5442
|
+
"",
|
|
5443
|
+
"## Kiro-Specific",
|
|
5444
|
+
"- Kiro reads steering files from .kiro/steering/ for project instructions",
|
|
5445
|
+
"- Configure MCP servers via IDE settings or global Kiro config",
|
|
5446
|
+
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5447
|
+
].join("\n");
|
|
5448
|
+
}
|
|
5449
|
+
function openclawSkillTemplate() {
|
|
5450
|
+
return [
|
|
5451
|
+
"---",
|
|
5452
|
+
"name: kuma-mcp",
|
|
5453
|
+
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5454
|
+
"---",
|
|
5455
|
+
"",
|
|
5456
|
+
"# Kuma MCP - OpenClaw Skill",
|
|
5457
|
+
"",
|
|
5458
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5459
|
+
"",
|
|
5460
|
+
"## OpenClaw-Specific",
|
|
5461
|
+
"- OpenClaw loads skills/ from workspace root or ~/.openclaw/skills for global",
|
|
5462
|
+
"- Configure Kuma MCP server via ~/.openclaw/openclaw.json or agents standard",
|
|
5463
|
+
"- Use kuma_guard periodically to check for anti-patterns",
|
|
5464
|
+
"",
|
|
5465
|
+
"## Verification",
|
|
5466
|
+
"- After edits, run execute_safe_test to verify no breakage",
|
|
5467
|
+
"- Use code_reviewer for correctness/security review",
|
|
5468
|
+
"- Check kuma_reflect to confirm on-track"
|
|
5469
|
+
].join("\n");
|
|
5470
|
+
}
|
|
5471
|
+
function codewhaleTemplate() {
|
|
5472
|
+
return [
|
|
5473
|
+
"---",
|
|
5474
|
+
"name: kuma-mcp",
|
|
5475
|
+
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5476
|
+
"---",
|
|
5477
|
+
"",
|
|
5478
|
+
"# Kuma MCP - CodeWhale Skill",
|
|
5479
|
+
"",
|
|
5480
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5481
|
+
"",
|
|
5482
|
+
"## CodeWhale-Specific",
|
|
5483
|
+
"- CodeWhale loads SKILL.md from skills/ (workspace-local), .agents/skills/, or ~/.codewhale/skills/",
|
|
5484
|
+
"- MCP config is in ~/.codewhale/mcp.json or ~/.deepseek/mcp.json",
|
|
5485
|
+
"- Use kuma_guard periodically to check for anti-patterns",
|
|
5486
|
+
"",
|
|
5487
|
+
"## Verification",
|
|
5488
|
+
"- After edits, run execute_safe_test to verify no breakage",
|
|
5489
|
+
"- Use code_reviewer for correctness/security review",
|
|
5490
|
+
"- Check kuma_reflect to confirm on-track"
|
|
5491
|
+
].join("\n");
|
|
5492
|
+
}
|
|
5493
|
+
function antigravitySkillTemplate() {
|
|
5494
|
+
return [
|
|
5495
|
+
"---",
|
|
5496
|
+
"name: kuma-mcp",
|
|
5497
|
+
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5498
|
+
"---",
|
|
5499
|
+
"",
|
|
5500
|
+
"# Kuma MCP - Antigravity Skill",
|
|
5501
|
+
"",
|
|
5502
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5503
|
+
"",
|
|
5504
|
+
"## Verification",
|
|
5505
|
+
"- After edits, run execute_safe_test to verify no breakage",
|
|
5506
|
+
"- Use code_reviewer for correctness/security review",
|
|
5507
|
+
"- Check kuma_reflect to confirm on-track"
|
|
5508
|
+
].join("\n");
|
|
5509
|
+
}
|
|
5510
|
+
function antigravityMcpConfigTemplate() {
|
|
5511
|
+
const config = {
|
|
5512
|
+
mcpServers: {
|
|
5513
|
+
kuma: {
|
|
5514
|
+
command: "npx",
|
|
5515
|
+
args: ["-y", "@plumpslabs/kuma"],
|
|
5516
|
+
env: {}
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
};
|
|
5520
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
5521
|
+
}
|
|
5522
|
+
var TEMPLATES = {
|
|
5523
|
+
claude: claudeTemplate,
|
|
5524
|
+
cursor: cursorRulesTemplate,
|
|
5525
|
+
windsurf: windsurfRulesTemplate,
|
|
5526
|
+
copilot: copilotTemplate,
|
|
5527
|
+
cline: clineRulesTemplate,
|
|
5528
|
+
aider: aiderTemplate,
|
|
5529
|
+
antigravity: antigravitySkillTemplate,
|
|
5530
|
+
opencode: opencodeTemplate,
|
|
5531
|
+
codex: codexTemplate,
|
|
5532
|
+
qwen: qwenTemplate,
|
|
5533
|
+
kiro: kiroRulesTemplate,
|
|
5534
|
+
openclaw: openclawSkillTemplate,
|
|
5535
|
+
codewhale: codewhaleTemplate
|
|
5536
|
+
};
|
|
5537
|
+
var APPEND_SEPARATOR = "\n\n---\n_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_\n\n";
|
|
5538
|
+
function handleOpencodeSecondary(root, results) {
|
|
5539
|
+
const claudePath = path15.resolve(root, "CLAUDE.md");
|
|
5540
|
+
if (!fs15.existsSync(claudePath)) {
|
|
5541
|
+
try {
|
|
5542
|
+
fs15.writeFileSync(claudePath, [
|
|
5543
|
+
"# Kuma MCP - OpenCode Instructions",
|
|
5544
|
+
"",
|
|
5545
|
+
KUMA_CORE_INSTRUCTIONS
|
|
5546
|
+
].join("\n"), "utf-8");
|
|
5547
|
+
results.push({ type: "opencode", filePath: "CLAUDE.md", action: "created" });
|
|
5548
|
+
} catch (err) {
|
|
5549
|
+
results.push({
|
|
5550
|
+
type: "opencode",
|
|
5551
|
+
filePath: "CLAUDE.md",
|
|
5552
|
+
action: "error",
|
|
5553
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5554
|
+
});
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5557
|
+
}
|
|
5558
|
+
function handleCodexSecondary(root, results) {
|
|
5559
|
+
const tomlPath = path15.resolve(root, ".codex/config.toml");
|
|
5560
|
+
if (results.some((r) => r.filePath === ".codex/config.toml")) return;
|
|
5561
|
+
try {
|
|
5562
|
+
const dir = path15.dirname(tomlPath);
|
|
5563
|
+
if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
|
|
5564
|
+
if (fs15.existsSync(tomlPath)) {
|
|
5565
|
+
const existingContent = fs15.readFileSync(tomlPath, "utf-8");
|
|
5566
|
+
if (existingContent.includes("kuma")) {
|
|
5567
|
+
results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
|
|
5568
|
+
return;
|
|
5569
|
+
}
|
|
5570
|
+
fs15.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
|
|
5571
|
+
results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
|
|
5572
|
+
} else {
|
|
5573
|
+
fs15.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
|
|
5574
|
+
results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
|
|
5575
|
+
}
|
|
5576
|
+
} catch (err) {
|
|
5577
|
+
results.push({
|
|
5578
|
+
type: "codex",
|
|
5579
|
+
filePath: ".codex/config.toml",
|
|
5580
|
+
action: "error",
|
|
5581
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5582
|
+
});
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
function handleQwenSecondary(root, results) {
|
|
5586
|
+
const settingsPath = path15.resolve(root, "settings.json");
|
|
5587
|
+
if (results.some((r) => r.filePath === "settings.json")) return;
|
|
5588
|
+
try {
|
|
5589
|
+
if (fs15.existsSync(settingsPath)) {
|
|
5590
|
+
const existingContent = fs15.readFileSync(settingsPath, "utf-8");
|
|
5591
|
+
if (existingContent.includes("kuma")) {
|
|
5592
|
+
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5593
|
+
try {
|
|
5594
|
+
const parsed = JSON.parse(existingContent);
|
|
5595
|
+
parsed.mcpServers = parsed.mcpServers || {};
|
|
5596
|
+
if (!parsed.mcpServers.kuma) {
|
|
5597
|
+
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5598
|
+
fs15.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5599
|
+
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
5600
|
+
return;
|
|
5601
|
+
}
|
|
5602
|
+
} catch {
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
results.push({ type: "qwen", filePath: "settings.json", action: "skipped" });
|
|
5606
|
+
return;
|
|
5607
|
+
}
|
|
5608
|
+
try {
|
|
5609
|
+
const parsed = JSON.parse(existingContent);
|
|
5610
|
+
parsed.mcpServers = parsed.mcpServers || {};
|
|
5611
|
+
if (!parsed.mcpServers.kuma) {
|
|
5612
|
+
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5613
|
+
fs15.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5614
|
+
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
5615
|
+
}
|
|
5616
|
+
} catch {
|
|
5617
|
+
}
|
|
5618
|
+
} else {
|
|
5619
|
+
fs15.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
|
|
5620
|
+
results.push({ type: "qwen", filePath: "settings.json", action: "created" });
|
|
5621
|
+
}
|
|
5622
|
+
} catch (err) {
|
|
5623
|
+
results.push({
|
|
5624
|
+
type: "qwen",
|
|
5625
|
+
filePath: "settings.json",
|
|
5626
|
+
action: "error",
|
|
5627
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5628
|
+
});
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
var AGENTS_MD_TYPES = ["codex", "qwen", "copilot"];
|
|
5632
|
+
function getAgentsMdHeader() {
|
|
5633
|
+
return [
|
|
5634
|
+
"# Kuma MCP - Combined Agent Instructions",
|
|
5635
|
+
"",
|
|
5636
|
+
"This file contains instructions for AI coding agents that read AGENTS.md.",
|
|
5637
|
+
"Each section applies to a specific agent. Unused sections can be safely removed.",
|
|
5638
|
+
"",
|
|
5639
|
+
"---",
|
|
5640
|
+
"_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_",
|
|
5641
|
+
""
|
|
5642
|
+
].join("\n");
|
|
5643
|
+
}
|
|
5644
|
+
function getCombinedAgentsMd(selectedTypes) {
|
|
5645
|
+
const sections = [getAgentsMdHeader()];
|
|
5646
|
+
const agentOrder = ["codex", "qwen", "copilot"];
|
|
5647
|
+
for (const t of agentOrder) {
|
|
5648
|
+
if (selectedTypes.has(t)) {
|
|
5649
|
+
sections.push(TEMPLATES[t]());
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
return sections.join("\n\n---\n\n");
|
|
5653
|
+
}
|
|
5654
|
+
function handleAntigravityMcpConfig(root, results) {
|
|
5655
|
+
const mcpPath = path15.resolve(root, ".agents/mcp_config.json");
|
|
5656
|
+
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
5657
|
+
try {
|
|
5658
|
+
const mcpDir = path15.dirname(mcpPath);
|
|
5659
|
+
if (fs15.existsSync(mcpPath)) {
|
|
5660
|
+
const existingContent = fs15.readFileSync(mcpPath, "utf-8");
|
|
5661
|
+
if (existingContent.includes("kuma")) {
|
|
5662
|
+
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5663
|
+
const trimmed = existingContent.trimEnd();
|
|
5664
|
+
if (trimmed.endsWith("}")) {
|
|
5665
|
+
const updated = trimmed.slice(0, -1).trimEnd() + ',\n "_kuma_note": "Kuma MCP - Generated by kuma init"\n}\n';
|
|
5666
|
+
fs15.writeFileSync(mcpPath, updated, "utf-8");
|
|
5667
|
+
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
return;
|
|
5671
|
+
}
|
|
5672
|
+
const parsed = JSON.parse(existingContent);
|
|
5673
|
+
parsed.mcpServers = parsed.mcpServers || {};
|
|
5674
|
+
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5675
|
+
fs15.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5676
|
+
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5677
|
+
} else {
|
|
5678
|
+
if (!fs15.existsSync(mcpDir)) fs15.mkdirSync(mcpDir, { recursive: true });
|
|
5679
|
+
fs15.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
5680
|
+
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
|
|
5681
|
+
}
|
|
5682
|
+
} catch (err) {
|
|
5683
|
+
results.push({
|
|
5684
|
+
type: "antigravity",
|
|
5685
|
+
filePath: ".agents/mcp_config.json",
|
|
5686
|
+
action: "error",
|
|
5687
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5688
|
+
});
|
|
5689
|
+
}
|
|
5690
|
+
}
|
|
5691
|
+
function handleAiderSecondary(root, results) {
|
|
5692
|
+
const ymlPath = path15.resolve(root, ".aider.conf.yml");
|
|
5693
|
+
if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
|
|
5694
|
+
try {
|
|
5695
|
+
const conventionsRef = "read: CONVENTIONS.md";
|
|
5696
|
+
if (fs15.existsSync(ymlPath)) {
|
|
5697
|
+
const existingContent = fs15.readFileSync(ymlPath, "utf-8");
|
|
5698
|
+
if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
|
|
5699
|
+
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
|
|
5700
|
+
return;
|
|
5701
|
+
}
|
|
5702
|
+
const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
|
|
5703
|
+
fs15.writeFileSync(ymlPath, newContent, "utf-8");
|
|
5704
|
+
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
|
|
5705
|
+
} else {
|
|
5706
|
+
const content = [
|
|
5707
|
+
"# Generated by Kuma MCP - https://github.com/plumpslabs/kuma",
|
|
5708
|
+
"# Aider will read CONVENTIONS.md for coding conventions",
|
|
5709
|
+
"",
|
|
5710
|
+
conventionsRef,
|
|
5711
|
+
""
|
|
5712
|
+
].join("\n");
|
|
5713
|
+
fs15.writeFileSync(ymlPath, content, "utf-8");
|
|
5714
|
+
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
|
|
5715
|
+
}
|
|
5716
|
+
} catch (err) {
|
|
5717
|
+
results.push({
|
|
5718
|
+
type: "aider",
|
|
5719
|
+
filePath: ".aider.conf.yml",
|
|
5720
|
+
action: "error",
|
|
5721
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5722
|
+
});
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
function handleCopilotSecondary(root, results) {
|
|
5726
|
+
const skillPath = path15.resolve(root, ".github/skills/kuma/SKILL.md");
|
|
5727
|
+
if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
|
|
5728
|
+
try {
|
|
5729
|
+
const dir = path15.dirname(skillPath);
|
|
5730
|
+
const content = [
|
|
5731
|
+
"---",
|
|
5732
|
+
"name: kuma-mcp",
|
|
5733
|
+
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5734
|
+
"---",
|
|
5735
|
+
"",
|
|
5736
|
+
"# Kuma MCP - Copilot Editor Skill",
|
|
5737
|
+
"",
|
|
5738
|
+
KUMA_CORE_INSTRUCTIONS
|
|
5739
|
+
].join("\n");
|
|
5740
|
+
if (fs15.existsSync(skillPath)) {
|
|
5741
|
+
const existingContent = fs15.readFileSync(skillPath, "utf-8");
|
|
5742
|
+
if (existingContent.includes("kuma")) {
|
|
5743
|
+
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
|
|
5744
|
+
return;
|
|
5745
|
+
}
|
|
5746
|
+
fs15.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
|
|
5747
|
+
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
|
|
5748
|
+
} else {
|
|
5749
|
+
if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
|
|
5750
|
+
fs15.writeFileSync(skillPath, content, "utf-8");
|
|
5751
|
+
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
|
|
5752
|
+
}
|
|
5753
|
+
} catch (err) {
|
|
5754
|
+
results.push({
|
|
5755
|
+
type: "copilot",
|
|
5756
|
+
filePath: ".github/skills/kuma/SKILL.md",
|
|
5757
|
+
action: "error",
|
|
5758
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5759
|
+
});
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
function handleOpenclawSecondary(root, results) {
|
|
5763
|
+
const mcpPath = path15.resolve(root, ".agents/mcp_config.json");
|
|
5764
|
+
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
5765
|
+
try {
|
|
5766
|
+
const dir = path15.dirname(mcpPath);
|
|
5767
|
+
if (fs15.existsSync(mcpPath)) {
|
|
5768
|
+
const existingContent = fs15.readFileSync(mcpPath, "utf-8");
|
|
5769
|
+
if (existingContent.includes("kuma")) return;
|
|
5770
|
+
const parsed = JSON.parse(existingContent);
|
|
5771
|
+
parsed.mcpServers = parsed.mcpServers || {};
|
|
5772
|
+
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5773
|
+
fs15.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5774
|
+
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5775
|
+
} else {
|
|
5776
|
+
if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
|
|
5777
|
+
fs15.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
5778
|
+
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
|
|
5779
|
+
}
|
|
5780
|
+
} catch (err) {
|
|
5781
|
+
results.push({
|
|
5782
|
+
type: "openclaw",
|
|
5783
|
+
filePath: ".agents/mcp_config.json",
|
|
5784
|
+
action: "error",
|
|
5785
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5786
|
+
});
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
function handleCodewhaleSecondary(root, results) {
|
|
5790
|
+
const mcpPath = path15.resolve(root, ".codewhale/mcp.json");
|
|
5791
|
+
if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
|
|
5792
|
+
try {
|
|
5793
|
+
const dir = path15.dirname(mcpPath);
|
|
5794
|
+
if (fs15.existsSync(mcpPath)) {
|
|
5795
|
+
const existingContent = fs15.readFileSync(mcpPath, "utf-8");
|
|
5796
|
+
if (existingContent.includes("kuma")) return;
|
|
5797
|
+
const parsed = JSON.parse(existingContent);
|
|
5798
|
+
parsed.mcpServers = parsed.mcpServers || {};
|
|
5799
|
+
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5800
|
+
fs15.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5801
|
+
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
|
|
5802
|
+
} else {
|
|
5803
|
+
if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
|
|
5804
|
+
const config = {
|
|
5805
|
+
mcpServers: {
|
|
5806
|
+
kuma: {
|
|
5807
|
+
command: "npx",
|
|
5808
|
+
args: ["-y", "@plumpslabs/kuma"],
|
|
5809
|
+
env: {}
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
};
|
|
5813
|
+
fs15.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5814
|
+
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
|
|
5815
|
+
}
|
|
5816
|
+
} catch (err) {
|
|
5817
|
+
results.push({
|
|
5818
|
+
type: "codewhale",
|
|
5819
|
+
filePath: ".codewhale/mcp.json",
|
|
5820
|
+
action: "error",
|
|
5821
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5822
|
+
});
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
function runInit(types, projectRoot) {
|
|
5826
|
+
const root = projectRoot ?? getProjectRoot();
|
|
5827
|
+
const selected = types.length > 0 ? types : ALL_CONFIG_TYPES;
|
|
5828
|
+
const results = [];
|
|
5829
|
+
const selectedSet = new Set(selected);
|
|
5830
|
+
const agentsMdSelected = AGENTS_MD_TYPES.filter((t) => selectedSet.has(t));
|
|
5831
|
+
let agentsMdHandled = false;
|
|
5832
|
+
for (const type of selected) {
|
|
5833
|
+
const relativePath = configFilePath(type);
|
|
5834
|
+
const fullPath = path15.resolve(root, relativePath);
|
|
5835
|
+
const getTemplate = TEMPLATES[type];
|
|
5836
|
+
try {
|
|
5837
|
+
if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
|
|
5838
|
+
agentsMdHandled = true;
|
|
5839
|
+
const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
|
|
5840
|
+
if (fs15.existsSync(fullPath)) {
|
|
5841
|
+
const existingContent = fs15.readFileSync(fullPath, "utf-8");
|
|
5842
|
+
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5843
|
+
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
5844
|
+
} else {
|
|
5845
|
+
fs15.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
|
|
5846
|
+
results.push({ type, filePath: relativePath, action: "appended" });
|
|
5847
|
+
}
|
|
5848
|
+
} else {
|
|
5849
|
+
const dir = path15.dirname(fullPath);
|
|
5850
|
+
if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
|
|
5851
|
+
fs15.writeFileSync(fullPath, combinedContent, "utf-8");
|
|
5852
|
+
results.push({ type, filePath: relativePath, action: "created" });
|
|
5853
|
+
}
|
|
5854
|
+
if (selectedSet.has("codex")) handleCodexSecondary(root, results);
|
|
5855
|
+
if (selectedSet.has("qwen")) handleQwenSecondary(root, results);
|
|
5856
|
+
if (selectedSet.has("copilot")) handleCopilotSecondary(root, results);
|
|
5857
|
+
} else if (AGENTS_MD_TYPES.includes(type) && agentsMdHandled) {
|
|
5858
|
+
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
5859
|
+
continue;
|
|
5860
|
+
} else {
|
|
5861
|
+
const template = getTemplate();
|
|
5862
|
+
if (fs15.existsSync(fullPath)) {
|
|
5863
|
+
const existingContent = fs15.readFileSync(fullPath, "utf-8");
|
|
5864
|
+
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5865
|
+
if (type === "antigravity") {
|
|
5866
|
+
handleAntigravityMcpConfig(root, results);
|
|
5867
|
+
} else if (type === "openclaw") {
|
|
5868
|
+
handleOpenclawSecondary(root, results);
|
|
5869
|
+
} else if (type === "codewhale") {
|
|
5870
|
+
handleCodewhaleSecondary(root, results);
|
|
5871
|
+
}
|
|
5872
|
+
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
5873
|
+
continue;
|
|
5874
|
+
}
|
|
5875
|
+
const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
|
|
5876
|
+
fs15.writeFileSync(fullPath, newContent, "utf-8");
|
|
5877
|
+
results.push({ type, filePath: relativePath, action: "appended" });
|
|
5878
|
+
} else {
|
|
5879
|
+
const dir = path15.dirname(fullPath);
|
|
5880
|
+
if (!fs15.existsSync(dir)) {
|
|
5881
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
5882
|
+
}
|
|
5883
|
+
fs15.writeFileSync(fullPath, template, "utf-8");
|
|
5884
|
+
results.push({ type, filePath: relativePath, action: "created" });
|
|
5885
|
+
}
|
|
5886
|
+
if (type === "antigravity") {
|
|
5887
|
+
handleAntigravityMcpConfig(root, results);
|
|
5888
|
+
} else if (type === "openclaw") {
|
|
5889
|
+
handleOpenclawSecondary(root, results);
|
|
5890
|
+
} else if (type === "codewhale") {
|
|
5891
|
+
handleCodewhaleSecondary(root, results);
|
|
5892
|
+
} else if (type === "aider") {
|
|
5893
|
+
handleAiderSecondary(root, results);
|
|
5894
|
+
} else if (type === "opencode") {
|
|
5895
|
+
handleOpencodeSecondary(root, results);
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
} catch (err) {
|
|
5899
|
+
results.push({
|
|
5900
|
+
type,
|
|
5901
|
+
filePath: relativePath,
|
|
5902
|
+
action: "error",
|
|
5903
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5904
|
+
});
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5907
|
+
return results;
|
|
5908
|
+
}
|
|
5909
|
+
function formatInitResults(results) {
|
|
5910
|
+
const lines = [
|
|
5911
|
+
"\u{1F43B} **Kuma Init - AI Agent Config Generator**",
|
|
5912
|
+
""
|
|
5913
|
+
];
|
|
5914
|
+
for (const r of results) {
|
|
5915
|
+
const label = CONFIG_LABELS[r.type];
|
|
5916
|
+
switch (r.action) {
|
|
5917
|
+
case "created":
|
|
5918
|
+
lines.push(" \u2705 " + label);
|
|
5919
|
+
lines.push(" \u2192 Created: " + r.filePath);
|
|
5920
|
+
break;
|
|
5921
|
+
case "appended":
|
|
5922
|
+
lines.push(" \u2795 " + label);
|
|
5923
|
+
lines.push(" \u2192 Appended to: " + r.filePath);
|
|
5924
|
+
break;
|
|
5925
|
+
case "skipped":
|
|
5926
|
+
lines.push(" \u23ED " + label);
|
|
5927
|
+
lines.push(" \u2192 Skipped (already has Kuma): " + r.filePath);
|
|
5928
|
+
break;
|
|
5929
|
+
case "error":
|
|
5930
|
+
lines.push(" \u274C " + label);
|
|
5931
|
+
lines.push(" \u2192 Error: " + (r.error ?? "unknown"));
|
|
5932
|
+
break;
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
const created = results.filter((r) => r.action === "created").length;
|
|
5936
|
+
const appended = results.filter((r) => r.action === "appended").length;
|
|
5937
|
+
const skipped = results.filter((r) => r.action === "skipped").length;
|
|
5938
|
+
const errors = results.filter((r) => r.action === "error").length;
|
|
5939
|
+
lines.push(
|
|
5940
|
+
"",
|
|
5941
|
+
"\u{1F4CA} Summary: " + created + " created, " + appended + " appended, " + skipped + " skipped, " + errors + " errors",
|
|
5942
|
+
"",
|
|
5943
|
+
"\u{1F4A1} Config files teach your AI how to use Kuma tools.",
|
|
5944
|
+
"\u{1F4A1} Run again to generate additional config files anytime."
|
|
5945
|
+
);
|
|
5946
|
+
return lines.join("\n");
|
|
4638
5947
|
}
|
|
4639
5948
|
|
|
4640
5949
|
// src/index.ts
|
|
@@ -4642,7 +5951,92 @@ var SERVER_NAME = "kuma";
|
|
|
4642
5951
|
var SERVER_VERSION = JSON.parse(
|
|
4643
5952
|
readFileSync(new URL("../package.json", import.meta.url), "utf-8")
|
|
4644
5953
|
).version;
|
|
5954
|
+
function printHelp() {
|
|
5955
|
+
console.error(`
|
|
5956
|
+
\u{1F43B} Kuma v${SERVER_VERSION} \u2014 Zero-setup safety toolkit for AI coding agents
|
|
5957
|
+
|
|
5958
|
+
Usage:
|
|
5959
|
+
npx @plumpslabs/kuma Start MCP server (default)
|
|
5960
|
+
npx @plumpslabs/kuma init Generate AI agent config files
|
|
5961
|
+
npx @plumpslabs/kuma init --all Generate ALL config files
|
|
5962
|
+
npx @plumpslabs/kuma init --claude --cursor Generate specific files
|
|
5963
|
+
npx @plumpslabs/kuma init --help Show this help
|
|
5964
|
+
|
|
5965
|
+
Available config files:
|
|
5966
|
+
--claude CLAUDE.md (Claude Code)
|
|
5967
|
+
--cursor .cursor/rules/kuma.mdc (Cursor)
|
|
5968
|
+
--windsurf .windsurfrules (Windsurf)
|
|
5969
|
+
--copilot AGENTS.md + .github/skills/ (GitHub Copilot Editor)
|
|
5970
|
+
--cline .clinerules/kuma.md (Cline)
|
|
5971
|
+
--aider CONVENTIONS.md + .aider.conf.yml (Aider)
|
|
5972
|
+
--antigravity .agents/skills/kuma/SKILL.md (Antigravity CLI)
|
|
5973
|
+
--opencode opencode.json (OpenCode)
|
|
5974
|
+
--codex AGENTS.md + .codex/ (Codex CLI - OpenAI)
|
|
5975
|
+
--qwen AGENTS.md + settings.json (Qwen Code)
|
|
5976
|
+
--kiro .kiro/steering/kuma.md (Kiro)
|
|
5977
|
+
--openclaw skills/kuma/SKILL.md (OpenClaw)
|
|
5978
|
+
--codewhale skills/kuma/SKILL.md + .codewhale/ (CodeWhale)
|
|
5979
|
+
|
|
5980
|
+
If no flags specified, you'll be prompted to select files interactively.
|
|
5981
|
+
`);
|
|
5982
|
+
}
|
|
4645
5983
|
async function main() {
|
|
5984
|
+
const args = process.argv.slice(2);
|
|
5985
|
+
if (args[0] === "init") {
|
|
5986
|
+
const flags = args.slice(1);
|
|
5987
|
+
if (flags.includes("--help") || flags.includes("-h")) {
|
|
5988
|
+
printHelp();
|
|
5989
|
+
process.exit(0);
|
|
5990
|
+
}
|
|
5991
|
+
const requestedFlags = flags.filter((f) => f.startsWith("--"));
|
|
5992
|
+
let selectedTypes;
|
|
5993
|
+
if (requestedFlags.length === 0) {
|
|
5994
|
+
console.error("\u{1F43B} Kuma Init \u2014 AI Agent Config Generator");
|
|
5995
|
+
console.error("");
|
|
5996
|
+
console.error("Select config files to generate. Press Ctrl+C to skip.");
|
|
5997
|
+
console.error("");
|
|
5998
|
+
selectedTypes = await interactiveSelect();
|
|
5999
|
+
if (selectedTypes.length === 0) {
|
|
6000
|
+
console.error("\n\u26A0\uFE0F No files selected. Exiting.");
|
|
6001
|
+
process.exit(0);
|
|
6002
|
+
}
|
|
6003
|
+
} else {
|
|
6004
|
+
if (requestedFlags.includes("--all")) {
|
|
6005
|
+
selectedTypes = ALL_CONFIG_TYPES;
|
|
6006
|
+
} else {
|
|
6007
|
+
const flagToType = {
|
|
6008
|
+
"--claude": "claude",
|
|
6009
|
+
"--cursor": "cursor",
|
|
6010
|
+
"--windsurf": "windsurf",
|
|
6011
|
+
"--copilot": "copilot",
|
|
6012
|
+
"--cline": "cline",
|
|
6013
|
+
"--aider": "aider",
|
|
6014
|
+
"--antigravity": "antigravity",
|
|
6015
|
+
"--opencode": "opencode",
|
|
6016
|
+
"--codex": "codex",
|
|
6017
|
+
"--qwen": "qwen",
|
|
6018
|
+
"--kiro": "kiro",
|
|
6019
|
+
"--openclaw": "openclaw",
|
|
6020
|
+
"--codewhale": "codewhale"
|
|
6021
|
+
};
|
|
6022
|
+
selectedTypes = [];
|
|
6023
|
+
for (const flag of requestedFlags) {
|
|
6024
|
+
const type = flagToType[flag];
|
|
6025
|
+
if (type) {
|
|
6026
|
+
selectedTypes.push(type);
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
if (selectedTypes.length === 0) {
|
|
6030
|
+
console.error("\u26A0\uFE0F No valid flags provided. Use --help to see options.");
|
|
6031
|
+
process.exit(1);
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
const results = runInit(selectedTypes, process.cwd());
|
|
6036
|
+
const output = formatInitResults(results);
|
|
6037
|
+
console.log(output);
|
|
6038
|
+
process.exit(0);
|
|
6039
|
+
}
|
|
4646
6040
|
sessionMemory.init({
|
|
4647
6041
|
projectRoot: process.cwd(),
|
|
4648
6042
|
startTime: Date.now()
|
|
@@ -4653,7 +6047,6 @@ async function main() {
|
|
|
4653
6047
|
version: SERVER_VERSION
|
|
4654
6048
|
},
|
|
4655
6049
|
{
|
|
4656
|
-
// Capabilities declaration
|
|
4657
6050
|
capabilities: {
|
|
4658
6051
|
tools: {},
|
|
4659
6052
|
resources: {}
|
|
@@ -4672,6 +6065,66 @@ async function main() {
|
|
|
4672
6065
|
`[${SERVER_NAME}] Server connected via stdio. Waiting for requests...`
|
|
4673
6066
|
);
|
|
4674
6067
|
}
|
|
6068
|
+
function interactiveSelect() {
|
|
6069
|
+
const labels = [
|
|
6070
|
+
{ type: "claude", label: "1) Claude Code (CLAUDE.md)" },
|
|
6071
|
+
{ type: "cursor", label: "2) Cursor (.cursor/rules/kuma.mdc)" },
|
|
6072
|
+
{ type: "windsurf", label: "3) Windsurf (.windsurfrules)" },
|
|
6073
|
+
{ type: "copilot", label: "4) GitHub Copilot Editor (AGENTS.md + Skill)" },
|
|
6074
|
+
{ type: "cline", label: "5) Cline (.clinerules/kuma.md)" },
|
|
6075
|
+
{ type: "aider", label: "6) Aider (CONVENTIONS.md via .aider.conf.yml)" },
|
|
6076
|
+
{ type: "antigravity", label: "7) Antigravity CLI (.agents/skills/)" },
|
|
6077
|
+
{ type: "opencode", label: "8) OpenCode (opencode.json)" },
|
|
6078
|
+
{ type: "codex", label: "9) Codex CLI - OpenAI (AGENTS.md + .codex/config.toml)" },
|
|
6079
|
+
{ type: "qwen", label: "10) Qwen Code (AGENTS.md + settings.json)" },
|
|
6080
|
+
{ type: "kiro", label: "11) Kiro (.kiro/steering/kuma.md)" },
|
|
6081
|
+
{ type: "openclaw", label: "12) OpenClaw (skills/kuma/SKILL.md)" },
|
|
6082
|
+
{ type: "codewhale", label: "13) CodeWhale (skills/kuma/SKILL.md + .codewhale/mcp.json)" }
|
|
6083
|
+
];
|
|
6084
|
+
const rl = readline.createInterface({
|
|
6085
|
+
input: process.stdin,
|
|
6086
|
+
output: process.stderr
|
|
6087
|
+
});
|
|
6088
|
+
return new Promise((resolve) => {
|
|
6089
|
+
console.error("");
|
|
6090
|
+
for (const l of labels) {
|
|
6091
|
+
console.error(l.label);
|
|
6092
|
+
}
|
|
6093
|
+
console.error("");
|
|
6094
|
+
rl.question("Enter numbers separated by space (e.g. '1 3 5'), or 'all': ", (answer) => {
|
|
6095
|
+
rl.close();
|
|
6096
|
+
const input = answer.trim().toLowerCase();
|
|
6097
|
+
if (input === "all") {
|
|
6098
|
+
resolve(ALL_CONFIG_TYPES);
|
|
6099
|
+
return;
|
|
6100
|
+
}
|
|
6101
|
+
const nums = input.split(/\s+/).map(Number).filter((n) => n >= 1 && n <= 13);
|
|
6102
|
+
const typeMap = {
|
|
6103
|
+
1: "claude",
|
|
6104
|
+
2: "cursor",
|
|
6105
|
+
3: "windsurf",
|
|
6106
|
+
4: "copilot",
|
|
6107
|
+
5: "cline",
|
|
6108
|
+
6: "aider",
|
|
6109
|
+
7: "antigravity",
|
|
6110
|
+
8: "opencode",
|
|
6111
|
+
9: "codex",
|
|
6112
|
+
10: "qwen",
|
|
6113
|
+
11: "kiro",
|
|
6114
|
+
12: "openclaw",
|
|
6115
|
+
13: "codewhale"
|
|
6116
|
+
};
|
|
6117
|
+
const selected = [];
|
|
6118
|
+
for (const n of nums) {
|
|
6119
|
+
const t = typeMap[n];
|
|
6120
|
+
if (t && !selected.includes(t)) {
|
|
6121
|
+
selected.push(t);
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
6124
|
+
resolve(selected);
|
|
6125
|
+
});
|
|
6126
|
+
});
|
|
6127
|
+
}
|
|
4675
6128
|
main().catch((err) => {
|
|
4676
6129
|
console.error(`[${SERVER_NAME}] Fatal error:`, err);
|
|
4677
6130
|
process.exit(1);
|