@plumpslabs/kuma 2.0.5 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -15
- package/dist/index.js +2346 -798
- 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
|
|
|
@@ -74,6 +75,21 @@ function validateFilePath(filePath, projectRoot) {
|
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
try {
|
|
79
|
+
if (fs.existsSync(resolvedPath)) {
|
|
80
|
+
const realPath = fs.realpathSync(resolvedPath);
|
|
81
|
+
const normalizedRealPath = path.normalize(realPath).toLowerCase();
|
|
82
|
+
if (!normalizedRealPath.startsWith(normalizedRoot)) {
|
|
83
|
+
return {
|
|
84
|
+
valid: false,
|
|
85
|
+
error: new Error(
|
|
86
|
+
`PATH_TRAVERSAL: Symlink escape detected. Path "${filePath}" resolves to "${realPath}" which is outside project root "${resolvedRoot}".`
|
|
87
|
+
)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
77
93
|
if (normalizedPath.includes("node_modules")) {
|
|
78
94
|
return {
|
|
79
95
|
valid: true,
|
|
@@ -579,6 +595,10 @@ No unresolved issues.`;
|
|
|
579
595
|
});
|
|
580
596
|
return results.slice(0, limit);
|
|
581
597
|
}
|
|
598
|
+
getConventions() {
|
|
599
|
+
this.ensureInit();
|
|
600
|
+
return this.state.conventions;
|
|
601
|
+
}
|
|
582
602
|
pruneMemory() {
|
|
583
603
|
this.ensureInit();
|
|
584
604
|
this.state.toolCalls = this.state.toolCalls.slice(-3);
|
|
@@ -899,7 +919,11 @@ async function handleSmartFilePicker(params) {
|
|
|
899
919
|
const content = fs4.readFileSync(resolvedPath, "utf-8");
|
|
900
920
|
const lines = content.split("\n");
|
|
901
921
|
const totalLines = lines.length;
|
|
902
|
-
sessionMemory.recordToolCall("smart_file_picker", {
|
|
922
|
+
sessionMemory.recordToolCall("smart_file_picker", {
|
|
923
|
+
filePath,
|
|
924
|
+
chunkStrategy,
|
|
925
|
+
totalLines
|
|
926
|
+
});
|
|
903
927
|
if (startLine !== void 0 || endLine !== void 0) {
|
|
904
928
|
const start = startLine ?? 1;
|
|
905
929
|
const end = endLine ?? totalLines;
|
|
@@ -915,7 +939,13 @@ async function handleSmartFilePicker(params) {
|
|
|
915
939
|
case "smart":
|
|
916
940
|
return handleSmartStrategy(filePath, lines, totalLines);
|
|
917
941
|
default:
|
|
918
|
-
return formatOutput(
|
|
942
|
+
return formatOutput(
|
|
943
|
+
filePath,
|
|
944
|
+
lines.slice(0, CHUNK_THRESHOLD),
|
|
945
|
+
1,
|
|
946
|
+
totalLines,
|
|
947
|
+
true
|
|
948
|
+
);
|
|
919
949
|
}
|
|
920
950
|
} catch (err) {
|
|
921
951
|
return 'Error reading file "' + filePath + '": ' + (err instanceof Error ? err.message : String(err));
|
|
@@ -940,7 +970,8 @@ async function handleOutlineStrategy(filePath, lines, totalLines) {
|
|
|
940
970
|
const exportLines = [];
|
|
941
971
|
for (let i = 0; i < lines.length; i++) {
|
|
942
972
|
const line = lines[i].trim();
|
|
943
|
-
if (line.startsWith("//") || line.startsWith("#") || line.startsWith("/*") || line.startsWith("*"))
|
|
973
|
+
if (line.startsWith("//") || line.startsWith("#") || line.startsWith("/*") || line.startsWith("*"))
|
|
974
|
+
continue;
|
|
944
975
|
if (line.startsWith("import ") || line.startsWith("from ") || line.startsWith("require(")) {
|
|
945
976
|
importLines.push(line);
|
|
946
977
|
continue;
|
|
@@ -974,10 +1005,15 @@ async function handleSmartStrategy(filePath, lines, totalLines) {
|
|
|
974
1005
|
}
|
|
975
1006
|
if (headerEnd < tailStart) {
|
|
976
1007
|
smartLines.push({ line: -1, text: "" });
|
|
977
|
-
smartLines.push({
|
|
1008
|
+
smartLines.push({
|
|
1009
|
+
line: -1,
|
|
1010
|
+
text: " ... " + (tailStart - headerEnd) + " lines hidden ..."
|
|
1011
|
+
});
|
|
978
1012
|
smartLines.push({ line: -1, text: "" });
|
|
979
1013
|
}
|
|
980
|
-
const keyDeclarations = extractKeyDeclarations(
|
|
1014
|
+
const keyDeclarations = extractKeyDeclarations(
|
|
1015
|
+
lines.slice(headerEnd, tailStart)
|
|
1016
|
+
);
|
|
981
1017
|
for (const decl of keyDeclarations) {
|
|
982
1018
|
smartLines.push({ line: decl.line + 1 + headerEnd, text: decl.text });
|
|
983
1019
|
}
|
|
@@ -1020,7 +1056,9 @@ function extractKeyDeclarations(lines) {
|
|
|
1020
1056
|
for (let i = 0; i < lines.length; i++) {
|
|
1021
1057
|
const line = lines[i].trim();
|
|
1022
1058
|
if (!line || line.startsWith("//") || line.startsWith("#")) continue;
|
|
1023
|
-
if (/^(export\s+)?(async\s+)?(function|class|interface|type|enum|const|let|var|def)\s/.test(
|
|
1059
|
+
if (/^(export\s+)?(async\s+)?(function|class|interface|type|enum|const|let|var|def)\s/.test(
|
|
1060
|
+
line
|
|
1061
|
+
)) {
|
|
1024
1062
|
decls.push({ line: i, text: line.substring(0, 150) });
|
|
1025
1063
|
}
|
|
1026
1064
|
}
|
|
@@ -1576,140 +1614,565 @@ function formatBatchResult(results, totalRequested) {
|
|
|
1576
1614
|
}
|
|
1577
1615
|
|
|
1578
1616
|
// src/tools/safeTerminalExec.ts
|
|
1579
|
-
import
|
|
1580
|
-
|
|
1581
|
-
test: "npm test",
|
|
1582
|
-
build: "npm run build",
|
|
1583
|
-
lint: "npm run lint",
|
|
1584
|
-
typecheck: "npx tsc --noEmit"
|
|
1585
|
-
};
|
|
1586
|
-
var DANGEROUS_PATTERNS = [
|
|
1587
|
-
"rm -rf",
|
|
1588
|
-
"rm -fr",
|
|
1589
|
-
"del /f",
|
|
1590
|
-
"rd /s",
|
|
1591
|
-
"rmdir /s",
|
|
1592
|
-
"git push",
|
|
1593
|
-
"git commit",
|
|
1594
|
-
"npm publish",
|
|
1595
|
-
"npx publish",
|
|
1596
|
-
"yarn publish",
|
|
1597
|
-
"pnpm publish",
|
|
1598
|
-
"> /dev/sda",
|
|
1599
|
-
"format",
|
|
1600
|
-
"mkfs",
|
|
1601
|
-
"dd if=",
|
|
1602
|
-
":(){ :|:& };:",
|
|
1603
|
-
// fork bomb
|
|
1604
|
-
"curl ",
|
|
1605
|
-
"wget "
|
|
1606
|
-
];
|
|
1607
|
-
async function handleSafeTerminalExec(params) {
|
|
1608
|
-
const { task, customCommand, timeout = 60 } = params;
|
|
1609
|
-
if (task === "custom" && !customCommand) {
|
|
1610
|
-
return "Error: Task 'custom' requires the 'customCommand' parameter.";
|
|
1611
|
-
}
|
|
1612
|
-
let command;
|
|
1613
|
-
if (task === "custom") {
|
|
1614
|
-
command = customCommand;
|
|
1615
|
-
} else {
|
|
1616
|
-
command = TASK_COMMANDS[task];
|
|
1617
|
-
}
|
|
1618
|
-
const cbResult = circuitBreaker.check("safe_terminal_exec", { task, command });
|
|
1619
|
-
if (!cbResult.allowed) {
|
|
1620
|
-
return `\u26A0\uFE0F Circuit breaker: ${cbResult.reason}
|
|
1617
|
+
import fs8 from "fs";
|
|
1618
|
+
import path8 from "path";
|
|
1621
1619
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1620
|
+
// src/utils/conventionsDetector.ts
|
|
1621
|
+
import fs7 from "fs";
|
|
1622
|
+
import path7 from "path";
|
|
1623
|
+
var cachedConventions = null;
|
|
1624
|
+
async function detectConventions(forceRescan = false) {
|
|
1625
|
+
if (cachedConventions && !forceRescan) {
|
|
1626
|
+
return cachedConventions;
|
|
1628
1627
|
}
|
|
1629
1628
|
const projectRoot = getProjectRoot();
|
|
1629
|
+
const workspaces = detectWorkspaces(projectRoot);
|
|
1630
|
+
const conventions = {
|
|
1631
|
+
framework: detectFramework(projectRoot),
|
|
1632
|
+
projectType: detectProjectType(projectRoot),
|
|
1633
|
+
testRunner: detectTestRunner(projectRoot),
|
|
1634
|
+
styling: detectStyling(projectRoot),
|
|
1635
|
+
importAlias: detectImportAlias(projectRoot),
|
|
1636
|
+
lintRules: detectLintRules(projectRoot),
|
|
1637
|
+
packageManager: detectPackageManager(),
|
|
1638
|
+
moduleSystem: detectModuleSystem(projectRoot),
|
|
1639
|
+
language: detectLanguage(projectRoot),
|
|
1640
|
+
features: detectFeatures(projectRoot),
|
|
1641
|
+
isMonorepo: workspaces.length > 0,
|
|
1642
|
+
workspaces
|
|
1643
|
+
};
|
|
1644
|
+
cachedConventions = conventions;
|
|
1645
|
+
return conventions;
|
|
1646
|
+
}
|
|
1647
|
+
function detectFramework(root) {
|
|
1648
|
+
const pkg = readJsonSafe(path7.join(root, "package.json"));
|
|
1649
|
+
if (pkg) {
|
|
1650
|
+
const pkgDeps = pkg.dependencies ?? {};
|
|
1651
|
+
const pkgDevDeps = pkg.devDependencies ?? {};
|
|
1652
|
+
const deps = { ...pkgDeps, ...pkgDevDeps };
|
|
1653
|
+
if (deps.next) return "Next.js";
|
|
1654
|
+
if (deps["@remix-run/react"]) return "Remix";
|
|
1655
|
+
if (deps.gatsby) return "Gatsby";
|
|
1656
|
+
if (deps.nuxt || deps["@nuxt/core"]) return "Nuxt.js";
|
|
1657
|
+
if (deps.vue) return "Vue";
|
|
1658
|
+
if (deps.react) return "React";
|
|
1659
|
+
if (deps.svelte) return "Svelte";
|
|
1660
|
+
if (deps.astro) return "Astro";
|
|
1661
|
+
if (deps.solid || deps["solid-js"]) return "SolidJS";
|
|
1662
|
+
if (deps.qwik || deps["@builder.io/qwik"]) return "Qwik";
|
|
1663
|
+
if (deps.vite) return "Vite";
|
|
1664
|
+
if (deps["@nestjs/core"]) return "NestJS";
|
|
1665
|
+
if (deps.fastify) return "Fastify";
|
|
1666
|
+
if (deps.express) return "Express";
|
|
1667
|
+
if (deps.koa) return "Koa";
|
|
1668
|
+
if (deps.hono) return "Hono";
|
|
1669
|
+
if (deps["@hapi/hapi"]) return "Hapi";
|
|
1670
|
+
if (deps["@modelcontextprotocol/sdk"]) return "MCP Server";
|
|
1671
|
+
if (deps["@anthropic-ai/claude-agent-sdk"]) return "Claude Agent SDK";
|
|
1672
|
+
if (deps.commander) return "Commander CLI";
|
|
1673
|
+
if (deps.yargs) return "Yargs CLI";
|
|
1674
|
+
if (deps["@oclif/core"] || deps.oclif) return "Oclif CLI";
|
|
1675
|
+
if (deps.ink) return "Ink (React CLI)";
|
|
1676
|
+
if (pkg.bin) return "CLI Tool";
|
|
1677
|
+
if (pkg.main || pkg.exports) return "Library";
|
|
1678
|
+
}
|
|
1679
|
+
const subFrameworks = scanSubProjectFrameworks(root);
|
|
1680
|
+
if (subFrameworks.length > 0) {
|
|
1681
|
+
const freq = /* @__PURE__ */ new Map();
|
|
1682
|
+
for (const fw of subFrameworks) {
|
|
1683
|
+
freq.set(fw, (freq.get(fw) ?? 0) + 1);
|
|
1684
|
+
}
|
|
1685
|
+
const [topFw] = [...freq.entries()].sort((a, b) => b[1] - a[1])[0] ?? [];
|
|
1686
|
+
if (topFw) return `${topFw} (monorepo)`;
|
|
1687
|
+
}
|
|
1688
|
+
return "unknown";
|
|
1689
|
+
}
|
|
1690
|
+
function scanSubProjectFrameworks(root) {
|
|
1691
|
+
const frameworks = [];
|
|
1630
1692
|
try {
|
|
1631
|
-
|
|
1632
|
-
const
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1693
|
+
const entries = fs7.readdirSync(root, { withFileTypes: true });
|
|
1694
|
+
for (const entry of entries) {
|
|
1695
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
1696
|
+
const subDir = path7.join(root, entry.name);
|
|
1697
|
+
const subPkg = readJsonSafe(path7.join(subDir, "package.json"));
|
|
1698
|
+
if (!subPkg) continue;
|
|
1699
|
+
const subDeps = { ...subPkg.dependencies ?? {}, ...subPkg.devDependencies ?? {} };
|
|
1700
|
+
if (subDeps.next) frameworks.push("Next.js");
|
|
1701
|
+
else if (subDeps.react) frameworks.push("React");
|
|
1702
|
+
else if (subDeps.vue) frameworks.push("Vue");
|
|
1703
|
+
else if (subDeps.svelte) frameworks.push("Svelte");
|
|
1704
|
+
else if (subDeps.astro) frameworks.push("Astro");
|
|
1705
|
+
else if (subDeps["@nestjs/core"]) frameworks.push("NestJS");
|
|
1706
|
+
else if (subDeps.express) frameworks.push("Express");
|
|
1707
|
+
else if (subDeps.hono) frameworks.push("Hono");
|
|
1708
|
+
else if (subDeps.vite) frameworks.push("Vite");
|
|
1709
|
+
else if (subDeps["@modelcontextprotocol/sdk"]) frameworks.push("MCP Server");
|
|
1636
1710
|
}
|
|
1637
|
-
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1711
|
+
} catch {
|
|
1712
|
+
}
|
|
1713
|
+
return frameworks;
|
|
1714
|
+
}
|
|
1715
|
+
function detectProjectType(root) {
|
|
1716
|
+
const pkg = readJsonSafe(path7.join(root, "package.json"));
|
|
1717
|
+
if (pkg) {
|
|
1718
|
+
const pkgDeps = pkg.dependencies ?? {};
|
|
1719
|
+
const pkgDevDeps = pkg.devDependencies ?? {};
|
|
1720
|
+
const deps = { ...pkgDeps, ...pkgDevDeps };
|
|
1721
|
+
if (deps["@modelcontextprotocol/sdk"]) return "mcp-server";
|
|
1722
|
+
if (deps.next || deps.react || deps.vue || deps.svelte || deps.astro || deps.gatsby || deps.nuxt || deps["@remix-run/react"] || deps.solid) {
|
|
1723
|
+
return "web-app";
|
|
1643
1724
|
}
|
|
1644
|
-
|
|
1725
|
+
if (deps.express || deps.fastify || deps.koa || deps.hono || deps["@nestjs/core"] || deps["@hapi/hapi"]) {
|
|
1726
|
+
return "backend";
|
|
1727
|
+
}
|
|
1728
|
+
if (deps.commander || deps.yargs || deps["@oclif/core"] || deps.oclif || deps.ink) {
|
|
1729
|
+
return "cli";
|
|
1730
|
+
}
|
|
1731
|
+
if (pkg.bin) return "cli";
|
|
1732
|
+
if (pkg.main || pkg.exports) return "library";
|
|
1645
1733
|
}
|
|
1734
|
+
const subFrameworks = scanSubProjectFrameworks(root);
|
|
1735
|
+
if (subFrameworks.length > 0) return "web-app";
|
|
1736
|
+
return "unknown";
|
|
1646
1737
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1738
|
+
function detectWorkspaces(root) {
|
|
1739
|
+
const results = [];
|
|
1740
|
+
const pkg = readJsonSafe(path7.join(root, "package.json"));
|
|
1741
|
+
const pnpmWorkspace = readYamlLite(path7.join(root, "pnpm-workspace.yaml"));
|
|
1742
|
+
const patterns = /* @__PURE__ */ new Set();
|
|
1743
|
+
const pkgWorkspaces = pkg?.workspaces;
|
|
1744
|
+
if (Array.isArray(pkgWorkspaces)) {
|
|
1745
|
+
for (const p of pkgWorkspaces) if (typeof p === "string") patterns.add(p);
|
|
1746
|
+
} else if (pkgWorkspaces && typeof pkgWorkspaces === "object") {
|
|
1747
|
+
const packages = pkgWorkspaces.packages;
|
|
1748
|
+
if (Array.isArray(packages)) {
|
|
1749
|
+
for (const p of packages) if (typeof p === "string") patterns.add(p);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (Array.isArray(pnpmWorkspace?.packages)) {
|
|
1753
|
+
for (const p of pnpmWorkspace.packages) if (typeof p === "string") patterns.add(p);
|
|
1754
|
+
}
|
|
1755
|
+
patterns.add("apps/*");
|
|
1756
|
+
patterns.add("packages/*");
|
|
1757
|
+
patterns.add("services/*");
|
|
1758
|
+
for (const pattern of patterns) {
|
|
1759
|
+
const match = pattern.match(/^([^*]+)\/\*$/);
|
|
1760
|
+
if (!match) continue;
|
|
1761
|
+
const dir = path7.join(root, match[1]);
|
|
1762
|
+
if (!fs7.existsSync(dir)) continue;
|
|
1763
|
+
let entries = [];
|
|
1764
|
+
try {
|
|
1765
|
+
entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
1766
|
+
} catch {
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
for (const entry of entries) {
|
|
1770
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
1771
|
+
const pkgPath = path7.join(dir, entry.name, "package.json");
|
|
1772
|
+
const subPkg = readJsonSafe(pkgPath);
|
|
1773
|
+
if (!subPkg) continue;
|
|
1774
|
+
const workspacePath = path7.join(dir, entry.name);
|
|
1775
|
+
results.push({
|
|
1776
|
+
path: path7.relative(root, workspacePath),
|
|
1777
|
+
name: subPkg.name ?? entry.name,
|
|
1778
|
+
framework: detectFramework(workspacePath),
|
|
1779
|
+
packageManager: detectPackageManagerForDir(workspacePath)
|
|
1685
1780
|
});
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1784
|
+
return results.filter((w) => {
|
|
1785
|
+
if (seen.has(w.path)) return false;
|
|
1786
|
+
seen.add(w.path);
|
|
1787
|
+
return true;
|
|
1691
1788
|
});
|
|
1692
1789
|
}
|
|
1693
|
-
function
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1790
|
+
function readYamlLite(filePath) {
|
|
1791
|
+
try {
|
|
1792
|
+
if (!fs7.existsSync(filePath)) return null;
|
|
1793
|
+
const text = fs7.readFileSync(filePath, "utf-8");
|
|
1794
|
+
const packages = [];
|
|
1795
|
+
let inPackages = false;
|
|
1796
|
+
for (const rawLine of text.split("\n")) {
|
|
1797
|
+
const line = rawLine.replace(/#.*$/, "").trimEnd();
|
|
1798
|
+
if (/^packages:\s*$/.test(line)) {
|
|
1799
|
+
inPackages = true;
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
if (inPackages) {
|
|
1803
|
+
const m = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/);
|
|
1804
|
+
if (m) packages.push(m[1]);
|
|
1805
|
+
else if (/^\S/.test(line)) inPackages = false;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return { packages };
|
|
1809
|
+
} catch {
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
function detectTestRunner(root) {
|
|
1814
|
+
const pkg = readJsonSafe(path7.join(root, "package.json"));
|
|
1815
|
+
if (!pkg) return "unknown";
|
|
1816
|
+
const pkgDeps2 = pkg.dependencies ?? {};
|
|
1817
|
+
const pkgDevDeps2 = pkg.devDependencies ?? {};
|
|
1818
|
+
const deps = { ...pkgDeps2, ...pkgDevDeps2 };
|
|
1819
|
+
if (deps.vitest) return "Vitest";
|
|
1820
|
+
if (deps.jest) return "Jest";
|
|
1821
|
+
if (deps.mocha) return "Mocha";
|
|
1822
|
+
if (deps.ava) return "AVA";
|
|
1823
|
+
if (deps.cypress) return "Cypress";
|
|
1824
|
+
if (deps.playwright) return "Playwright";
|
|
1825
|
+
if (deps["@testing-library/react"]) return "React Testing Library";
|
|
1826
|
+
const scripts = pkg.scripts ?? {};
|
|
1827
|
+
if (scripts.test) {
|
|
1828
|
+
if (scripts.test.includes("vitest")) return "Vitest";
|
|
1829
|
+
if (scripts.test.includes("jest")) return "Jest";
|
|
1830
|
+
if (scripts.test.includes("mocha")) return "Mocha";
|
|
1831
|
+
}
|
|
1832
|
+
return "unknown";
|
|
1833
|
+
}
|
|
1834
|
+
function detectStyling(root) {
|
|
1835
|
+
const pkg = readJsonSafe(path7.join(root, "package.json"));
|
|
1836
|
+
if (!pkg) return "unknown";
|
|
1837
|
+
const pkgDeps3 = pkg.dependencies ?? {};
|
|
1838
|
+
const pkgDevDeps3 = pkg.devDependencies ?? {};
|
|
1839
|
+
const deps = { ...pkgDeps3, ...pkgDevDeps3 };
|
|
1840
|
+
if (deps.tailwindcss) return "Tailwind CSS";
|
|
1841
|
+
if (deps["styled-components"]) return "Styled Components";
|
|
1842
|
+
if (deps["@emotion/react"]) return "Emotion";
|
|
1843
|
+
if (deps.linaria) return "Linaria";
|
|
1844
|
+
if (deps.sass || deps["node-sass"]) return "SCSS";
|
|
1845
|
+
if (deps.less) return "Less";
|
|
1846
|
+
if (deps["@vanilla-extract/css"]) return "Vanilla Extract";
|
|
1847
|
+
if (deps.cssmodules || hasCSSModules(root)) return "CSS Modules";
|
|
1848
|
+
const srcDir = path7.join(root, "src");
|
|
1849
|
+
if (fs7.existsSync(srcDir)) {
|
|
1850
|
+
const cssFiles = findFiles(srcDir, [".css", ".scss", ".less"]);
|
|
1851
|
+
if (cssFiles.length > 0) return "Plain CSS/SCSS";
|
|
1852
|
+
}
|
|
1853
|
+
return "unknown";
|
|
1854
|
+
}
|
|
1855
|
+
function detectImportAlias(root) {
|
|
1856
|
+
const tsconfig = readJsonSafe(path7.join(root, "tsconfig.json"));
|
|
1857
|
+
const tsconfigPaths = tsconfig?.compilerOptions;
|
|
1858
|
+
if (tsconfigPaths?.paths) {
|
|
1859
|
+
const paths = tsconfigPaths.paths;
|
|
1860
|
+
const alias = Object.keys(paths)[0];
|
|
1861
|
+
if (alias) return alias.replace("/*", "");
|
|
1862
|
+
}
|
|
1863
|
+
const jsconfig = readJsonSafe(path7.join(root, "jsconfig.json"));
|
|
1864
|
+
const jsconfigPaths = jsconfig?.compilerOptions;
|
|
1865
|
+
if (jsconfigPaths?.paths) {
|
|
1866
|
+
const paths = jsconfigPaths.paths;
|
|
1867
|
+
const alias = Object.keys(paths)[0];
|
|
1868
|
+
if (alias) return alias.replace("/*", "");
|
|
1869
|
+
}
|
|
1870
|
+
return void 0;
|
|
1871
|
+
}
|
|
1872
|
+
function detectLintRules(root) {
|
|
1873
|
+
const rules = [];
|
|
1874
|
+
if (fs7.existsSync(path7.join(root, ".eslintrc"))) rules.push("ESLint (.eslintrc)");
|
|
1875
|
+
if (fs7.existsSync(path7.join(root, ".eslintrc.js"))) rules.push("ESLint (.eslintrc.js)");
|
|
1876
|
+
if (fs7.existsSync(path7.join(root, ".eslintrc.json"))) rules.push("ESLint (.eslintrc.json)");
|
|
1877
|
+
if (fs7.existsSync(path7.join(root, "eslint.config.js"))) rules.push("ESLint (flat config)");
|
|
1878
|
+
if (fs7.existsSync(path7.join(root, ".prettierrc"))) rules.push("Prettier");
|
|
1879
|
+
if (fs7.existsSync(path7.join(root, ".prettierrc.json"))) rules.push("Prettier");
|
|
1880
|
+
if (fs7.existsSync(path7.join(root, ".prettierrc.js"))) rules.push("Prettier");
|
|
1881
|
+
if (fs7.existsSync(path7.join(root, ".stylelintrc"))) rules.push("StyleLint");
|
|
1882
|
+
if (fs7.existsSync(path7.join(root, ".stylelintrc.json"))) rules.push("StyleLint");
|
|
1883
|
+
return rules;
|
|
1884
|
+
}
|
|
1885
|
+
function detectPackageManager() {
|
|
1886
|
+
const root = getProjectRoot();
|
|
1887
|
+
return detectPackageManagerForDir(root);
|
|
1888
|
+
}
|
|
1889
|
+
function detectPackageManagerForDir(dir) {
|
|
1890
|
+
const root = getProjectRoot();
|
|
1891
|
+
let currentDir = path7.resolve(dir);
|
|
1892
|
+
while (currentDir.startsWith(root)) {
|
|
1893
|
+
if (fs7.existsSync(path7.join(currentDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
1894
|
+
if (fs7.existsSync(path7.join(currentDir, "yarn.lock"))) return "yarn";
|
|
1895
|
+
if (fs7.existsSync(path7.join(currentDir, "package-lock.json"))) return "npm";
|
|
1896
|
+
if (fs7.existsSync(path7.join(currentDir, "bun.lockb"))) return "bun";
|
|
1897
|
+
if (currentDir === root) break;
|
|
1898
|
+
currentDir = path7.dirname(currentDir);
|
|
1899
|
+
}
|
|
1900
|
+
return "npm";
|
|
1901
|
+
}
|
|
1902
|
+
function detectModuleSystem(root) {
|
|
1903
|
+
const pkg = readJsonSafe(path7.join(root, "package.json"));
|
|
1904
|
+
if (pkg?.type === "module") return "esm";
|
|
1905
|
+
const srcDir = path7.join(root, "src");
|
|
1906
|
+
if (fs7.existsSync(srcDir)) {
|
|
1907
|
+
const tsFiles = findFiles(srcDir, [".ts", ".tsx", ".js", ".jsx", ".mjs"]);
|
|
1908
|
+
for (const file of tsFiles.slice(0, 20)) {
|
|
1909
|
+
try {
|
|
1910
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
1911
|
+
if (content.includes("import ") || content.includes("export ")) return "esm";
|
|
1912
|
+
} catch {
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return "cjs";
|
|
1918
|
+
}
|
|
1919
|
+
function detectLanguage(root) {
|
|
1920
|
+
if (fs7.existsSync(path7.join(root, "tsconfig.json"))) return "typescript";
|
|
1921
|
+
const srcDir = path7.join(root, "src");
|
|
1922
|
+
if (fs7.existsSync(srcDir)) {
|
|
1923
|
+
const tsFiles = findFiles(srcDir, [".ts", ".tsx"]);
|
|
1924
|
+
if (tsFiles.length > 0) return "typescript";
|
|
1925
|
+
}
|
|
1926
|
+
return "javascript";
|
|
1927
|
+
}
|
|
1928
|
+
function detectFeatures(root) {
|
|
1929
|
+
const features = [];
|
|
1930
|
+
const pkg = readJsonSafe(path7.join(root, "package.json"));
|
|
1931
|
+
if (!pkg) return features;
|
|
1932
|
+
const pkgDeps4 = pkg.dependencies ?? {};
|
|
1933
|
+
const pkgDevDeps4 = pkg.devDependencies ?? {};
|
|
1934
|
+
const deps = { ...pkgDeps4, ...pkgDevDeps4 };
|
|
1935
|
+
if (deps["@prisma/client"] || deps.prisma) features.push("Prisma ORM");
|
|
1936
|
+
if (deps.typeorm) features.push("TypeORM");
|
|
1937
|
+
if (deps["drizzle-orm"]) features.push("Drizzle ORM");
|
|
1938
|
+
if (deps.graphql || deps["@graphql-tools"]) features.push("GraphQL");
|
|
1939
|
+
if (deps.trpc || deps["@trpc/client"]) features.push("tRPC");
|
|
1940
|
+
if (deps["socket.io"] || deps.ws) features.push("WebSockets");
|
|
1941
|
+
if (deps["@reduxjs/toolkit"] || deps.redux) features.push("Redux");
|
|
1942
|
+
if (deps.zustand) features.push("Zustand");
|
|
1943
|
+
if (deps["react-router"] || deps["react-router-dom"]) features.push("React Router");
|
|
1944
|
+
if (deps["next-auth"] || deps["next-auth/react"]) features.push("NextAuth");
|
|
1945
|
+
if (deps["@tanstack/react-query"]) features.push("React Query");
|
|
1946
|
+
if (deps.jest || deps.vitest) features.push("Unit Testing");
|
|
1947
|
+
if (deps.cypress || deps.playwright) features.push("E2E Testing");
|
|
1948
|
+
if (deps.storybook || deps["@storybook/react"]) features.push("Storybook");
|
|
1949
|
+
if (deps.i18next || deps["react-i18next"]) features.push("i18n");
|
|
1950
|
+
return features;
|
|
1951
|
+
}
|
|
1952
|
+
function readJsonSafe(filePath) {
|
|
1953
|
+
try {
|
|
1954
|
+
if (fs7.existsSync(filePath)) {
|
|
1955
|
+
return JSON.parse(fs7.readFileSync(filePath, "utf-8"));
|
|
1956
|
+
}
|
|
1957
|
+
} catch {
|
|
1958
|
+
}
|
|
1959
|
+
return null;
|
|
1960
|
+
}
|
|
1961
|
+
function hasCSSModules(root) {
|
|
1962
|
+
const srcDir = path7.join(root, "src");
|
|
1963
|
+
if (!fs7.existsSync(srcDir)) return false;
|
|
1964
|
+
const files = findFiles(srcDir, [".module.css", ".module.scss", ".module.less"]);
|
|
1965
|
+
return files.length > 0;
|
|
1966
|
+
}
|
|
1967
|
+
function findFiles(dir, extensions) {
|
|
1968
|
+
const results = [];
|
|
1969
|
+
try {
|
|
1970
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
1971
|
+
for (const entry of entries) {
|
|
1972
|
+
const fullPath = path7.join(dir, entry.name);
|
|
1973
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1974
|
+
results.push(...findFiles(fullPath, extensions));
|
|
1975
|
+
} else if (entry.isFile()) {
|
|
1976
|
+
if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
1977
|
+
results.push(fullPath);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
} catch {
|
|
1982
|
+
}
|
|
1983
|
+
return results;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// src/utils/processRunner.ts
|
|
1987
|
+
import { spawn } from "child_process";
|
|
1988
|
+
var DEFAULT_MAX_STDOUT = 5e3;
|
|
1989
|
+
var DEFAULT_MAX_STDERR = 2e3;
|
|
1990
|
+
var DEFAULT_TIMEOUT = 60;
|
|
1991
|
+
function spawnProcess(command, options) {
|
|
1992
|
+
const {
|
|
1993
|
+
cwd,
|
|
1994
|
+
timeoutSeconds = DEFAULT_TIMEOUT,
|
|
1995
|
+
useShell = false,
|
|
1996
|
+
maxStdout = DEFAULT_MAX_STDOUT,
|
|
1997
|
+
maxStderr = DEFAULT_MAX_STDERR
|
|
1998
|
+
} = options;
|
|
1999
|
+
return new Promise((resolve) => {
|
|
2000
|
+
const parts = command.split(" ");
|
|
2001
|
+
const cmd = parts[0];
|
|
2002
|
+
const args = parts.slice(1);
|
|
2003
|
+
const proc = spawn(cmd, args, {
|
|
2004
|
+
cwd,
|
|
2005
|
+
shell: useShell,
|
|
2006
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2007
|
+
});
|
|
2008
|
+
let stdout = "";
|
|
2009
|
+
let stderr = "";
|
|
2010
|
+
let timedOut = false;
|
|
2011
|
+
const timeoutId = setTimeout(() => {
|
|
2012
|
+
timedOut = true;
|
|
2013
|
+
proc.kill("SIGTERM");
|
|
2014
|
+
if (process.platform === "win32") {
|
|
2015
|
+
try {
|
|
2016
|
+
spawn("taskkill", ["/pid", String(proc.pid), "/f", "/t"], {
|
|
2017
|
+
stdio: "ignore"
|
|
2018
|
+
});
|
|
2019
|
+
} catch {
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}, timeoutSeconds * 1e3);
|
|
2023
|
+
proc.stdout?.on("data", (data) => {
|
|
2024
|
+
stdout += data.toString();
|
|
2025
|
+
});
|
|
2026
|
+
proc.stderr?.on("data", (data) => {
|
|
2027
|
+
stderr += data.toString();
|
|
2028
|
+
});
|
|
2029
|
+
proc.on("close", (code) => {
|
|
2030
|
+
clearTimeout(timeoutId);
|
|
2031
|
+
resolve({
|
|
2032
|
+
stdout: truncateOutput(stdout, maxStdout),
|
|
2033
|
+
stderr: truncateOutput(stderr, maxStderr),
|
|
2034
|
+
exitCode: code ?? -1,
|
|
2035
|
+
timedOut
|
|
2036
|
+
});
|
|
2037
|
+
});
|
|
2038
|
+
proc.on("error", () => {
|
|
2039
|
+
clearTimeout(timeoutId);
|
|
2040
|
+
resolve({
|
|
2041
|
+
stdout,
|
|
2042
|
+
stderr: `Failed to spawn process: ${cmd}`,
|
|
2043
|
+
exitCode: -1,
|
|
2044
|
+
timedOut: false
|
|
2045
|
+
});
|
|
2046
|
+
});
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
function spawnShell(command, options) {
|
|
2050
|
+
return spawnProcess(command, { ...options, useShell: true });
|
|
2051
|
+
}
|
|
2052
|
+
function truncateOutput(output, maxChars) {
|
|
2053
|
+
if (output.length <= maxChars) return output;
|
|
2054
|
+
return output.slice(0, maxChars) + `
|
|
2055
|
+
|
|
2056
|
+
[...truncated, ${output.length - maxChars} more characters]`;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/tools/safeTerminalExec.ts
|
|
2060
|
+
function buildTaskCommand(task, cwd) {
|
|
2061
|
+
const pm = detectPackageManagerForDir(cwd);
|
|
2062
|
+
const prefix = pm === "pnpm" ? "pnpm" : pm === "yarn" ? "yarn" : pm === "bun" ? "bun" : "npm";
|
|
2063
|
+
const npx = pm === "pnpm" ? "pnpm" : pm === "bun" ? "bunx" : "npx";
|
|
2064
|
+
switch (task) {
|
|
2065
|
+
case "test":
|
|
2066
|
+
return `${prefix} test`;
|
|
2067
|
+
case "build":
|
|
2068
|
+
return `${prefix} run build`;
|
|
2069
|
+
case "lint":
|
|
2070
|
+
return `${prefix} run lint`;
|
|
2071
|
+
case "typecheck":
|
|
2072
|
+
return `${npx} tsc --noEmit`;
|
|
2073
|
+
default:
|
|
2074
|
+
return `${prefix} test`;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
var DANGEROUS_PATTERNS = [
|
|
2078
|
+
"rm -rf",
|
|
2079
|
+
"rm -fr",
|
|
2080
|
+
"del /f",
|
|
2081
|
+
"rd /s",
|
|
2082
|
+
"rmdir /s",
|
|
2083
|
+
"git push",
|
|
2084
|
+
"git commit",
|
|
2085
|
+
"npm publish",
|
|
2086
|
+
"npx publish",
|
|
2087
|
+
"yarn publish",
|
|
2088
|
+
"pnpm publish",
|
|
2089
|
+
"> /dev/sda",
|
|
2090
|
+
"format",
|
|
2091
|
+
"mkfs",
|
|
2092
|
+
"dd if=",
|
|
2093
|
+
":(){ :|:& };:",
|
|
2094
|
+
"curl ",
|
|
2095
|
+
"wget "
|
|
2096
|
+
];
|
|
2097
|
+
async function handleSafeTerminalExec(params) {
|
|
2098
|
+
const { task, customCommand, timeout = 60, cwd: inputCwd, workspace } = params;
|
|
2099
|
+
if (task === "custom" && !customCommand) {
|
|
2100
|
+
return "Error: Task 'custom' requires the 'customCommand' parameter.";
|
|
2101
|
+
}
|
|
2102
|
+
const projectRoot = getProjectRoot();
|
|
2103
|
+
let workingDir = projectRoot;
|
|
2104
|
+
let resolvedFrom = "root";
|
|
2105
|
+
if (workspace) {
|
|
2106
|
+
const conventions = sessionMemory.getConventions();
|
|
2107
|
+
const workspaces = conventions?.workspaces;
|
|
2108
|
+
const matched = workspaces?.find((w) => w.name === workspace || w.path === workspace);
|
|
2109
|
+
if (matched) {
|
|
2110
|
+
workingDir = path8.resolve(projectRoot, matched.path);
|
|
2111
|
+
resolvedFrom = `workspace "${matched.name}" \u2192 ${matched.path}`;
|
|
2112
|
+
} else {
|
|
2113
|
+
return `\u26A0\uFE0F Workspace "${workspace}" not found. Run project_conventions first to detect workspaces, or use 'cwd' parameter with a direct path.
|
|
2114
|
+
|
|
2115
|
+
Available workspaces: ${workspaces?.map((w) => `"${w.name}" (${w.path})`).join(", ") || "none detected"}`;
|
|
2116
|
+
}
|
|
2117
|
+
} else if (inputCwd) {
|
|
2118
|
+
const resolved = path8.resolve(projectRoot, inputCwd);
|
|
2119
|
+
const normalizedResolved = path8.normalize(resolved).toLowerCase();
|
|
2120
|
+
const normalizedRoot = path8.normalize(projectRoot).toLowerCase();
|
|
2121
|
+
if (!normalizedResolved.startsWith(normalizedRoot)) {
|
|
2122
|
+
return `\u{1F6AB} BLOCKED: Path "${inputCwd}" resolves outside project root "${projectRoot}".`;
|
|
2123
|
+
}
|
|
2124
|
+
if (!fs8.existsSync(resolved)) {
|
|
2125
|
+
return `\u26A0\uFE0F Directory "${inputCwd}" does not exist at "${resolved}".`;
|
|
2126
|
+
}
|
|
2127
|
+
workingDir = resolved;
|
|
2128
|
+
resolvedFrom = `cwd: ${inputCwd}`;
|
|
2129
|
+
}
|
|
2130
|
+
const command = task === "custom" ? customCommand : buildTaskCommand(task, workingDir);
|
|
2131
|
+
const cbResult = circuitBreaker.check("safe_terminal_exec", { task, command });
|
|
2132
|
+
if (!cbResult.allowed) {
|
|
2133
|
+
return `\u26A0\uFE0F Circuit breaker: ${cbResult.reason}
|
|
2134
|
+
|
|
2135
|
+
Fix the code first before running the task again.`;
|
|
2136
|
+
}
|
|
2137
|
+
const dangerousPattern = DANGEROUS_PATTERNS.find((p) => command.toLowerCase().includes(p.toLowerCase()));
|
|
2138
|
+
if (dangerousPattern) {
|
|
2139
|
+
return `\u{1F6AB} BLOCKED: Command contains a dangerous pattern: "${dangerousPattern}".
|
|
2140
|
+
This command is not permitted.`;
|
|
2141
|
+
}
|
|
2142
|
+
try {
|
|
2143
|
+
sessionMemory.recordToolCall("execute_safe_test", { task, command, cwd: workingDir });
|
|
2144
|
+
const result = await spawnShell(command, {
|
|
2145
|
+
cwd: workingDir,
|
|
2146
|
+
timeoutSeconds: timeout
|
|
2147
|
+
});
|
|
2148
|
+
const output = formatExecResult(result, command, task, resolvedFrom);
|
|
2149
|
+
if (result.exitCode !== 0) {
|
|
2150
|
+
sessionMemory.addFailedFile(task, result.stderr || result.stdout);
|
|
2151
|
+
}
|
|
2152
|
+
return output;
|
|
2153
|
+
} catch (err) {
|
|
2154
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2155
|
+
if (errorMsg.toLowerCase().includes("timeout")) {
|
|
2156
|
+
return formatTimeoutResult(command, timeout);
|
|
2157
|
+
}
|
|
2158
|
+
return `Error running "${command}": ${errorMsg}`;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
function formatExecResult(result, command, task, resolvedFrom) {
|
|
2162
|
+
const status = result.exitCode === 0 ? "\u2705 PASS" : "\u274C FAIL";
|
|
2163
|
+
const lines = [
|
|
2164
|
+
`\u{1F4BB} ${status} \u2014 Task: ${task}`,
|
|
2165
|
+
`$ ${command}`,
|
|
2166
|
+
...resolvedFrom ? [`\u{1F4CD} ${resolvedFrom}`] : [],
|
|
2167
|
+
`Exit code: ${result.exitCode}`,
|
|
2168
|
+
`Duration: ${result.timedOut ? "TIMEOUT" : "completed"}`,
|
|
2169
|
+
""
|
|
2170
|
+
];
|
|
2171
|
+
if (result.stdout.trim()) {
|
|
2172
|
+
lines.push("\u{1F4E4} STDOUT:", "```", result.stdout, "```", "");
|
|
2173
|
+
}
|
|
2174
|
+
if (result.stderr.trim()) {
|
|
2175
|
+
lines.push("\u{1F4E4} STDERR:", "```", result.stderr, "```", "");
|
|
1713
2176
|
}
|
|
1714
2177
|
if (result.exitCode !== 0) {
|
|
1715
2178
|
lines.push(
|
|
@@ -1741,8 +2204,8 @@ function formatTimeoutResult(command, timeout) {
|
|
|
1741
2204
|
}
|
|
1742
2205
|
|
|
1743
2206
|
// src/agents/codeReviewer.ts
|
|
1744
|
-
import
|
|
1745
|
-
import
|
|
2207
|
+
import fs9 from "fs";
|
|
2208
|
+
import path9 from "path";
|
|
1746
2209
|
import { execSync } from "child_process";
|
|
1747
2210
|
var CODE_EXTENSIONS = [
|
|
1748
2211
|
".ts",
|
|
@@ -1774,7 +2237,7 @@ function getGitChangedFiles() {
|
|
|
1774
2237
|
if (filePath.startsWith('"') && filePath.endsWith('"')) {
|
|
1775
2238
|
filePath = filePath.substring(1, filePath.length - 1);
|
|
1776
2239
|
}
|
|
1777
|
-
const ext =
|
|
2240
|
+
const ext = path9.extname(filePath).toLowerCase();
|
|
1778
2241
|
if (CODE_EXTENSIONS.includes(ext)) files.push(filePath);
|
|
1779
2242
|
}
|
|
1780
2243
|
return files.slice(0, 10);
|
|
@@ -1811,7 +2274,7 @@ Pass an explicit 'files' array to review specific files.`;
|
|
|
1811
2274
|
continue;
|
|
1812
2275
|
}
|
|
1813
2276
|
const resolvedPath = validation.resolvedPath;
|
|
1814
|
-
if (!
|
|
2277
|
+
if (!fs9.existsSync(resolvedPath)) {
|
|
1815
2278
|
allIssues.push({
|
|
1816
2279
|
file: filePath,
|
|
1817
2280
|
line: 0,
|
|
@@ -1823,7 +2286,7 @@ Pass an explicit 'files' array to review specific files.`;
|
|
|
1823
2286
|
continue;
|
|
1824
2287
|
}
|
|
1825
2288
|
try {
|
|
1826
|
-
const content =
|
|
2289
|
+
const content = fs9.readFileSync(resolvedPath, "utf-8");
|
|
1827
2290
|
filesReviewed++;
|
|
1828
2291
|
checkGeneral(filePath, content, allIssues);
|
|
1829
2292
|
switch (focus) {
|
|
@@ -1917,7 +2380,7 @@ function isTestFile(filePath) {
|
|
|
1917
2380
|
return /\.(test|spec)\.[a-z]+$/i.test(filePath) || /__tests__/.test(filePath);
|
|
1918
2381
|
}
|
|
1919
2382
|
function isTsLike(filePath) {
|
|
1920
|
-
const ext =
|
|
2383
|
+
const ext = path9.extname(filePath).toLowerCase();
|
|
1921
2384
|
return ext === ".ts" || ext === ".tsx";
|
|
1922
2385
|
}
|
|
1923
2386
|
function checkGeneral(filePath, content, issues) {
|
|
@@ -2249,567 +2712,212 @@ function checkPerformance(filePath, content, issues) {
|
|
|
2249
2712
|
file: filePath,
|
|
2250
2713
|
line: lineNum,
|
|
2251
2714
|
severity: "info",
|
|
2252
|
-
rule: "perf/spread-in-loop",
|
|
2253
|
-
message: "Spread inside a loop is O(n)",
|
|
2254
|
-
suggestion: "Use push() or concat() to grow arrays in loops"
|
|
2255
|
-
});
|
|
2256
|
-
}
|
|
2257
|
-
if (/\b(readFileSync|writeFileSync|existsSync)\s*\(/.test(line) && /\b(for|while|forEach|map)\b/.test(lines[Math.max(0, i - 2)] + lines[Math.max(0, i - 1)] + lines[i])) {
|
|
2258
|
-
issues.push({
|
|
2259
|
-
file: filePath,
|
|
2260
|
-
line: lineNum,
|
|
2261
|
-
severity: "warning",
|
|
2262
|
-
rule: "perf/sync-io-in-loop",
|
|
2263
|
-
message: "Synchronous I/O inside a loop",
|
|
2264
|
-
suggestion: "Use the async variant and await in parallel where safe"
|
|
2265
|
-
});
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
function checkOverEngineering(filePath, content, issues) {
|
|
2270
|
-
const lines = content.split("\n");
|
|
2271
|
-
const text = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
2272
|
-
const classBodies = findBlockBodies(lines, /^\s*(export\s+)?(abstract\s+)?class\s+\w+/);
|
|
2273
|
-
for (const { body, nameLine, name } of classBodies) {
|
|
2274
|
-
const methodCount = (body.match(/^\s*(public|private|protected|static|async)?\s*\w+\s*\([^)]*\)\s*{/gm) || []).length;
|
|
2275
|
-
if (methodCount <= 1 && body.length > 0) {
|
|
2276
|
-
const trimmed = body.trim();
|
|
2277
|
-
const nonEmptyLines = trimmed ? trimmed.split("\n").filter((l) => l.trim()).length : 0;
|
|
2278
|
-
if (nonEmptyLines <= 3) {
|
|
2279
|
-
issues.push({
|
|
2280
|
-
file: filePath,
|
|
2281
|
-
line: nameLine,
|
|
2282
|
-
severity: "info",
|
|
2283
|
-
rule: "over-engineering/wrapper-class",
|
|
2284
|
-
message: `Class "${name}" has only 1 method \u2014 likely a wrapper that could be a plain function`,
|
|
2285
|
-
suggestion: "Replace with a standalone function or a utility module export"
|
|
2286
|
-
});
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
const factoryMatches = text.matchAll(/^\s*(export\s+)?function\s+(create\w+|make\w+|build\w+|factory\w+)\s*\(/gm);
|
|
2291
|
-
for (const match of factoryMatches) {
|
|
2292
|
-
const funcName = match[2];
|
|
2293
|
-
const body = extractBodyAfter(lines, match);
|
|
2294
|
-
const creationCount = (body.match(/\bnew\s+\w+/g) || []).length;
|
|
2295
|
-
if (creationCount <= 1) {
|
|
2296
|
-
issues.push({
|
|
2297
|
-
file: filePath,
|
|
2298
|
-
line: lines.findIndex((l) => l.includes(funcName)) + 1,
|
|
2299
|
-
severity: "warning",
|
|
2300
|
-
rule: "over-engineering/single-product-factory",
|
|
2301
|
-
message: `Factory "${funcName}" produces only 1 product \u2014 unnecessary abstraction`,
|
|
2302
|
-
suggestion: "Inline the construction or drop the factory pattern"
|
|
2303
|
-
});
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
const interfaceNames = [];
|
|
2307
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2308
|
-
const m = lines[i].match(/^\s*(export\s+)?interface\s+(\w+)/);
|
|
2309
|
-
if (m) interfaceNames.push(m[2]);
|
|
2310
|
-
}
|
|
2311
|
-
for (const iface of interfaceNames) {
|
|
2312
|
-
const implCount = (text.match(new RegExp(`implements\\s+.*\\b${iface}\\b`, "g")) || []).length;
|
|
2313
|
-
if (implCount <= 1) {
|
|
2314
|
-
const lineNum = lines.findIndex((l) => l.includes(`interface ${iface}`)) + 1;
|
|
2315
|
-
issues.push({
|
|
2316
|
-
file: filePath,
|
|
2317
|
-
line: lineNum,
|
|
2318
|
-
severity: "info",
|
|
2319
|
-
rule: "over-engineering/single-impl-interface",
|
|
2320
|
-
message: `Interface "${iface}" has only 1 implementation \u2014 unnecessary unless more planned`,
|
|
2321
|
-
suggestion: "Remove the interface or inline the type"
|
|
2322
|
-
});
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
const configObjPattern = /^\s*(export\s+)?(const|let|var)\s+(\w+)\s*[=:]\s*\{/;
|
|
2326
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2327
|
-
const cm = lines[i].match(configObjPattern);
|
|
2328
|
-
if (!cm || !/config|setting|option|default/i.test(cm[3])) continue;
|
|
2329
|
-
const configName = cm[3];
|
|
2330
|
-
const refs = (text.match(new RegExp(`\\b${configName}\\b`, "g")) || []).length;
|
|
2331
|
-
if (refs <= 2) {
|
|
2332
|
-
issues.push({
|
|
2333
|
-
file: filePath,
|
|
2334
|
-
line: i + 1,
|
|
2335
|
-
severity: "info",
|
|
2336
|
-
rule: "over-engineering/unused-config",
|
|
2337
|
-
message: `Config "${configName}" is referenced ${refs === 0 ? "nowhere" : `only ${refs - 1}x outside definition`}`,
|
|
2338
|
-
suggestion: refs === 0 ? "Remove the dead config" : "Inline the value or remove if unused elsewhere"
|
|
2339
|
-
});
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
const stdlibPatterns = [
|
|
2343
|
-
{ pattern: /function\s+\w+\s*\([^)]*\)\s*\{[^}]*fs\.readFileSync[^}]*\}/, rule: "over-engineering/hand-rolled-file-read", msg: "Hand-rolled file reader \u2014 use fs.readFileSync directly", suggestion: "Call fs.readFileSync directly instead of wrapping" },
|
|
2344
|
-
{ pattern: /function\s+\w+\s*\([^)]*\)\s*\{[^}]*crypto\.createHash[^}]*\}/, rule: "over-engineering/hand-rolled-hash", msg: "Hand-rolled hash \u2014 use crypto directly", suggestion: "Call crypto.createHash directly" },
|
|
2345
|
-
{ pattern: /function\s+uuid\s*\(|function\s+generateId\s*\(/, rule: "over-engineering/hand-rolled-uuid", msg: "Hand-rolled UUID \u2014 use crypto.randomUUID()", suggestion: "Replace with crypto.randomUUID() (Node 19+) or the uuid package" },
|
|
2346
|
-
{ pattern: /function\s+debounce\s*\(|function\s+throttle\s*\(/, rule: "over-engineering/hand-rolled-debounce", msg: "Hand-rolled debounce/throttle \u2014 utility lib or native", suggestion: "Use lodash.debounce or a 3-line inline version" }
|
|
2347
|
-
];
|
|
2348
|
-
for (const sp of stdlibPatterns) {
|
|
2349
|
-
const sl = text.match(sp.pattern);
|
|
2350
|
-
if (sl) {
|
|
2351
|
-
const matchIndex = sl.index ?? 0;
|
|
2352
|
-
const preText = text.substring(0, matchIndex);
|
|
2353
|
-
const lineNum = preText.split("\n").length;
|
|
2354
|
-
issues.push({
|
|
2355
|
-
file: filePath,
|
|
2356
|
-
line: lineNum,
|
|
2357
|
-
severity: "info",
|
|
2358
|
-
rule: sp.rule,
|
|
2359
|
-
message: sp.msg,
|
|
2360
|
-
suggestion: sp.suggestion
|
|
2715
|
+
rule: "perf/spread-in-loop",
|
|
2716
|
+
message: "Spread inside a loop is O(n)",
|
|
2717
|
+
suggestion: "Use push() or concat() to grow arrays in loops"
|
|
2361
2718
|
});
|
|
2362
2719
|
}
|
|
2363
|
-
|
|
2364
|
-
const exportDecls = [];
|
|
2365
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2366
|
-
const ed = lines[i].match(/^\s*export\s+(const|let|var|function|class)\s+(\w+)/);
|
|
2367
|
-
if (ed) exportDecls.push({ name: ed[2], line: i + 1 });
|
|
2368
|
-
}
|
|
2369
|
-
for (const decl of exportDecls) {
|
|
2370
|
-
const refCount = (text.match(new RegExp(`\\b${decl.name}\\b`, "g")) || []).length;
|
|
2371
|
-
const isReactComponent = /^[A-Z]/.test(decl.name) && /return\s+<|React\./.test(text);
|
|
2372
|
-
if (refCount <= 1 && decl.name !== "default" && !isReactComponent) {
|
|
2720
|
+
if (/\b(readFileSync|writeFileSync|existsSync)\s*\(/.test(line) && /\b(for|while|forEach|map)\b/.test(lines[Math.max(0, i - 2)] + lines[Math.max(0, i - 1)] + lines[i])) {
|
|
2373
2721
|
issues.push({
|
|
2374
2722
|
file: filePath,
|
|
2375
|
-
line:
|
|
2376
|
-
severity: "
|
|
2377
|
-
rule: "
|
|
2378
|
-
message:
|
|
2379
|
-
suggestion: "
|
|
2723
|
+
line: lineNum,
|
|
2724
|
+
severity: "warning",
|
|
2725
|
+
rule: "perf/sync-io-in-loop",
|
|
2726
|
+
message: "Synchronous I/O inside a loop",
|
|
2727
|
+
suggestion: "Use the async variant and await in parallel where safe"
|
|
2380
2728
|
});
|
|
2381
2729
|
}
|
|
2382
2730
|
}
|
|
2383
2731
|
}
|
|
2384
|
-
function
|
|
2385
|
-
const
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
const
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
if (ch === "{") {
|
|
2404
|
-
depth++;
|
|
2405
|
-
started = true;
|
|
2406
|
-
} else if (ch === "}") depth--;
|
|
2407
|
-
}
|
|
2408
|
-
if (started && depth <= 0) break;
|
|
2409
|
-
}
|
|
2410
|
-
return block;
|
|
2411
|
-
}
|
|
2412
|
-
function extractBodyAfter(lines, match) {
|
|
2413
|
-
const startIdx = lines.findIndex((l) => l.includes(match[0].trim().split(/\s+/).pop() ?? ""));
|
|
2414
|
-
if (startIdx < 0) return "";
|
|
2415
|
-
return collectBlockLines(lines, startIdx).join("\n");
|
|
2416
|
-
}
|
|
2417
|
-
function formatReviewOutput(issues, filesReviewed, totalFiles, focus, customCriteria, autoDetected) {
|
|
2418
|
-
const errors = issues.filter((i) => i.severity === "error");
|
|
2419
|
-
const warnings = issues.filter((i) => i.severity === "warning");
|
|
2420
|
-
const infos = issues.filter((i) => i.severity === "info");
|
|
2421
|
-
const lines = [
|
|
2422
|
-
`\u{1F50D} **Code Review \u2014 Focus: ${focus}**`,
|
|
2423
|
-
...focus === "over-engineering" ? ["\u{1F4A1} **The Ladder:** 1. Does this code need to exist? 2. Does stdlib cover it? 3. Is there a one-liner? 4. Only then, write it."] : [],
|
|
2424
|
-
...autoDetected ? [`\u{1F4C2} Auto-detected ${totalFiles} changed file(s) from git`] : [],
|
|
2425
|
-
...customCriteria ? [`\u{1F4CB} **Custom Criteria:** ${customCriteria}`] : [],
|
|
2426
|
-
`\u{1F4C1} ${filesReviewed}/${totalFiles} files reviewed`,
|
|
2427
|
-
`\u{1F534} ${errors.length} errors | \u{1F7E1} ${warnings.length} warnings | \u{1F535} ${infos.length} info`,
|
|
2428
|
-
""
|
|
2429
|
-
];
|
|
2430
|
-
if (issues.length === 0) {
|
|
2431
|
-
lines.push("\u2705 No issues found.");
|
|
2432
|
-
return lines.join("\n");
|
|
2433
|
-
}
|
|
2434
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
2435
|
-
for (const issue of issues) {
|
|
2436
|
-
const existing = grouped.get(issue.file) ?? [];
|
|
2437
|
-
existing.push(issue);
|
|
2438
|
-
grouped.set(issue.file, existing);
|
|
2439
|
-
}
|
|
2440
|
-
for (const [file, fileIssues] of grouped) {
|
|
2441
|
-
lines.push(`**\u{1F4C4} ${file}:**`);
|
|
2442
|
-
for (const issue of fileIssues) {
|
|
2443
|
-
const icon = issue.severity === "error" ? "\u{1F534}" : issue.severity === "warning" ? "\u{1F7E1}" : "\u{1F535}";
|
|
2444
|
-
lines.push(` ${icon} [L${issue.line}] [${issue.rule}] ${issue.message}`);
|
|
2445
|
-
if (issue.suggestion) lines.push(` \u{1F4A1} ${issue.suggestion}`);
|
|
2446
|
-
}
|
|
2447
|
-
lines.push("");
|
|
2448
|
-
}
|
|
2449
|
-
if (errors.length > 0) {
|
|
2450
|
-
lines.push("\u26A0\uFE0F Fix errors first, then warnings.");
|
|
2451
|
-
lines.push("\u{1F4A1} Use precise_diff_editor to apply fixes.");
|
|
2452
|
-
} else if (warnings.length > 0) {
|
|
2453
|
-
lines.push("\u{1F4A1} Address warnings before merging.");
|
|
2454
|
-
} else {
|
|
2455
|
-
lines.push("\u2705 Only informational issues \u2014 ready for testing.");
|
|
2456
|
-
}
|
|
2457
|
-
return lines.join("\n");
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
// src/utils/conventionsDetector.ts
|
|
2461
|
-
import fs8 from "fs";
|
|
2462
|
-
import path8 from "path";
|
|
2463
|
-
var cachedConventions = null;
|
|
2464
|
-
async function detectConventions(forceRescan = false) {
|
|
2465
|
-
if (cachedConventions && !forceRescan) {
|
|
2466
|
-
return cachedConventions;
|
|
2467
|
-
}
|
|
2468
|
-
const projectRoot = getProjectRoot();
|
|
2469
|
-
const workspaces = detectWorkspaces(projectRoot);
|
|
2470
|
-
const conventions = {
|
|
2471
|
-
framework: detectFramework(projectRoot),
|
|
2472
|
-
projectType: detectProjectType(projectRoot),
|
|
2473
|
-
testRunner: detectTestRunner(projectRoot),
|
|
2474
|
-
styling: detectStyling(projectRoot),
|
|
2475
|
-
importAlias: detectImportAlias(projectRoot),
|
|
2476
|
-
lintRules: detectLintRules(projectRoot),
|
|
2477
|
-
packageManager: detectPackageManager(),
|
|
2478
|
-
moduleSystem: detectModuleSystem(projectRoot),
|
|
2479
|
-
language: detectLanguage(projectRoot),
|
|
2480
|
-
features: detectFeatures(projectRoot),
|
|
2481
|
-
isMonorepo: workspaces.length > 0,
|
|
2482
|
-
workspaces
|
|
2483
|
-
};
|
|
2484
|
-
cachedConventions = conventions;
|
|
2485
|
-
return conventions;
|
|
2486
|
-
}
|
|
2487
|
-
function detectFramework(root) {
|
|
2488
|
-
const pkg = readJsonSafe(path8.join(root, "package.json"));
|
|
2489
|
-
if (pkg) {
|
|
2490
|
-
const pkgDeps = pkg.dependencies ?? {};
|
|
2491
|
-
const pkgDevDeps = pkg.devDependencies ?? {};
|
|
2492
|
-
const deps = { ...pkgDeps, ...pkgDevDeps };
|
|
2493
|
-
if (deps.next) return "Next.js";
|
|
2494
|
-
if (deps["@remix-run/react"]) return "Remix";
|
|
2495
|
-
if (deps.gatsby) return "Gatsby";
|
|
2496
|
-
if (deps.nuxt || deps["@nuxt/core"]) return "Nuxt.js";
|
|
2497
|
-
if (deps.vue) return "Vue";
|
|
2498
|
-
if (deps.react) return "React";
|
|
2499
|
-
if (deps.svelte) return "Svelte";
|
|
2500
|
-
if (deps.astro) return "Astro";
|
|
2501
|
-
if (deps.solid || deps["solid-js"]) return "SolidJS";
|
|
2502
|
-
if (deps.qwik || deps["@builder.io/qwik"]) return "Qwik";
|
|
2503
|
-
if (deps.vite) return "Vite";
|
|
2504
|
-
if (deps["@nestjs/core"]) return "NestJS";
|
|
2505
|
-
if (deps.fastify) return "Fastify";
|
|
2506
|
-
if (deps.express) return "Express";
|
|
2507
|
-
if (deps.koa) return "Koa";
|
|
2508
|
-
if (deps.hono) return "Hono";
|
|
2509
|
-
if (deps["@hapi/hapi"]) return "Hapi";
|
|
2510
|
-
if (deps["@modelcontextprotocol/sdk"]) return "MCP Server";
|
|
2511
|
-
if (deps["@anthropic-ai/claude-agent-sdk"]) return "Claude Agent SDK";
|
|
2512
|
-
if (deps.commander) return "Commander CLI";
|
|
2513
|
-
if (deps.yargs) return "Yargs CLI";
|
|
2514
|
-
if (deps["@oclif/core"] || deps.oclif) return "Oclif CLI";
|
|
2515
|
-
if (deps.ink) return "Ink (React CLI)";
|
|
2516
|
-
if (pkg.bin) return "CLI Tool";
|
|
2517
|
-
if (pkg.main || pkg.exports) return "Library";
|
|
2518
|
-
}
|
|
2519
|
-
const subFrameworks = scanSubProjectFrameworks(root);
|
|
2520
|
-
if (subFrameworks.length > 0) {
|
|
2521
|
-
const freq = /* @__PURE__ */ new Map();
|
|
2522
|
-
for (const fw of subFrameworks) {
|
|
2523
|
-
freq.set(fw, (freq.get(fw) ?? 0) + 1);
|
|
2524
|
-
}
|
|
2525
|
-
const [topFw] = [...freq.entries()].sort((a, b) => b[1] - a[1])[0] ?? [];
|
|
2526
|
-
if (topFw) return `${topFw} (monorepo)`;
|
|
2527
|
-
}
|
|
2528
|
-
return "unknown";
|
|
2529
|
-
}
|
|
2530
|
-
function scanSubProjectFrameworks(root) {
|
|
2531
|
-
const frameworks = [];
|
|
2532
|
-
try {
|
|
2533
|
-
const entries = fs8.readdirSync(root, { withFileTypes: true });
|
|
2534
|
-
for (const entry of entries) {
|
|
2535
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
2536
|
-
const subDir = path8.join(root, entry.name);
|
|
2537
|
-
const subPkg = readJsonSafe(path8.join(subDir, "package.json"));
|
|
2538
|
-
if (!subPkg) continue;
|
|
2539
|
-
const subDeps = { ...subPkg.dependencies ?? {}, ...subPkg.devDependencies ?? {} };
|
|
2540
|
-
if (subDeps.next) frameworks.push("Next.js");
|
|
2541
|
-
else if (subDeps.react) frameworks.push("React");
|
|
2542
|
-
else if (subDeps.vue) frameworks.push("Vue");
|
|
2543
|
-
else if (subDeps.svelte) frameworks.push("Svelte");
|
|
2544
|
-
else if (subDeps.astro) frameworks.push("Astro");
|
|
2545
|
-
else if (subDeps["@nestjs/core"]) frameworks.push("NestJS");
|
|
2546
|
-
else if (subDeps.express) frameworks.push("Express");
|
|
2547
|
-
else if (subDeps.hono) frameworks.push("Hono");
|
|
2548
|
-
else if (subDeps.vite) frameworks.push("Vite");
|
|
2549
|
-
else if (subDeps["@modelcontextprotocol/sdk"]) frameworks.push("MCP Server");
|
|
2550
|
-
}
|
|
2551
|
-
} catch {
|
|
2552
|
-
}
|
|
2553
|
-
return frameworks;
|
|
2554
|
-
}
|
|
2555
|
-
function detectProjectType(root) {
|
|
2556
|
-
const pkg = readJsonSafe(path8.join(root, "package.json"));
|
|
2557
|
-
if (pkg) {
|
|
2558
|
-
const pkgDeps = pkg.dependencies ?? {};
|
|
2559
|
-
const pkgDevDeps = pkg.devDependencies ?? {};
|
|
2560
|
-
const deps = { ...pkgDeps, ...pkgDevDeps };
|
|
2561
|
-
if (deps["@modelcontextprotocol/sdk"]) return "mcp-server";
|
|
2562
|
-
if (deps.next || deps.react || deps.vue || deps.svelte || deps.astro || deps.gatsby || deps.nuxt || deps["@remix-run/react"] || deps.solid) {
|
|
2563
|
-
return "web-app";
|
|
2564
|
-
}
|
|
2565
|
-
if (deps.express || deps.fastify || deps.koa || deps.hono || deps["@nestjs/core"] || deps["@hapi/hapi"]) {
|
|
2566
|
-
return "backend";
|
|
2567
|
-
}
|
|
2568
|
-
if (deps.commander || deps.yargs || deps["@oclif/core"] || deps.oclif || deps.ink) {
|
|
2569
|
-
return "cli";
|
|
2732
|
+
function checkOverEngineering(filePath, content, issues) {
|
|
2733
|
+
const lines = content.split("\n");
|
|
2734
|
+
const text = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
2735
|
+
const classBodies = findBlockBodies(lines, /^\s*(export\s+)?(abstract\s+)?class\s+\w+/);
|
|
2736
|
+
for (const { body, nameLine, name } of classBodies) {
|
|
2737
|
+
const methodCount = (body.match(/^\s*(public|private|protected|static|async)?\s*\w+\s*\([^)]*\)\s*{/gm) || []).length;
|
|
2738
|
+
if (methodCount <= 1 && body.length > 0) {
|
|
2739
|
+
const trimmed = body.trim();
|
|
2740
|
+
const nonEmptyLines = trimmed ? trimmed.split("\n").filter((l) => l.trim()).length : 0;
|
|
2741
|
+
if (nonEmptyLines <= 3) {
|
|
2742
|
+
issues.push({
|
|
2743
|
+
file: filePath,
|
|
2744
|
+
line: nameLine,
|
|
2745
|
+
severity: "info",
|
|
2746
|
+
rule: "over-engineering/wrapper-class",
|
|
2747
|
+
message: `Class "${name}" has only 1 method \u2014 likely a wrapper that could be a plain function`,
|
|
2748
|
+
suggestion: "Replace with a standalone function or a utility module export"
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2570
2751
|
}
|
|
2571
|
-
if (pkg.bin) return "cli";
|
|
2572
|
-
if (pkg.main || pkg.exports) return "library";
|
|
2573
2752
|
}
|
|
2574
|
-
const
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
if (Array.isArray(packages)) {
|
|
2589
|
-
for (const p of packages) if (typeof p === "string") patterns.add(p);
|
|
2753
|
+
const factoryMatches = text.matchAll(/^\s*(export\s+)?function\s+(create\w+|make\w+|build\w+|factory\w+)\s*\(/gm);
|
|
2754
|
+
for (const match of factoryMatches) {
|
|
2755
|
+
const funcName = match[2];
|
|
2756
|
+
const body = extractBodyAfter(lines, match);
|
|
2757
|
+
const creationCount = (body.match(/\bnew\s+\w+/g) || []).length;
|
|
2758
|
+
if (creationCount <= 1) {
|
|
2759
|
+
issues.push({
|
|
2760
|
+
file: filePath,
|
|
2761
|
+
line: lines.findIndex((l) => l.includes(funcName)) + 1,
|
|
2762
|
+
severity: "warning",
|
|
2763
|
+
rule: "over-engineering/single-product-factory",
|
|
2764
|
+
message: `Factory "${funcName}" produces only 1 product \u2014 unnecessary abstraction`,
|
|
2765
|
+
suggestion: "Inline the construction or drop the factory pattern"
|
|
2766
|
+
});
|
|
2590
2767
|
}
|
|
2591
2768
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2769
|
+
const interfaceNames = [];
|
|
2770
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2771
|
+
const m = lines[i].match(/^\s*(export\s+)?interface\s+(\w+)/);
|
|
2772
|
+
if (m) interfaceNames.push(m[2]);
|
|
2594
2773
|
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
continue;
|
|
2774
|
+
for (const iface of interfaceNames) {
|
|
2775
|
+
const implCount = (text.match(new RegExp(`implements\\s+.*\\b${iface}\\b`, "g")) || []).length;
|
|
2776
|
+
if (implCount <= 1) {
|
|
2777
|
+
const lineNum = lines.findIndex((l) => l.includes(`interface ${iface}`)) + 1;
|
|
2778
|
+
issues.push({
|
|
2779
|
+
file: filePath,
|
|
2780
|
+
line: lineNum,
|
|
2781
|
+
severity: "info",
|
|
2782
|
+
rule: "over-engineering/single-impl-interface",
|
|
2783
|
+
message: `Interface "${iface}" has only 1 implementation \u2014 unnecessary unless more planned`,
|
|
2784
|
+
suggestion: "Remove the interface or inline the type"
|
|
2785
|
+
});
|
|
2608
2786
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2787
|
+
}
|
|
2788
|
+
const configObjPattern = /^\s*(export\s+)?(const|let|var)\s+(\w+)\s*[=:]\s*\{/;
|
|
2789
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2790
|
+
const cm = lines[i].match(configObjPattern);
|
|
2791
|
+
if (!cm || !/config|setting|option|default/i.test(cm[3])) continue;
|
|
2792
|
+
const configName = cm[3];
|
|
2793
|
+
const refs = (text.match(new RegExp(`\\b${configName}\\b`, "g")) || []).length;
|
|
2794
|
+
if (refs <= 2) {
|
|
2795
|
+
issues.push({
|
|
2796
|
+
file: filePath,
|
|
2797
|
+
line: i + 1,
|
|
2798
|
+
severity: "info",
|
|
2799
|
+
rule: "over-engineering/unused-config",
|
|
2800
|
+
message: `Config "${configName}" is referenced ${refs === 0 ? "nowhere" : `only ${refs - 1}x outside definition`}`,
|
|
2801
|
+
suggestion: refs === 0 ? "Remove the dead config" : "Inline the value or remove if unused elsewhere"
|
|
2618
2802
|
});
|
|
2619
2803
|
}
|
|
2620
2804
|
}
|
|
2621
|
-
const
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
const m = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/);
|
|
2642
|
-
if (m) packages.push(m[1]);
|
|
2643
|
-
else if (/^\S/.test(line)) inPackages = false;
|
|
2644
|
-
}
|
|
2805
|
+
const stdlibPatterns = [
|
|
2806
|
+
{ pattern: /function\s+\w+\s*\([^)]*\)\s*\{[^}]*fs\.readFileSync[^}]*\}/, rule: "over-engineering/hand-rolled-file-read", msg: "Hand-rolled file reader \u2014 use fs.readFileSync directly", suggestion: "Call fs.readFileSync directly instead of wrapping" },
|
|
2807
|
+
{ pattern: /function\s+\w+\s*\([^)]*\)\s*\{[^}]*crypto\.createHash[^}]*\}/, rule: "over-engineering/hand-rolled-hash", msg: "Hand-rolled hash \u2014 use crypto directly", suggestion: "Call crypto.createHash directly" },
|
|
2808
|
+
{ pattern: /function\s+uuid\s*\(|function\s+generateId\s*\(/, rule: "over-engineering/hand-rolled-uuid", msg: "Hand-rolled UUID \u2014 use crypto.randomUUID()", suggestion: "Replace with crypto.randomUUID() (Node 19+) or the uuid package" },
|
|
2809
|
+
{ pattern: /function\s+debounce\s*\(|function\s+throttle\s*\(/, rule: "over-engineering/hand-rolled-debounce", msg: "Hand-rolled debounce/throttle \u2014 utility lib or native", suggestion: "Use lodash.debounce or a 3-line inline version" }
|
|
2810
|
+
];
|
|
2811
|
+
for (const sp of stdlibPatterns) {
|
|
2812
|
+
const sl = text.match(sp.pattern);
|
|
2813
|
+
if (sl) {
|
|
2814
|
+
const matchIndex = sl.index ?? 0;
|
|
2815
|
+
const preText = text.substring(0, matchIndex);
|
|
2816
|
+
const lineNum = preText.split("\n").length;
|
|
2817
|
+
issues.push({
|
|
2818
|
+
file: filePath,
|
|
2819
|
+
line: lineNum,
|
|
2820
|
+
severity: "info",
|
|
2821
|
+
rule: sp.rule,
|
|
2822
|
+
message: sp.msg,
|
|
2823
|
+
suggestion: sp.suggestion
|
|
2824
|
+
});
|
|
2645
2825
|
}
|
|
2646
|
-
return { packages };
|
|
2647
|
-
} catch {
|
|
2648
|
-
return null;
|
|
2649
2826
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
const pkgDeps2 = pkg.dependencies ?? {};
|
|
2655
|
-
const pkgDevDeps2 = pkg.devDependencies ?? {};
|
|
2656
|
-
const deps = { ...pkgDeps2, ...pkgDevDeps2 };
|
|
2657
|
-
if (deps.vitest) return "Vitest";
|
|
2658
|
-
if (deps.jest) return "Jest";
|
|
2659
|
-
if (deps.mocha) return "Mocha";
|
|
2660
|
-
if (deps.ava) return "AVA";
|
|
2661
|
-
if (deps.cypress) return "Cypress";
|
|
2662
|
-
if (deps.playwright) return "Playwright";
|
|
2663
|
-
if (deps["@testing-library/react"]) return "React Testing Library";
|
|
2664
|
-
const scripts = pkg.scripts ?? {};
|
|
2665
|
-
if (scripts.test) {
|
|
2666
|
-
if (scripts.test.includes("vitest")) return "Vitest";
|
|
2667
|
-
if (scripts.test.includes("jest")) return "Jest";
|
|
2668
|
-
if (scripts.test.includes("mocha")) return "Mocha";
|
|
2827
|
+
const exportDecls = [];
|
|
2828
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2829
|
+
const ed = lines[i].match(/^\s*export\s+(const|let|var|function|class)\s+(\w+)/);
|
|
2830
|
+
if (ed) exportDecls.push({ name: ed[2], line: i + 1 });
|
|
2669
2831
|
}
|
|
2670
|
-
|
|
2671
|
-
}
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
if (deps.less) return "Less";
|
|
2684
|
-
if (deps["@vanilla-extract/css"]) return "Vanilla Extract";
|
|
2685
|
-
if (deps.cssmodules || hasCSSModules(root)) return "CSS Modules";
|
|
2686
|
-
const srcDir = path8.join(root, "src");
|
|
2687
|
-
if (fs8.existsSync(srcDir)) {
|
|
2688
|
-
const cssFiles = findFiles(srcDir, [".css", ".scss", ".less"]);
|
|
2689
|
-
if (cssFiles.length > 0) return "Plain CSS/SCSS";
|
|
2832
|
+
for (const decl of exportDecls) {
|
|
2833
|
+
const refCount = (text.match(new RegExp(`\\b${decl.name}\\b`, "g")) || []).length;
|
|
2834
|
+
const isReactComponent = /^[A-Z]/.test(decl.name) && /return\s+<|React\./.test(text);
|
|
2835
|
+
if (refCount <= 1 && decl.name !== "default" && !isReactComponent) {
|
|
2836
|
+
issues.push({
|
|
2837
|
+
file: filePath,
|
|
2838
|
+
line: decl.line,
|
|
2839
|
+
severity: "info",
|
|
2840
|
+
rule: "over-engineering/unused-export",
|
|
2841
|
+
message: `"${decl.name}" is exported but referenced only in its declaration`,
|
|
2842
|
+
suggestion: "Check if it is imported elsewhere; if not, remove or inline"
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2690
2845
|
}
|
|
2691
|
-
return "unknown";
|
|
2692
2846
|
}
|
|
2693
|
-
function
|
|
2694
|
-
const
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
const jsconfig = readJsonSafe(path8.join(root, "jsconfig.json"));
|
|
2702
|
-
const jsconfigPaths = jsconfig?.compilerOptions;
|
|
2703
|
-
if (jsconfigPaths?.paths) {
|
|
2704
|
-
const paths = jsconfigPaths.paths;
|
|
2705
|
-
const alias = Object.keys(paths)[0];
|
|
2706
|
-
if (alias) return alias.replace("/*", "");
|
|
2847
|
+
function findBlockBodies(lines, startPattern) {
|
|
2848
|
+
const results = [];
|
|
2849
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2850
|
+
const m = lines[i].match(startPattern);
|
|
2851
|
+
if (!m) continue;
|
|
2852
|
+
const name = m[0].match(/class\s+(\w+)/)?.[1] ?? "Unknown";
|
|
2853
|
+
const body = collectBlockLines(lines, i);
|
|
2854
|
+
results.push({ body: body.join("\n"), nameLine: i + 1, name });
|
|
2707
2855
|
}
|
|
2708
|
-
return
|
|
2709
|
-
}
|
|
2710
|
-
function detectLintRules(root) {
|
|
2711
|
-
const rules = [];
|
|
2712
|
-
if (fs8.existsSync(path8.join(root, ".eslintrc"))) rules.push("ESLint (.eslintrc)");
|
|
2713
|
-
if (fs8.existsSync(path8.join(root, ".eslintrc.js"))) rules.push("ESLint (.eslintrc.js)");
|
|
2714
|
-
if (fs8.existsSync(path8.join(root, ".eslintrc.json"))) rules.push("ESLint (.eslintrc.json)");
|
|
2715
|
-
if (fs8.existsSync(path8.join(root, "eslint.config.js"))) rules.push("ESLint (flat config)");
|
|
2716
|
-
if (fs8.existsSync(path8.join(root, ".prettierrc"))) rules.push("Prettier");
|
|
2717
|
-
if (fs8.existsSync(path8.join(root, ".prettierrc.json"))) rules.push("Prettier");
|
|
2718
|
-
if (fs8.existsSync(path8.join(root, ".prettierrc.js"))) rules.push("Prettier");
|
|
2719
|
-
if (fs8.existsSync(path8.join(root, ".stylelintrc"))) rules.push("StyleLint");
|
|
2720
|
-
if (fs8.existsSync(path8.join(root, ".stylelintrc.json"))) rules.push("StyleLint");
|
|
2721
|
-
return rules;
|
|
2722
|
-
}
|
|
2723
|
-
function detectPackageManager() {
|
|
2724
|
-
const root = getProjectRoot();
|
|
2725
|
-
if (fs8.existsSync(path8.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2726
|
-
if (fs8.existsSync(path8.join(root, "yarn.lock"))) return "yarn";
|
|
2727
|
-
if (fs8.existsSync(path8.join(root, "package-lock.json"))) return "npm";
|
|
2728
|
-
if (fs8.existsSync(path8.join(root, "bun.lockb"))) return "bun";
|
|
2729
|
-
return "npm";
|
|
2856
|
+
return results;
|
|
2730
2857
|
}
|
|
2731
|
-
function
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
const
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
}
|
|
2858
|
+
function collectBlockLines(lines, startIdx) {
|
|
2859
|
+
let depth = 0;
|
|
2860
|
+
let started = false;
|
|
2861
|
+
const block = [];
|
|
2862
|
+
for (let j = startIdx; j < lines.length; j++) {
|
|
2863
|
+
const line = lines[j];
|
|
2864
|
+
block.push(line);
|
|
2865
|
+
for (const ch of line) {
|
|
2866
|
+
if (ch === "{") {
|
|
2867
|
+
depth++;
|
|
2868
|
+
started = true;
|
|
2869
|
+
} else if (ch === "}") depth--;
|
|
2744
2870
|
}
|
|
2871
|
+
if (started && depth <= 0) break;
|
|
2745
2872
|
}
|
|
2746
|
-
return
|
|
2747
|
-
}
|
|
2748
|
-
function detectLanguage(root) {
|
|
2749
|
-
if (fs8.existsSync(path8.join(root, "tsconfig.json"))) return "typescript";
|
|
2750
|
-
const srcDir = path8.join(root, "src");
|
|
2751
|
-
if (fs8.existsSync(srcDir)) {
|
|
2752
|
-
const tsFiles = findFiles(srcDir, [".ts", ".tsx"]);
|
|
2753
|
-
if (tsFiles.length > 0) return "typescript";
|
|
2754
|
-
}
|
|
2755
|
-
return "javascript";
|
|
2873
|
+
return block;
|
|
2756
2874
|
}
|
|
2757
|
-
function
|
|
2758
|
-
const
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
const pkgDeps4 = pkg.dependencies ?? {};
|
|
2762
|
-
const pkgDevDeps4 = pkg.devDependencies ?? {};
|
|
2763
|
-
const deps = { ...pkgDeps4, ...pkgDevDeps4 };
|
|
2764
|
-
if (deps["@prisma/client"] || deps.prisma) features.push("Prisma ORM");
|
|
2765
|
-
if (deps.typeorm) features.push("TypeORM");
|
|
2766
|
-
if (deps["drizzle-orm"]) features.push("Drizzle ORM");
|
|
2767
|
-
if (deps.graphql || deps["@graphql-tools"]) features.push("GraphQL");
|
|
2768
|
-
if (deps.trpc || deps["@trpc/client"]) features.push("tRPC");
|
|
2769
|
-
if (deps["socket.io"] || deps.ws) features.push("WebSockets");
|
|
2770
|
-
if (deps["@reduxjs/toolkit"] || deps.redux) features.push("Redux");
|
|
2771
|
-
if (deps.zustand) features.push("Zustand");
|
|
2772
|
-
if (deps["react-router"] || deps["react-router-dom"]) features.push("React Router");
|
|
2773
|
-
if (deps["next-auth"] || deps["next-auth/react"]) features.push("NextAuth");
|
|
2774
|
-
if (deps["@tanstack/react-query"]) features.push("React Query");
|
|
2775
|
-
if (deps.jest || deps.vitest) features.push("Unit Testing");
|
|
2776
|
-
if (deps.cypress || deps.playwright) features.push("E2E Testing");
|
|
2777
|
-
if (deps.storybook || deps["@storybook/react"]) features.push("Storybook");
|
|
2778
|
-
if (deps.i18next || deps["react-i18next"]) features.push("i18n");
|
|
2779
|
-
return features;
|
|
2875
|
+
function extractBodyAfter(lines, match) {
|
|
2876
|
+
const startIdx = lines.findIndex((l) => l.includes(match[0].trim().split(/\s+/).pop() ?? ""));
|
|
2877
|
+
if (startIdx < 0) return "";
|
|
2878
|
+
return collectBlockLines(lines, startIdx).join("\n");
|
|
2780
2879
|
}
|
|
2781
|
-
function
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2880
|
+
function formatReviewOutput(issues, filesReviewed, totalFiles, focus, customCriteria, autoDetected) {
|
|
2881
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
2882
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
2883
|
+
const infos = issues.filter((i) => i.severity === "info");
|
|
2884
|
+
const lines = [
|
|
2885
|
+
`\u{1F50D} **Code Review \u2014 Focus: ${focus}**`,
|
|
2886
|
+
...focus === "over-engineering" ? ["\u{1F4A1} **The Ladder:** 1. Does this code need to exist? 2. Does stdlib cover it? 3. Is there a one-liner? 4. Only then, write it."] : [],
|
|
2887
|
+
...autoDetected ? [`\u{1F4C2} Auto-detected ${totalFiles} changed file(s) from git`] : [],
|
|
2888
|
+
...customCriteria ? [`\u{1F4CB} **Custom Criteria:** ${customCriteria}`] : [],
|
|
2889
|
+
`\u{1F4C1} ${filesReviewed}/${totalFiles} files reviewed`,
|
|
2890
|
+
`\u{1F534} ${errors.length} errors | \u{1F7E1} ${warnings.length} warnings | \u{1F535} ${infos.length} info`,
|
|
2891
|
+
""
|
|
2892
|
+
];
|
|
2893
|
+
if (issues.length === 0) {
|
|
2894
|
+
lines.push("\u2705 No issues found.");
|
|
2895
|
+
return lines.join("\n");
|
|
2896
|
+
}
|
|
2897
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2898
|
+
for (const issue of issues) {
|
|
2899
|
+
const existing = grouped.get(issue.file) ?? [];
|
|
2900
|
+
existing.push(issue);
|
|
2901
|
+
grouped.set(issue.file, existing);
|
|
2787
2902
|
}
|
|
2788
|
-
|
|
2789
|
-
}
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
return files.length > 0;
|
|
2795
|
-
}
|
|
2796
|
-
function findFiles(dir, extensions) {
|
|
2797
|
-
const results = [];
|
|
2798
|
-
try {
|
|
2799
|
-
const entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
2800
|
-
for (const entry of entries) {
|
|
2801
|
-
const fullPath = path8.join(dir, entry.name);
|
|
2802
|
-
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2803
|
-
results.push(...findFiles(fullPath, extensions));
|
|
2804
|
-
} else if (entry.isFile()) {
|
|
2805
|
-
if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
2806
|
-
results.push(fullPath);
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2903
|
+
for (const [file, fileIssues] of grouped) {
|
|
2904
|
+
lines.push(`**\u{1F4C4} ${file}:**`);
|
|
2905
|
+
for (const issue of fileIssues) {
|
|
2906
|
+
const icon = issue.severity === "error" ? "\u{1F534}" : issue.severity === "warning" ? "\u{1F7E1}" : "\u{1F535}";
|
|
2907
|
+
lines.push(` ${icon} [L${issue.line}] [${issue.rule}] ${issue.message}`);
|
|
2908
|
+
if (issue.suggestion) lines.push(` \u{1F4A1} ${issue.suggestion}`);
|
|
2809
2909
|
}
|
|
2810
|
-
|
|
2910
|
+
lines.push("");
|
|
2811
2911
|
}
|
|
2812
|
-
|
|
2912
|
+
if (errors.length > 0) {
|
|
2913
|
+
lines.push("\u26A0\uFE0F Fix errors first, then warnings.");
|
|
2914
|
+
lines.push("\u{1F4A1} Use precise_diff_editor to apply fixes.");
|
|
2915
|
+
} else if (warnings.length > 0) {
|
|
2916
|
+
lines.push("\u{1F4A1} Address warnings before merging.");
|
|
2917
|
+
} else {
|
|
2918
|
+
lines.push("\u2705 Only informational issues \u2014 ready for testing.");
|
|
2919
|
+
}
|
|
2920
|
+
return lines.join("\n");
|
|
2813
2921
|
}
|
|
2814
2922
|
|
|
2815
2923
|
// src/agents/projectConventions.ts
|
|
@@ -2868,20 +2976,26 @@ function formatConventionsOutput(conventions) {
|
|
|
2868
2976
|
return lines.join("\n");
|
|
2869
2977
|
}
|
|
2870
2978
|
|
|
2979
|
+
// src/utils/gitUtils.ts
|
|
2980
|
+
import { execSync as execSync2 } from "child_process";
|
|
2981
|
+
function runGitCommand(command) {
|
|
2982
|
+
const root = getProjectRoot();
|
|
2983
|
+
return execSync2(command, {
|
|
2984
|
+
cwd: root,
|
|
2985
|
+
encoding: "utf-8",
|
|
2986
|
+
maxBuffer: 2 * 1024 * 1024
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2871
2990
|
// src/tools/gitLog.ts
|
|
2872
|
-
import child_process from "child_process";
|
|
2873
2991
|
async function handleGitLog(params) {
|
|
2874
2992
|
const { maxCount = 10, filePath } = params;
|
|
2875
|
-
const root = getProjectRoot();
|
|
2876
2993
|
try {
|
|
2877
2994
|
let command = `git log -n ${maxCount} --oneline`;
|
|
2878
2995
|
if (filePath) {
|
|
2879
2996
|
command += ` -- "${filePath}"`;
|
|
2880
2997
|
}
|
|
2881
|
-
const stdout =
|
|
2882
|
-
cwd: root,
|
|
2883
|
-
encoding: "utf-8"
|
|
2884
|
-
});
|
|
2998
|
+
const stdout = runGitCommand(command);
|
|
2885
2999
|
sessionMemory.recordToolCall("git_log", { maxCount, filePath });
|
|
2886
3000
|
if (!stdout.trim()) {
|
|
2887
3001
|
return `\u2139\uFE0F No commit history found${filePath ? ` for file "${filePath}"` : ""}.`;
|
|
@@ -2895,10 +3009,8 @@ ${stdout}`;
|
|
|
2895
3009
|
}
|
|
2896
3010
|
|
|
2897
3011
|
// src/tools/gitDiff.ts
|
|
2898
|
-
import child_process2 from "child_process";
|
|
2899
3012
|
async function handleGitDiff(params) {
|
|
2900
3013
|
const { filePath, staged = false, contextLines = 3, baseRef, targetRef } = params;
|
|
2901
|
-
const root = getProjectRoot();
|
|
2902
3014
|
try {
|
|
2903
3015
|
let command = "git diff";
|
|
2904
3016
|
if (staged) {
|
|
@@ -2914,11 +3026,7 @@ async function handleGitDiff(params) {
|
|
|
2914
3026
|
if (filePath) {
|
|
2915
3027
|
command += ' -- "' + filePath + '"';
|
|
2916
3028
|
}
|
|
2917
|
-
const stdout =
|
|
2918
|
-
cwd: root,
|
|
2919
|
-
encoding: "utf-8",
|
|
2920
|
-
maxBuffer: 2 * 1024 * 1024
|
|
2921
|
-
});
|
|
3029
|
+
const stdout = runGitCommand(command);
|
|
2922
3030
|
sessionMemory.recordToolCall("git_diff", { filePath, staged, baseRef, targetRef });
|
|
2923
3031
|
if (!stdout.trim()) {
|
|
2924
3032
|
if (staged) {
|
|
@@ -3000,8 +3108,8 @@ async function handleGitDiff(params) {
|
|
|
3000
3108
|
}
|
|
3001
3109
|
|
|
3002
3110
|
// src/tools/projectStructure.ts
|
|
3003
|
-
import
|
|
3004
|
-
import
|
|
3111
|
+
import fs10 from "fs";
|
|
3112
|
+
import path10 from "path";
|
|
3005
3113
|
var DEFAULT_IGNORE = [
|
|
3006
3114
|
"node_modules",
|
|
3007
3115
|
".git",
|
|
@@ -3018,6 +3126,8 @@ var DEFAULT_IGNORE = [
|
|
|
3018
3126
|
".DS_Store",
|
|
3019
3127
|
"*.log"
|
|
3020
3128
|
];
|
|
3129
|
+
var treeCache = /* @__PURE__ */ new Map();
|
|
3130
|
+
var CACHE_TTL_MS2 = 6e4;
|
|
3021
3131
|
async function handleProjectStructure(params) {
|
|
3022
3132
|
const {
|
|
3023
3133
|
depth = 3,
|
|
@@ -3027,10 +3137,16 @@ async function handleProjectStructure(params) {
|
|
|
3027
3137
|
} = params;
|
|
3028
3138
|
const root = getProjectRoot();
|
|
3029
3139
|
const clampedDepth = Math.max(1, Math.min(depth, 6));
|
|
3140
|
+
const cacheKey = `${root}:${clampedDepth}:${folderOnly}:${includePattern ?? ""}:${excludePattern ?? ""}`;
|
|
3141
|
+
const cached = treeCache.get(cacheKey);
|
|
3142
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS2) {
|
|
3143
|
+
sessionMemory.recordToolCall("project_structure", { depth: clampedDepth, folderOnly, cached: true });
|
|
3144
|
+
return cached.result;
|
|
3145
|
+
}
|
|
3030
3146
|
sessionMemory.recordToolCall("project_structure", { depth: clampedDepth, folderOnly });
|
|
3031
3147
|
try {
|
|
3032
3148
|
const tree = buildTree(root, root, clampedDepth, 0, folderOnly, includePattern, excludePattern);
|
|
3033
|
-
const projectName =
|
|
3149
|
+
const projectName = path10.basename(root);
|
|
3034
3150
|
const lines = [
|
|
3035
3151
|
"[Project Structure] - " + projectName,
|
|
3036
3152
|
"Depth: " + clampedDepth + " | " + (folderOnly ? "Folders only" : "Files and folders"),
|
|
@@ -3041,7 +3157,9 @@ async function handleProjectStructure(params) {
|
|
|
3041
3157
|
"Increase depth (max 6) for deeper structure.",
|
|
3042
3158
|
"Use folderOnly: true for high-level overview."
|
|
3043
3159
|
];
|
|
3044
|
-
|
|
3160
|
+
const result = lines.join("\n");
|
|
3161
|
+
treeCache.set(cacheKey, { result, timestamp: Date.now() });
|
|
3162
|
+
return result;
|
|
3045
3163
|
} catch (err) {
|
|
3046
3164
|
return "Error building project structure: " + (err instanceof Error ? err.message : String(err));
|
|
3047
3165
|
}
|
|
@@ -3051,7 +3169,7 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3051
3169
|
const lines = [];
|
|
3052
3170
|
let entries = [];
|
|
3053
3171
|
try {
|
|
3054
|
-
entries =
|
|
3172
|
+
entries = fs10.readdirSync(currentDir, { withFileTypes: true });
|
|
3055
3173
|
} catch {
|
|
3056
3174
|
return lines;
|
|
3057
3175
|
}
|
|
@@ -3060,12 +3178,12 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3060
3178
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
3061
3179
|
return a.name.localeCompare(b.name);
|
|
3062
3180
|
});
|
|
3063
|
-
const relativeDir =
|
|
3181
|
+
const relativeDir = path10.relative(root, currentDir) || ".";
|
|
3064
3182
|
const prefix = getPrefix(currentDepth);
|
|
3065
3183
|
for (const entry of entries) {
|
|
3066
3184
|
if (shouldIgnore(entry.name, relativeDir, root)) continue;
|
|
3067
|
-
const fullPath =
|
|
3068
|
-
const relativePath =
|
|
3185
|
+
const fullPath = path10.join(currentDir, entry.name);
|
|
3186
|
+
const relativePath = path10.relative(root, fullPath);
|
|
3069
3187
|
if (includePattern && !entry.name.includes(includePattern) && !relativePath.includes(includePattern)) {
|
|
3070
3188
|
if (!entry.isDirectory()) continue;
|
|
3071
3189
|
}
|
|
@@ -3076,11 +3194,11 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3076
3194
|
lines.push(prefix + "[D] " + entry.name + "/");
|
|
3077
3195
|
let subEntries = [];
|
|
3078
3196
|
try {
|
|
3079
|
-
subEntries =
|
|
3197
|
+
subEntries = fs10.readdirSync(fullPath, { withFileTypes: true });
|
|
3080
3198
|
} catch {
|
|
3081
3199
|
}
|
|
3082
3200
|
const hasVisibleContent = subEntries.some(
|
|
3083
|
-
(e) => !shouldIgnore(e.name,
|
|
3201
|
+
(e) => !shouldIgnore(e.name, path10.relative(root, fullPath), root)
|
|
3084
3202
|
);
|
|
3085
3203
|
if (hasVisibleContent) {
|
|
3086
3204
|
const subLines = buildTree(root, fullPath, maxDepth, currentDepth + 1, folderOnly, includePattern, excludePattern);
|
|
@@ -3111,7 +3229,7 @@ function getPrefix(depth) {
|
|
|
3111
3229
|
}
|
|
3112
3230
|
function getFileSize(fullPath) {
|
|
3113
3231
|
try {
|
|
3114
|
-
const stat =
|
|
3232
|
+
const stat = fs10.statSync(fullPath);
|
|
3115
3233
|
if (stat.size < 1024) return stat.size + "B";
|
|
3116
3234
|
if (stat.size < 1024 * 1024) return (stat.size / 1024).toFixed(0) + "KB";
|
|
3117
3235
|
const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
|
|
@@ -3122,9 +3240,8 @@ function getFileSize(fullPath) {
|
|
|
3122
3240
|
}
|
|
3123
3241
|
|
|
3124
3242
|
// src/tools/staticAnalysis.ts
|
|
3125
|
-
import
|
|
3126
|
-
import
|
|
3127
|
-
import path10 from "path";
|
|
3243
|
+
import fs11 from "fs";
|
|
3244
|
+
import path11 from "path";
|
|
3128
3245
|
async function handleStaticAnalysis(params) {
|
|
3129
3246
|
const { tool = "all", files, autoFix = false, timeout = 60 } = params;
|
|
3130
3247
|
const root = getProjectRoot();
|
|
@@ -3179,11 +3296,11 @@ function detectAvailableTools(root) {
|
|
|
3179
3296
|
"eslint.config.js",
|
|
3180
3297
|
"eslint.config.mjs"
|
|
3181
3298
|
];
|
|
3182
|
-
const hasEslintConfig = eslintConfigs.some((cfg) =>
|
|
3299
|
+
const hasEslintConfig = eslintConfigs.some((cfg) => fs11.existsSync(path11.join(root, cfg)));
|
|
3183
3300
|
if (hasEslintConfig && depNames.has("eslint")) {
|
|
3184
3301
|
tools.push("eslint");
|
|
3185
3302
|
}
|
|
3186
|
-
if (
|
|
3303
|
+
if (fs11.existsSync(path11.join(root, "tsconfig.json")) && depNames.has("typescript")) {
|
|
3187
3304
|
tools.push("tsc");
|
|
3188
3305
|
}
|
|
3189
3306
|
const prettierConfigs = [
|
|
@@ -3194,20 +3311,20 @@ function detectAvailableTools(root) {
|
|
|
3194
3311
|
".prettierrc.toml",
|
|
3195
3312
|
"prettier.config.js"
|
|
3196
3313
|
];
|
|
3197
|
-
const hasPrettierConfig = prettierConfigs.some((cfg) =>
|
|
3314
|
+
const hasPrettierConfig = prettierConfigs.some((cfg) => fs11.existsSync(path11.join(root, cfg)));
|
|
3198
3315
|
if (hasPrettierConfig && depNames.has("prettier")) {
|
|
3199
3316
|
tools.push("prettier");
|
|
3200
3317
|
}
|
|
3201
|
-
if (
|
|
3318
|
+
if (fs11.existsSync(path11.join(root, "ruff.toml")) || fs11.existsSync(path11.join(root, ".ruff.toml"))) {
|
|
3202
3319
|
tools.push("ruff");
|
|
3203
3320
|
}
|
|
3204
3321
|
return tools;
|
|
3205
3322
|
}
|
|
3206
3323
|
function readPackageJson(root) {
|
|
3207
3324
|
try {
|
|
3208
|
-
const pkgPath =
|
|
3209
|
-
if (
|
|
3210
|
-
return JSON.parse(
|
|
3325
|
+
const pkgPath = path11.join(root, "package.json");
|
|
3326
|
+
if (fs11.existsSync(pkgPath)) {
|
|
3327
|
+
return JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
|
|
3211
3328
|
}
|
|
3212
3329
|
} catch {
|
|
3213
3330
|
}
|
|
@@ -3216,45 +3333,14 @@ function readPackageJson(root) {
|
|
|
3216
3333
|
async function runTool(tool, cwd, files, autoFix, timeoutSeconds) {
|
|
3217
3334
|
const cmd = buildToolCommand(tool, files, autoFix);
|
|
3218
3335
|
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
|
-
});
|
|
3336
|
+
const result = await spawnProcess(cmd, {
|
|
3337
|
+
cwd,
|
|
3338
|
+
timeoutSeconds: timeoutSeconds ?? 60,
|
|
3339
|
+
maxStdout: 1e5,
|
|
3340
|
+
// Tool output can be large
|
|
3341
|
+
maxStderr: 5e4
|
|
3257
3342
|
});
|
|
3343
|
+
return result;
|
|
3258
3344
|
}
|
|
3259
3345
|
function buildToolCommand(tool, files, autoFix) {
|
|
3260
3346
|
const pm = detectPackageManagerPrefix();
|
|
@@ -3283,8 +3369,8 @@ function buildToolCommand(tool, files, autoFix) {
|
|
|
3283
3369
|
}
|
|
3284
3370
|
function detectPackageManagerPrefix() {
|
|
3285
3371
|
const root = getProjectRoot();
|
|
3286
|
-
if (
|
|
3287
|
-
if (
|
|
3372
|
+
if (fs11.existsSync(path11.join(root, "pnpm-lock.yaml"))) return "pnpm ";
|
|
3373
|
+
if (fs11.existsSync(path11.join(root, "yarn.lock"))) return "yarn ";
|
|
3288
3374
|
return "npx --no-install ";
|
|
3289
3375
|
}
|
|
3290
3376
|
function parseToolOutput(tool, stdout, stderr, projectRoot) {
|
|
@@ -3303,7 +3389,7 @@ function parseToolOutput(tool, stdout, stderr, projectRoot) {
|
|
|
3303
3389
|
}
|
|
3304
3390
|
function resolveToolPath(filePath, projectRoot) {
|
|
3305
3391
|
const trimmed = filePath.trim();
|
|
3306
|
-
return
|
|
3392
|
+
return path11.isAbsolute(trimmed) ? path11.relative(projectRoot, trimmed) : trimmed.replace(/^\.\//, "");
|
|
3307
3393
|
}
|
|
3308
3394
|
function parseEslintOutput(output, projectRoot) {
|
|
3309
3395
|
const issues = [];
|
|
@@ -3483,7 +3569,7 @@ function formatToolNotAvailable(requested, available) {
|
|
|
3483
3569
|
}
|
|
3484
3570
|
|
|
3485
3571
|
// src/tools/kumaReflect.ts
|
|
3486
|
-
import { execSync as
|
|
3572
|
+
import { execSync as execSync3 } from "child_process";
|
|
3487
3573
|
async function handleReflect(params) {
|
|
3488
3574
|
const summary = sessionMemory.getSummary();
|
|
3489
3575
|
const goal = params.goal || summary.currentGoal || "";
|
|
@@ -3505,7 +3591,7 @@ async function handleReflect(params) {
|
|
|
3505
3591
|
let gitStat = "";
|
|
3506
3592
|
try {
|
|
3507
3593
|
const root = getProjectRoot();
|
|
3508
|
-
gitStat =
|
|
3594
|
+
gitStat = execSync3("git diff --stat", {
|
|
3509
3595
|
cwd: root,
|
|
3510
3596
|
encoding: "utf-8",
|
|
3511
3597
|
timeout: 5e3
|
|
@@ -3576,10 +3662,465 @@ async function handleReflect(params) {
|
|
|
3576
3662
|
);
|
|
3577
3663
|
}
|
|
3578
3664
|
|
|
3665
|
+
// src/tools/kumaGuard.ts
|
|
3666
|
+
import { execSync as execSync6 } from "child_process";
|
|
3667
|
+
|
|
3668
|
+
// src/guards/antiPatternDetector.ts
|
|
3669
|
+
import fs12 from "fs";
|
|
3670
|
+
import path12 from "path";
|
|
3671
|
+
import { execSync as execSync4 } from "child_process";
|
|
3672
|
+
var SCRIPT_PATCH_PATTERNS = [
|
|
3673
|
+
"writeFileSync",
|
|
3674
|
+
"writeFile",
|
|
3675
|
+
"replace(",
|
|
3676
|
+
"replaceAll(",
|
|
3677
|
+
"sed",
|
|
3678
|
+
"awk",
|
|
3679
|
+
"fs.write",
|
|
3680
|
+
"patch",
|
|
3681
|
+
"modify"
|
|
3682
|
+
];
|
|
3683
|
+
var BASH_GREP_PATTERNS = [
|
|
3684
|
+
"grep -rn",
|
|
3685
|
+
"grep -r",
|
|
3686
|
+
"grep -n",
|
|
3687
|
+
"| grep",
|
|
3688
|
+
"ripgrep",
|
|
3689
|
+
"rg ",
|
|
3690
|
+
"ag "
|
|
3691
|
+
];
|
|
3692
|
+
var SCRIPT_EXTENSIONS = [".py", ".js", ".mjs", ".cjs", ".ts"];
|
|
3693
|
+
function findPatchScripts(projectRoot) {
|
|
3694
|
+
const warnings = [];
|
|
3695
|
+
const recentFiles = scanRecentFiles(projectRoot);
|
|
3696
|
+
for (const file of recentFiles) {
|
|
3697
|
+
const ext = path12.extname(file).toLowerCase();
|
|
3698
|
+
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
3699
|
+
const relativePath = path12.relative(projectRoot, file);
|
|
3700
|
+
if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".")) {
|
|
3701
|
+
continue;
|
|
3702
|
+
}
|
|
3703
|
+
try {
|
|
3704
|
+
const content = fs12.readFileSync(file, "utf-8").toLowerCase();
|
|
3705
|
+
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
3706
|
+
if (matchedPattern) {
|
|
3707
|
+
warnings.push({
|
|
3708
|
+
severity: "high",
|
|
3709
|
+
pattern: "script-patching",
|
|
3710
|
+
message: `Created script file that modifies other files: ${path12.basename(file)}`,
|
|
3711
|
+
suggestion: "Use **precise_diff_editor** instead \u2014 it has fuzzy matching, auto-backup, and rollback support",
|
|
3712
|
+
evidence: `File: ${relativePath} contains '${matchedPattern}'`,
|
|
3713
|
+
filePath: relativePath
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
} catch {
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
return warnings;
|
|
3720
|
+
}
|
|
3721
|
+
function detectBashGrepUsage() {
|
|
3722
|
+
const warnings = [];
|
|
3723
|
+
const toolCalls = sessionMemory.getToolCallHistory(100);
|
|
3724
|
+
for (const call of toolCalls) {
|
|
3725
|
+
if (call.toolName !== "execute_safe_test") continue;
|
|
3726
|
+
const cmd = call.params?.customCommand || call.params?.command || "";
|
|
3727
|
+
const matchedPattern = BASH_GREP_PATTERNS.find((p) => cmd.toLowerCase().includes(p));
|
|
3728
|
+
if (matchedPattern) {
|
|
3729
|
+
warnings.push({
|
|
3730
|
+
severity: "medium",
|
|
3731
|
+
pattern: "bash-grep",
|
|
3732
|
+
message: `Used bash grep instead of smart_grep`,
|
|
3733
|
+
suggestion: "Use **smart_grep** \u2014 it returns line numbers + context, caches results, respects .gitignore",
|
|
3734
|
+
evidence: `Command: ${cmd.substring(0, 120)}`
|
|
3735
|
+
});
|
|
3736
|
+
break;
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
return warnings;
|
|
3740
|
+
}
|
|
3741
|
+
function scanRecentFiles(projectRoot) {
|
|
3742
|
+
const recent = [];
|
|
3743
|
+
try {
|
|
3744
|
+
const entries = fs12.readdirSync(projectRoot, { withFileTypes: true });
|
|
3745
|
+
const now = Date.now();
|
|
3746
|
+
const recentThreshold = 30 * 60 * 1e3;
|
|
3747
|
+
for (const entry of entries) {
|
|
3748
|
+
if (!entry.isFile()) continue;
|
|
3749
|
+
try {
|
|
3750
|
+
const stat = fs12.statSync(path12.join(projectRoot, entry.name));
|
|
3751
|
+
if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
|
|
3752
|
+
recent.push(path12.join(projectRoot, entry.name));
|
|
3753
|
+
}
|
|
3754
|
+
} catch {
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
} catch {
|
|
3759
|
+
}
|
|
3760
|
+
return recent;
|
|
3761
|
+
}
|
|
3762
|
+
function detectGitPatchScripts(projectRoot) {
|
|
3763
|
+
const warnings = [];
|
|
3764
|
+
try {
|
|
3765
|
+
const statusStdout = execSync4("git status --porcelain", {
|
|
3766
|
+
cwd: projectRoot,
|
|
3767
|
+
encoding: "utf-8",
|
|
3768
|
+
timeout: 3e3
|
|
3769
|
+
}).trim();
|
|
3770
|
+
if (statusStdout) {
|
|
3771
|
+
const lines = statusStdout.split("\n").filter(Boolean);
|
|
3772
|
+
for (const line of lines) {
|
|
3773
|
+
const prefix = line.substring(0, 2);
|
|
3774
|
+
if (prefix !== "??" && prefix !== "A ") continue;
|
|
3775
|
+
const file = line.substring(3).trim();
|
|
3776
|
+
const ext = path12.extname(file).toLowerCase();
|
|
3777
|
+
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
3778
|
+
const isRootLevel = !file.includes("/");
|
|
3779
|
+
const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
|
|
3780
|
+
if (!isRootLevel && !isScriptsDir) continue;
|
|
3781
|
+
const fullPath = path12.join(projectRoot, file);
|
|
3782
|
+
if (fs12.existsSync(fullPath)) {
|
|
3783
|
+
const content = fs12.readFileSync(fullPath, "utf-8").toLowerCase();
|
|
3784
|
+
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
3785
|
+
if (matchedPattern) {
|
|
3786
|
+
warnings.push({
|
|
3787
|
+
severity: "high",
|
|
3788
|
+
pattern: "script-patching",
|
|
3789
|
+
message: `Patch script detected: ${file}`,
|
|
3790
|
+
suggestion: "Use **precise_diff_editor** instead \u2014 it has fuzzy matching, auto-backup, and rollback support",
|
|
3791
|
+
evidence: `File: ${file} (contains '${matchedPattern}')`,
|
|
3792
|
+
filePath: file
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
const deletedStdout = execSync4("git diff --name-only --diff-filter=D HEAD", {
|
|
3799
|
+
cwd: projectRoot,
|
|
3800
|
+
encoding: "utf-8",
|
|
3801
|
+
timeout: 3e3
|
|
3802
|
+
}).trim();
|
|
3803
|
+
if (deletedStdout) {
|
|
3804
|
+
const deletedFiles = deletedStdout.split("\n").filter(Boolean);
|
|
3805
|
+
for (const file of deletedFiles) {
|
|
3806
|
+
const ext = path12.extname(file).toLowerCase();
|
|
3807
|
+
if (SCRIPT_EXTENSIONS.includes(ext)) {
|
|
3808
|
+
warnings.push({
|
|
3809
|
+
severity: "high",
|
|
3810
|
+
pattern: "script-patching",
|
|
3811
|
+
message: `Patch script was tracked then deleted: ${file}`,
|
|
3812
|
+
suggestion: "Use **precise_diff_editor** instead of creating disposable scripts. It has auto-backup + rollback.",
|
|
3813
|
+
evidence: `Git shows ${file} was deleted`,
|
|
3814
|
+
filePath: file
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
} catch {
|
|
3820
|
+
}
|
|
3821
|
+
return warnings;
|
|
3822
|
+
}
|
|
3823
|
+
function detectBashSedUsage() {
|
|
3824
|
+
const warnings = [];
|
|
3825
|
+
const toolCalls = sessionMemory.getToolCallHistory(100);
|
|
3826
|
+
const SED_PATTERNS = [
|
|
3827
|
+
"sed -i",
|
|
3828
|
+
"sed \\'",
|
|
3829
|
+
"sed '",
|
|
3830
|
+
"awk '",
|
|
3831
|
+
"awk \\'",
|
|
3832
|
+
"cat <<",
|
|
3833
|
+
'>> "$file',
|
|
3834
|
+
">> '$file",
|
|
3835
|
+
'echo " >> ',
|
|
3836
|
+
"echo ' >> ",
|
|
3837
|
+
"printf '%s' >"
|
|
3838
|
+
];
|
|
3839
|
+
for (const call of toolCalls) {
|
|
3840
|
+
if (call.toolName !== "execute_safe_test") continue;
|
|
3841
|
+
const cmd = call.params?.customCommand || call.params?.command || "";
|
|
3842
|
+
if (SED_PATTERNS.some((p) => cmd.includes(p))) {
|
|
3843
|
+
warnings.push({
|
|
3844
|
+
severity: "high",
|
|
3845
|
+
pattern: "bash-sed-editing",
|
|
3846
|
+
message: "Used bash sed/awk to edit source files instead of precise_diff_editor",
|
|
3847
|
+
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})',
|
|
3848
|
+
evidence: `Command: ${cmd.substring(0, 150)}`
|
|
3849
|
+
});
|
|
3850
|
+
break;
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
return warnings;
|
|
3854
|
+
}
|
|
3855
|
+
function detectAllAntiPatterns() {
|
|
3856
|
+
const projectRoot = getProjectRoot();
|
|
3857
|
+
const warnings = [];
|
|
3858
|
+
warnings.push(...findPatchScripts(projectRoot));
|
|
3859
|
+
warnings.push(...detectBashGrepUsage());
|
|
3860
|
+
warnings.push(...detectGitPatchScripts(projectRoot));
|
|
3861
|
+
warnings.push(...detectBashSedUsage());
|
|
3862
|
+
return warnings;
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
// src/engine/contextSnapshot.ts
|
|
3866
|
+
import fs13 from "fs";
|
|
3867
|
+
import path13 from "path";
|
|
3868
|
+
import { execSync as execSync5 } from "child_process";
|
|
3869
|
+
var SNAPSHOT_DIR = "context-snapshots";
|
|
3870
|
+
function snapshotDir() {
|
|
3871
|
+
return path13.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
|
|
3872
|
+
}
|
|
3873
|
+
function ensureSnapshotDir() {
|
|
3874
|
+
const dir = snapshotDir();
|
|
3875
|
+
if (!fs13.existsSync(dir)) {
|
|
3876
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
function snapshotFilePath(timestamp) {
|
|
3880
|
+
return path13.join(snapshotDir(), `${timestamp}.json`);
|
|
3881
|
+
}
|
|
3882
|
+
function saveSnapshot(goal) {
|
|
3883
|
+
try {
|
|
3884
|
+
ensureSnapshotDir();
|
|
3885
|
+
} catch {
|
|
3886
|
+
return null;
|
|
3887
|
+
}
|
|
3888
|
+
const summary = sessionMemory.getSummary();
|
|
3889
|
+
const snapshot = {
|
|
3890
|
+
version: 1,
|
|
3891
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3892
|
+
goal: goal || summary.currentGoal || "",
|
|
3893
|
+
modifiedFiles: summary.modifiedFiles || [],
|
|
3894
|
+
unresolvedFailures: summary.unresolvedFailures || [],
|
|
3895
|
+
toolCallCount: summary.toolCallCount || 0,
|
|
3896
|
+
gitDiffStat: getGitDiffStat(),
|
|
3897
|
+
completedSteps: summary.completedSteps || [],
|
|
3898
|
+
hasConventions: !!summary.hasConventions
|
|
3899
|
+
};
|
|
3900
|
+
try {
|
|
3901
|
+
fs13.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
|
|
3902
|
+
} catch {
|
|
3903
|
+
}
|
|
3904
|
+
sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
|
|
3905
|
+
return snapshot;
|
|
3906
|
+
}
|
|
3907
|
+
function listSnapshots() {
|
|
3908
|
+
const dir = snapshotDir();
|
|
3909
|
+
if (!fs13.existsSync(dir)) return [];
|
|
3910
|
+
try {
|
|
3911
|
+
const files = fs13.readdirSync(dir).filter((f) => f.endsWith(".json")).sort((a, b) => b.localeCompare(a));
|
|
3912
|
+
return files.map((f) => {
|
|
3913
|
+
try {
|
|
3914
|
+
const content = fs13.readFileSync(path13.join(dir, f), "utf-8");
|
|
3915
|
+
return JSON.parse(content);
|
|
3916
|
+
} catch {
|
|
3917
|
+
return null;
|
|
3918
|
+
}
|
|
3919
|
+
}).filter((s) => s !== null);
|
|
3920
|
+
} catch {
|
|
3921
|
+
return [];
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
function formatSnapshot(snapshot) {
|
|
3925
|
+
const lines = [
|
|
3926
|
+
`\u{1F4F8} **Context Snapshot** \u2014 ${snapshot.timestamp}`,
|
|
3927
|
+
"",
|
|
3928
|
+
`\u{1F3AF} **Goal:** ${snapshot.goal || "(not set)"}`,
|
|
3929
|
+
`\u{1F4C1} **Modified Files:** ${snapshot.modifiedFiles.length}`,
|
|
3930
|
+
`\u274C **Unresolved Failures:** ${snapshot.unresolvedFailures.length}`,
|
|
3931
|
+
`\u{1F527} **Tool Calls:** ${snapshot.toolCallCount}`,
|
|
3932
|
+
`\u{1F4CA} **Git Diff:** ${snapshot.gitDiffStat || "clean"}`,
|
|
3933
|
+
`\u2705 **Completed Steps:** ${snapshot.completedSteps.length}`,
|
|
3934
|
+
`\u{1F4D0} **Conventions:** ${snapshot.hasConventions ? "detected" : "not detected"}`,
|
|
3935
|
+
""
|
|
3936
|
+
];
|
|
3937
|
+
if (snapshot.modifiedFiles.length > 0) {
|
|
3938
|
+
lines.push("**Modified Files:**");
|
|
3939
|
+
for (const f of snapshot.modifiedFiles) {
|
|
3940
|
+
lines.push(` - ${f.filePath} (${f.status})`);
|
|
3941
|
+
}
|
|
3942
|
+
lines.push("");
|
|
3943
|
+
}
|
|
3944
|
+
if (snapshot.unresolvedFailures.length > 0) {
|
|
3945
|
+
lines.push("**Unresolved Failures:**");
|
|
3946
|
+
for (const f of snapshot.unresolvedFailures) {
|
|
3947
|
+
lines.push(` - [${f.task}] ${f.error.substring(0, 150)}`);
|
|
3948
|
+
}
|
|
3949
|
+
lines.push("");
|
|
3950
|
+
}
|
|
3951
|
+
lines.push('\u{1F4A1} Use kuma_guard({ check: "context" }) to create a new snapshot.');
|
|
3952
|
+
return lines.join("\n");
|
|
3953
|
+
}
|
|
3954
|
+
function getGitDiffStat() {
|
|
3955
|
+
try {
|
|
3956
|
+
const root = getProjectRoot();
|
|
3957
|
+
const stdout = execSync5("git diff --stat", {
|
|
3958
|
+
cwd: root,
|
|
3959
|
+
encoding: "utf-8",
|
|
3960
|
+
timeout: 3e3
|
|
3961
|
+
}).trim();
|
|
3962
|
+
return stdout || "clean";
|
|
3963
|
+
} catch {
|
|
3964
|
+
return "clean";
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
// src/tools/kumaGuard.ts
|
|
3969
|
+
async function handleKumaGuard(params) {
|
|
3970
|
+
const { check = "all", goal: inputGoal } = params;
|
|
3971
|
+
const summary = sessionMemory.getSummary();
|
|
3972
|
+
const goal = inputGoal || summary.currentGoal || "";
|
|
3973
|
+
sessionMemory.recordToolCall("kuma_guard", { check, goal });
|
|
3974
|
+
const warnings = [];
|
|
3975
|
+
if (check === "all" || check === "anti-pattern") {
|
|
3976
|
+
warnings.push(...detectAllAntiPatterns());
|
|
3977
|
+
}
|
|
3978
|
+
const loop = check === "all" || check === "loop" ? sessionMemory.detectLoop() : { isLooping: false };
|
|
3979
|
+
if (loop.isLooping) {
|
|
3980
|
+
warnings.push({
|
|
3981
|
+
severity: "high",
|
|
3982
|
+
pattern: "tool-loop",
|
|
3983
|
+
message: loop.message ?? "Detected potential tool call loop",
|
|
3984
|
+
suggestion: "Switch approach \u2014 try reading the file first with smart_file_picker"
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
const drifts = [];
|
|
3988
|
+
const toolCalls = sessionMemory.getToolCallHistory(50);
|
|
3989
|
+
const hasRunTests = toolCalls.some((c) => c.toolName === "execute_safe_test");
|
|
3990
|
+
if (check === "all" || check === "drift") {
|
|
3991
|
+
const modifiedFiles = sessionMemory.getModifiedFiles();
|
|
3992
|
+
const failedFiles = sessionMemory.getFailedFiles();
|
|
3993
|
+
let unresolvedCount = 0;
|
|
3994
|
+
for (const f of failedFiles) {
|
|
3995
|
+
for (const ff of f.failures) {
|
|
3996
|
+
if (!ff.resolved) unresolvedCount++;
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
if (modifiedFiles.length > 0 && !hasRunTests) {
|
|
4000
|
+
drifts.push(`${modifiedFiles.length} file(s) edited but no test run`);
|
|
4001
|
+
warnings.push({
|
|
4002
|
+
severity: "medium",
|
|
4003
|
+
pattern: "no-test-after-edit",
|
|
4004
|
+
message: `${modifiedFiles.length} file(s) modified without running tests`,
|
|
4005
|
+
suggestion: 'Run execute_safe_test({ task: "typecheck" }) to verify changes'
|
|
4006
|
+
});
|
|
4007
|
+
}
|
|
4008
|
+
if (unresolvedCount > 0) {
|
|
4009
|
+
drifts.push(`${unresolvedCount} unresolved failure(s)`);
|
|
4010
|
+
}
|
|
4011
|
+
try {
|
|
4012
|
+
const root = getProjectRoot();
|
|
4013
|
+
const gitStat = execSync6("git diff --stat", {
|
|
4014
|
+
cwd: root,
|
|
4015
|
+
encoding: "utf-8",
|
|
4016
|
+
timeout: 3e3
|
|
4017
|
+
}).trim();
|
|
4018
|
+
if (gitStat) {
|
|
4019
|
+
drifts.push(`Git diff: ${gitStat}`);
|
|
4020
|
+
}
|
|
4021
|
+
} catch {
|
|
4022
|
+
}
|
|
4023
|
+
const editCalls = toolCalls.filter(
|
|
4024
|
+
(c) => c.toolName === "precise_diff_editor" || c.toolName === "batch_file_writer"
|
|
4025
|
+
).length;
|
|
4026
|
+
if (editCalls > 5) {
|
|
4027
|
+
warnings.push({
|
|
4028
|
+
severity: "low",
|
|
4029
|
+
pattern: "excessive-edits",
|
|
4030
|
+
message: `${editCalls} file operations in a row`,
|
|
4031
|
+
suggestion: "Consider if all edits are needed. Run tests before making more changes."
|
|
4032
|
+
});
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
if (check === "context") {
|
|
4036
|
+
const snapshot = saveSnapshot(goal);
|
|
4037
|
+
if (!snapshot) {
|
|
4038
|
+
return "\u26A0\uFE0F Could not create context snapshot. The .kuma directory might not be accessible.";
|
|
4039
|
+
}
|
|
4040
|
+
return formatSnapshot(snapshot);
|
|
4041
|
+
}
|
|
4042
|
+
const hasWarnings = warnings.length > 0;
|
|
4043
|
+
const hasDrifts = drifts.length > 0;
|
|
4044
|
+
const onTrack = !hasWarnings && !hasDrifts;
|
|
4045
|
+
let suggestion;
|
|
4046
|
+
if (warnings.some((w) => w.severity === "high" && w.pattern === "script-patching")) {
|
|
4047
|
+
suggestion = "Remove patch scripts and use precise_diff_editor for all file modifications";
|
|
4048
|
+
} else if (warnings.some((w) => w.pattern === "tool-loop")) {
|
|
4049
|
+
suggestion = "Switch approach \u2014 current tool is not making progress";
|
|
4050
|
+
} else if (warnings.some((w) => w.pattern === "no-test-after-edit")) {
|
|
4051
|
+
suggestion = "Run tests to verify your changes before continuing";
|
|
4052
|
+
} else if (warnings.some((w) => w.pattern === "bash-grep")) {
|
|
4053
|
+
suggestion = "Use smart_grep for code search instead of bash grep";
|
|
4054
|
+
} else if (warnings.some((w) => w.pattern === "excessive-edits")) {
|
|
4055
|
+
suggestion = "Pause and review: are all these edits necessary?";
|
|
4056
|
+
} else if (!goal) {
|
|
4057
|
+
suggestion = "No goal set \u2014 use goal parameter or setGoal to track intent";
|
|
4058
|
+
} else {
|
|
4059
|
+
suggestion = "On track \u2014 continue with current approach";
|
|
4060
|
+
}
|
|
4061
|
+
const report = {
|
|
4062
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4063
|
+
onTrack,
|
|
4064
|
+
warnings,
|
|
4065
|
+
drifts,
|
|
4066
|
+
suggestion,
|
|
4067
|
+
stats: {
|
|
4068
|
+
goal,
|
|
4069
|
+
modifiedFiles: summary.modifiedFiles.length,
|
|
4070
|
+
toolCalls: summary.toolCallCount ?? 0,
|
|
4071
|
+
unresolvedFailures: summary.unresolvedFailures ? summary.unresolvedFailures.length : 0,
|
|
4072
|
+
hasLoop: loop.isLooping,
|
|
4073
|
+
hasRunTests
|
|
4074
|
+
}
|
|
4075
|
+
};
|
|
4076
|
+
return JSON.stringify(report, null, 2);
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
// src/tools/kumaContext.ts
|
|
4080
|
+
async function handleKumaContext(params) {
|
|
4081
|
+
const { action, goal } = params;
|
|
4082
|
+
switch (action) {
|
|
4083
|
+
case "save": {
|
|
4084
|
+
const snapshot = saveSnapshot(goal);
|
|
4085
|
+
if (!snapshot) {
|
|
4086
|
+
return "\u26A0\uFE0F Could not create context snapshot. The .kuma directory might not be accessible.";
|
|
4087
|
+
}
|
|
4088
|
+
return formatSnapshot(snapshot);
|
|
4089
|
+
}
|
|
4090
|
+
case "list": {
|
|
4091
|
+
const snapshots = listSnapshots();
|
|
4092
|
+
if (snapshots.length === 0) {
|
|
4093
|
+
return '\u{1F4F8} **No snapshots found.**\n\nRun `kuma_guard({ check: "context" })` or `kuma_context({ action: "save" })` to create one.';
|
|
4094
|
+
}
|
|
4095
|
+
const lines = [
|
|
4096
|
+
`\u{1F4F8} **Context Snapshots** \u2014 ${snapshots.length} available`,
|
|
4097
|
+
""
|
|
4098
|
+
];
|
|
4099
|
+
for (let i = 0; i < snapshots.length; i++) {
|
|
4100
|
+
const s = snapshots[i];
|
|
4101
|
+
const fileCount = s.modifiedFiles.length;
|
|
4102
|
+
const failureCount = s.unresolvedFailures.length;
|
|
4103
|
+
lines.push(
|
|
4104
|
+
`[${i + 1}] ${s.timestamp}`,
|
|
4105
|
+
` \u{1F3AF} Goal: ${s.goal || "(not set)"}`,
|
|
4106
|
+
` \u{1F4C1} ${fileCount} files, \u274C ${failureCount} failures, \u{1F527} ${s.toolCallCount} tool calls`,
|
|
4107
|
+
` \u{1F4CA} Git: ${s.gitDiffStat || "clean"}`,
|
|
4108
|
+
""
|
|
4109
|
+
);
|
|
4110
|
+
}
|
|
4111
|
+
lines.push('\u{1F4A1} Use kuma_context({ action: "save" }) to create a new snapshot.');
|
|
4112
|
+
lines.push('\u{1F4A1} Or kuma_guard({ check: "context" }) to save and get full context.');
|
|
4113
|
+
return lines.join("\n");
|
|
4114
|
+
}
|
|
4115
|
+
default:
|
|
4116
|
+
return `Error: Action "${action}" not supported. Use "save" or "list".`;
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
|
|
3579
4120
|
// src/engine/lspClient.ts
|
|
3580
|
-
import { spawn as
|
|
3581
|
-
import
|
|
3582
|
-
import
|
|
4121
|
+
import { spawn as spawn2 } from "child_process";
|
|
4122
|
+
import path14 from "path";
|
|
4123
|
+
import fs14 from "fs";
|
|
3583
4124
|
var LSPClient = class {
|
|
3584
4125
|
process = null;
|
|
3585
4126
|
requestId = 0;
|
|
@@ -3617,11 +4158,11 @@ var LSPClient = class {
|
|
|
3617
4158
|
console.error(`[LSP] typescript-language-server not found. LSP features will fallback to regex.`);
|
|
3618
4159
|
return;
|
|
3619
4160
|
}
|
|
3620
|
-
const tsconfigPath =
|
|
3621
|
-
if (!
|
|
4161
|
+
const tsconfigPath = path14.join(root, "tsconfig.json");
|
|
4162
|
+
if (!fs14.existsSync(tsconfigPath)) {
|
|
3622
4163
|
console.error(`[LSP] Warning: tsconfig.json not found at "${root}". Running in implicit project mode.`);
|
|
3623
4164
|
}
|
|
3624
|
-
this.process =
|
|
4165
|
+
this.process = spawn2(lspBinary, ["--stdio", "--log-level=4"], {
|
|
3625
4166
|
cwd: root,
|
|
3626
4167
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3627
4168
|
env: { ...process.env }
|
|
@@ -3680,19 +4221,19 @@ var LSPClient = class {
|
|
|
3680
4221
|
/** Resolve the typescript-language-server binary from local or global install */
|
|
3681
4222
|
resolveLspBinary(projectRoot) {
|
|
3682
4223
|
const candidates = [
|
|
3683
|
-
|
|
3684
|
-
|
|
4224
|
+
path14.join(projectRoot, "node_modules", ".bin", "typescript-language-server"),
|
|
4225
|
+
path14.join(projectRoot, "..", "node_modules", ".bin", "typescript-language-server")
|
|
3685
4226
|
];
|
|
3686
4227
|
for (const candidate of candidates) {
|
|
3687
|
-
if (
|
|
4228
|
+
if (fs14.existsSync(candidate)) {
|
|
3688
4229
|
return candidate;
|
|
3689
4230
|
}
|
|
3690
4231
|
}
|
|
3691
4232
|
try {
|
|
3692
4233
|
const envPath = process.env.PATH ?? "";
|
|
3693
|
-
for (const dir of envPath.split(
|
|
3694
|
-
const binPath =
|
|
3695
|
-
if (
|
|
4234
|
+
for (const dir of envPath.split(path14.delimiter)) {
|
|
4235
|
+
const binPath = path14.join(dir, "typescript-language-server");
|
|
4236
|
+
if (fs14.existsSync(binPath)) {
|
|
3696
4237
|
return binPath;
|
|
3697
4238
|
}
|
|
3698
4239
|
}
|
|
@@ -3713,7 +4254,7 @@ var LSPClient = class {
|
|
|
3713
4254
|
this.openDocuments.add(filePath);
|
|
3714
4255
|
let content;
|
|
3715
4256
|
try {
|
|
3716
|
-
content =
|
|
4257
|
+
content = fs14.readFileSync(filePath, "utf-8");
|
|
3717
4258
|
} catch {
|
|
3718
4259
|
content = "";
|
|
3719
4260
|
}
|
|
@@ -3930,8 +4471,8 @@ var LSPClient = class {
|
|
|
3930
4471
|
var lspClient = new LSPClient();
|
|
3931
4472
|
|
|
3932
4473
|
// src/tools/lspTools.ts
|
|
3933
|
-
import
|
|
3934
|
-
import
|
|
4474
|
+
import fs15 from "fs";
|
|
4475
|
+
import path15 from "path";
|
|
3935
4476
|
async function handleFindReferences(params) {
|
|
3936
4477
|
const { filePath, line, character } = params;
|
|
3937
4478
|
const validation = validateFilePath(filePath);
|
|
@@ -3939,7 +4480,7 @@ async function handleFindReferences(params) {
|
|
|
3939
4480
|
return `Error: ${validation.error.message}`;
|
|
3940
4481
|
}
|
|
3941
4482
|
const resolvedPath = validation.resolvedPath;
|
|
3942
|
-
if (!
|
|
4483
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
3943
4484
|
return `Error: File not found: "${filePath}".`;
|
|
3944
4485
|
}
|
|
3945
4486
|
if (!lspClient.isAvailable()) {
|
|
@@ -3961,7 +4502,7 @@ async function handleFindReferences(params) {
|
|
|
3961
4502
|
const enrichedRefs = references.map((ref) => {
|
|
3962
4503
|
let lineContent = "";
|
|
3963
4504
|
try {
|
|
3964
|
-
const content =
|
|
4505
|
+
const content = fs15.readFileSync(ref.filePath, "utf-8");
|
|
3965
4506
|
const lines2 = content.split("\n");
|
|
3966
4507
|
lineContent = lines2[ref.line]?.trim() ?? "";
|
|
3967
4508
|
} catch {
|
|
@@ -3977,11 +4518,11 @@ async function handleFindReferences(params) {
|
|
|
3977
4518
|
const projectRoot = getProjectRoot();
|
|
3978
4519
|
const lines = [
|
|
3979
4520
|
`\u{1F50D} **Find References** \u2014 ${enrichedRefs.length} references found`,
|
|
3980
|
-
`\u{1F4CD} File: ${
|
|
4521
|
+
`\u{1F4CD} File: ${path15.relative(projectRoot, resolvedPath)}:${line + 1}:${character + 1}`,
|
|
3981
4522
|
""
|
|
3982
4523
|
];
|
|
3983
4524
|
for (const [file, refs] of grouped) {
|
|
3984
|
-
const relPath =
|
|
4525
|
+
const relPath = path15.relative(projectRoot, file);
|
|
3985
4526
|
lines.push(`**\u{1F4C4} ${relPath}:**`);
|
|
3986
4527
|
for (const ref of refs) {
|
|
3987
4528
|
const loc = `L${ref.line + 1}:${ref.character + 1}`;
|
|
@@ -4002,7 +4543,7 @@ async function handleGoToDefinition(params) {
|
|
|
4002
4543
|
return `Error: ${validation.error.message}`;
|
|
4003
4544
|
}
|
|
4004
4545
|
const resolvedPath = validation.resolvedPath;
|
|
4005
|
-
if (!
|
|
4546
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
4006
4547
|
return `Error: File not found: "${filePath}".`;
|
|
4007
4548
|
}
|
|
4008
4549
|
if (!lspClient.isAvailable()) {
|
|
@@ -4022,10 +4563,10 @@ async function handleGoToDefinition(params) {
|
|
|
4022
4563
|
\u26A0\uFE0F Cannot find definition for symbol at this position.`;
|
|
4023
4564
|
}
|
|
4024
4565
|
const projectRoot = getProjectRoot();
|
|
4025
|
-
const relPath =
|
|
4566
|
+
const relPath = path15.relative(projectRoot, definition.filePath);
|
|
4026
4567
|
let lineContent = "";
|
|
4027
4568
|
try {
|
|
4028
|
-
const content =
|
|
4569
|
+
const content = fs15.readFileSync(definition.filePath, "utf-8");
|
|
4029
4570
|
const lines2 = content.split("\n");
|
|
4030
4571
|
lineContent = lines2[definition.line]?.trim() ?? "";
|
|
4031
4572
|
} catch {
|
|
@@ -4057,7 +4598,7 @@ async function handleRenameSymbol(params) {
|
|
|
4057
4598
|
return `Error: ${validation.error.message}`;
|
|
4058
4599
|
}
|
|
4059
4600
|
const resolvedPath = validation.resolvedPath;
|
|
4060
|
-
if (!
|
|
4601
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
4061
4602
|
return `Error: File not found: "${filePath}".`;
|
|
4062
4603
|
}
|
|
4063
4604
|
if (!lspClient.isAvailable()) {
|
|
@@ -4086,7 +4627,7 @@ Make sure:
|
|
|
4086
4627
|
const fileChanges = [];
|
|
4087
4628
|
for (const change of result.changes) {
|
|
4088
4629
|
try {
|
|
4089
|
-
const content =
|
|
4630
|
+
const content = fs15.readFileSync(change.filePath, "utf-8");
|
|
4090
4631
|
const lines2 = content.split("\n");
|
|
4091
4632
|
const sortedEdits = [...change.edits].sort((a, b) => {
|
|
4092
4633
|
if (b.line !== a.line) return b.line - a.line;
|
|
@@ -4100,10 +4641,10 @@ Make sure:
|
|
|
4100
4641
|
lines2[edit.line] = before + edit.newText + after;
|
|
4101
4642
|
}
|
|
4102
4643
|
}
|
|
4103
|
-
|
|
4644
|
+
fs15.writeFileSync(change.filePath, lines2.join("\n"), "utf-8");
|
|
4104
4645
|
totalEdits += change.edits.length;
|
|
4105
4646
|
fileChanges.push({
|
|
4106
|
-
filePath:
|
|
4647
|
+
filePath: path15.relative(projectRoot, change.filePath),
|
|
4107
4648
|
editCount: change.edits.length
|
|
4108
4649
|
});
|
|
4109
4650
|
} catch (err) {
|
|
@@ -4130,13 +4671,13 @@ async function handleGetTypeInfo(params) {
|
|
|
4130
4671
|
return `Error: ${validation.error.message}`;
|
|
4131
4672
|
}
|
|
4132
4673
|
const resolvedPath = validation.resolvedPath;
|
|
4133
|
-
if (!
|
|
4674
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
4134
4675
|
return `Error: File not found: "${filePath}".`;
|
|
4135
4676
|
}
|
|
4136
4677
|
if (!lspClient.isAvailable()) {
|
|
4137
4678
|
sessionMemory.recordToolCall("lsp_query", { action: "type", filePath, line, character, fallback: "line-context" });
|
|
4138
4679
|
try {
|
|
4139
|
-
const fileContent =
|
|
4680
|
+
const fileContent = fs15.readFileSync(resolvedPath, "utf-8");
|
|
4140
4681
|
const fileLines = fileContent.split("\n");
|
|
4141
4682
|
const targetLine = fileLines[line];
|
|
4142
4683
|
if (!targetLine) {
|
|
@@ -4144,7 +4685,7 @@ async function handleGetTypeInfo(params) {
|
|
|
4144
4685
|
}
|
|
4145
4686
|
const symbolName = extractSymbolAtPosition(resolvedPath, line, character);
|
|
4146
4687
|
const projectRoot = getProjectRoot();
|
|
4147
|
-
const relPath =
|
|
4688
|
+
const relPath = path15.relative(projectRoot, resolvedPath);
|
|
4148
4689
|
const contextStart = Math.max(0, line - 3);
|
|
4149
4690
|
const contextEnd = Math.min(fileLines.length, line + 4);
|
|
4150
4691
|
const contextLines = [];
|
|
@@ -4182,7 +4723,7 @@ async function handleGetTypeInfo(params) {
|
|
|
4182
4723
|
\u26A0\uFE0F No type info for this position.`;
|
|
4183
4724
|
}
|
|
4184
4725
|
const projectRoot = getProjectRoot();
|
|
4185
|
-
const relPath =
|
|
4726
|
+
const relPath = path15.relative(projectRoot, resolvedPath);
|
|
4186
4727
|
const lines = [
|
|
4187
4728
|
`\u{1F4CB} **Type Info** \u2014 \`${relPath}:${line + 1}:${character + 1}\``,
|
|
4188
4729
|
"",
|
|
@@ -4223,7 +4764,7 @@ async function handleLspQuery(params) {
|
|
|
4223
4764
|
}
|
|
4224
4765
|
function extractSymbolAtPosition(filePath, line, character) {
|
|
4225
4766
|
try {
|
|
4226
|
-
const content =
|
|
4767
|
+
const content = fs15.readFileSync(filePath, "utf-8");
|
|
4227
4768
|
const lines = content.split("\n");
|
|
4228
4769
|
const targetLine = lines[line];
|
|
4229
4770
|
if (!targetLine) return null;
|
|
@@ -4254,7 +4795,7 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4254
4795
|
const regex = new RegExp(`\\b${escapedSymbol}\\b`, "g");
|
|
4255
4796
|
for (const file of tsFiles.slice(0, 100)) {
|
|
4256
4797
|
try {
|
|
4257
|
-
const content =
|
|
4798
|
+
const content = fs15.readFileSync(file, "utf-8");
|
|
4258
4799
|
const lines2 = content.split("\n");
|
|
4259
4800
|
for (let i = 0; i < lines2.length; i++) {
|
|
4260
4801
|
if (regex.test(lines2[i])) {
|
|
@@ -4283,8 +4824,28 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4283
4824
|
`\u26A0\uFE0F Regex results may be less accurate than LSP (includes comments/strings).`,
|
|
4284
4825
|
""
|
|
4285
4826
|
];
|
|
4827
|
+
if (grouped.size > 1) {
|
|
4828
|
+
const fileList = [...grouped.keys()].map((f) => path15.relative(projectRoot, f)).join(", ");
|
|
4829
|
+
lines.push(`\u26A0\uFE0F **Ambiguity warning:** Found matches across ${grouped.size} different files (${fileList}).`);
|
|
4830
|
+
lines.push(` Regex fallback cannot distinguish between different scopes with the same symbol name.`);
|
|
4831
|
+
lines.push(` If you intended only one scope, clarify which file or location you mean.`);
|
|
4832
|
+
lines.push(` \u{1F4A1} Install typescript-language-server for scope-aware disambiguation.`);
|
|
4833
|
+
lines.push("");
|
|
4834
|
+
} else if (results.length > 1) {
|
|
4835
|
+
const filePath = [...grouped.keys()][0];
|
|
4836
|
+
const refs = grouped.get(filePath);
|
|
4837
|
+
const matchLines = refs.map((r) => r.line);
|
|
4838
|
+
const minLine = Math.min(...matchLines);
|
|
4839
|
+
const maxLine = Math.max(...matchLines);
|
|
4840
|
+
if (maxLine - minLine > 15) {
|
|
4841
|
+
lines.push(`\u26A0\uFE0F **Ambiguity warning:** Found ${results.length} matches for "${symbolName}" spanning lines ${minLine}-${maxLine} in the same file.`);
|
|
4842
|
+
lines.push(` They may be in different function scopes \u2014 regex cannot distinguish them.`);
|
|
4843
|
+
lines.push(` \u{1F4A1} Verify each match is the intended symbol before editing.`);
|
|
4844
|
+
lines.push("");
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4286
4847
|
for (const [file, refs] of grouped) {
|
|
4287
|
-
const relPath =
|
|
4848
|
+
const relPath = path15.relative(projectRoot, file);
|
|
4288
4849
|
lines.push(`**\u{1F4C4} ${relPath}:**`);
|
|
4289
4850
|
for (const ref of refs) {
|
|
4290
4851
|
lines.push(` \u2514 L${ref.line} \u2014 ${ref.content}`);
|
|
@@ -4318,14 +4879,14 @@ async function fallbackGrepDefinition(symbolName) {
|
|
|
4318
4879
|
];
|
|
4319
4880
|
for (const file of tsFiles) {
|
|
4320
4881
|
try {
|
|
4321
|
-
const content =
|
|
4882
|
+
const content = fs15.readFileSync(file, "utf-8");
|
|
4322
4883
|
const lines = content.split("\n");
|
|
4323
4884
|
for (let i = 0; i < lines.length; i++) {
|
|
4324
4885
|
const trimmed = lines[i].trim();
|
|
4325
4886
|
for (const pattern of declPatterns) {
|
|
4326
4887
|
if (pattern.test(trimmed)) {
|
|
4327
4888
|
const projectRoot = getProjectRoot();
|
|
4328
|
-
const relPath =
|
|
4889
|
+
const relPath = path15.relative(projectRoot, file);
|
|
4329
4890
|
return [
|
|
4330
4891
|
`\u{1F4CD} **Go to Definition** (regex fallback)`,
|
|
4331
4892
|
`\u{1F4C4} File: \`${relPath}\``,
|
|
@@ -4356,7 +4917,7 @@ function registerAllTools(server) {
|
|
|
4356
4917
|
"smart_grep",
|
|
4357
4918
|
"Narrow down to the specific code you need. Locates functions or text patterns \u2014 returns filename, line number, and 3 lines of context.",
|
|
4358
4919
|
{
|
|
4359
|
-
query: z.string().min(1).describe("Regex pattern to search for"),
|
|
4920
|
+
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
4921
|
targetFolder: z.string().optional().describe("Target folder (default: project root, max depth 3)"),
|
|
4361
4922
|
maxResults: z.number().min(1).max(100).optional().default(30).describe("Max results to return"),
|
|
4362
4923
|
extensions: z.array(z.string()).optional().describe("Filter results by file extensions (e.g. ['ts', 'js'])")
|
|
@@ -4392,15 +4953,17 @@ function registerAllTools(server) {
|
|
|
4392
4953
|
"precise_diff_editor",
|
|
4393
4954
|
"Edit code with safety net. Search-and-replace with fuzzy fallback + automatic versioned backup. Use action:'rollback' to undo edits.",
|
|
4394
4955
|
{
|
|
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"),
|
|
4956
|
+
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"),
|
|
4957
|
+
action: z.enum(["rollback"]).optional().describe("Set to 'rollback' to restore from backup. Only use when restoring from backup."),
|
|
4397
4958
|
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"),
|
|
4959
|
+
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)"),
|
|
4960
|
+
replaceBlock: z.string({ invalid_type_error: '"replaceBlock" must be a string of the replacement code' }).describe("Replacement code block"),
|
|
4400
4961
|
allowMultiple: z.boolean().optional().default(false).describe("Allow multiple replacements"),
|
|
4401
4962
|
fuzzyThreshold: z.number().min(0).max(1).optional().default(0.85).describe("Fuzzy match threshold (0.0-1.0)")
|
|
4402
|
-
})
|
|
4403
|
-
|
|
4963
|
+
}), {
|
|
4964
|
+
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}'
|
|
4965
|
+
}).min(1).max(10).optional().describe("Array of edits (max 10). Each edit has: searchBlock (code to find), replaceBlock (new code)"),
|
|
4966
|
+
dryRun: z.boolean().optional().default(false).describe("Preview changes without writing to disk. Set dryRun: true to preview first."),
|
|
4404
4967
|
version: z.union([z.number().min(1), z.literal("list")]).optional().describe("Backup version to restore (1=newest, omit=latest, 'list'=show versions)")
|
|
4405
4968
|
},
|
|
4406
4969
|
async (params) => {
|
|
@@ -4420,7 +4983,10 @@ function registerAllTools(server) {
|
|
|
4420
4983
|
filePath: z.string().min(1).describe("File path to create"),
|
|
4421
4984
|
content: z.string().describe("File content"),
|
|
4422
4985
|
instructions: z.string().min(1).describe("Reason for creating the file")
|
|
4423
|
-
})
|
|
4986
|
+
}), {
|
|
4987
|
+
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}',
|
|
4988
|
+
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}'
|
|
4989
|
+
}).min(1).max(15).describe("Array of files to create (max 15). Example: [{ filePath: 'src/x.ts', content: '...', instructions: 'why' }]")
|
|
4424
4990
|
},
|
|
4425
4991
|
async (params) => {
|
|
4426
4992
|
try {
|
|
@@ -4433,11 +4999,13 @@ function registerAllTools(server) {
|
|
|
4433
4999
|
);
|
|
4434
5000
|
server.tool(
|
|
4435
5001
|
"execute_safe_test",
|
|
4436
|
-
"Run tests, lint, or typecheck with timeout protection and circuit breaker. Use after every edit to verify you didn't break anything.",
|
|
5002
|
+
"Run tests, lint, or typecheck with timeout protection and circuit breaker. Supports monorepo workspaces via 'workspace' or relative 'cwd'. Use after every edit to verify you didn't break anything.",
|
|
4437
5003
|
{
|
|
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)")
|
|
5004
|
+
task: z.enum(["test", "build", "lint", "typecheck", "custom"]).describe("Task to execute: test, build, lint, typecheck, or custom"),
|
|
5005
|
+
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')"),
|
|
5006
|
+
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)"),
|
|
5007
|
+
cwd: z.string().optional().describe("Relative directory from project root (e.g. 'packages/web'). Defaults to project root."),
|
|
5008
|
+
workspace: z.string().optional().describe("Workspace name from project_conventions (e.g. 'frontend', 'api'). Auto-resolves to path.")
|
|
4441
5009
|
},
|
|
4442
5010
|
async (params) => {
|
|
4443
5011
|
try {
|
|
@@ -4485,7 +5053,7 @@ function registerAllTools(server) {
|
|
|
4485
5053
|
"get_session_memory",
|
|
4486
5054
|
"Check what you've done this session: modified files, unresolved failures, tool history, memories. Accepts optional topic to load a specific memory file.",
|
|
4487
5055
|
{
|
|
4488
|
-
topic: z.enum(MEMORY_TOPICS).optional().describe("Load a specific memory topic
|
|
5056
|
+
topic: z.enum(MEMORY_TOPICS).optional().describe("Load a specific memory topic: decisions, glossary, architecture, conventions, known-issues. Example: { topic: 'decisions' }")
|
|
4489
5057
|
},
|
|
4490
5058
|
async (params) => {
|
|
4491
5059
|
try {
|
|
@@ -4500,11 +5068,11 @@ function registerAllTools(server) {
|
|
|
4500
5068
|
"lsp_query",
|
|
4501
5069
|
"Jump to definition, find references, inspect types, or rename symbols. Uses TypeScript Language Server \u2014 falls back to regex when LSP is unavailable.",
|
|
4502
5070
|
{
|
|
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')")
|
|
5071
|
+
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"),
|
|
5072
|
+
line: z.number({ invalid_type_error: '"line" must be a number (0-indexed). Line 0 = first line.' }).min(0).describe("Line number (0-indexed)"),
|
|
5073
|
+
character: z.number({ invalid_type_error: '"character" must be a number (0-indexed). Position within the line.' }).min(0).describe("Character position (0-indexed)"),
|
|
5074
|
+
action: z.enum(["def", "refs", "type", "rename"]).describe("Action: def=go to definition, refs=find references, type=get type info, rename=rename symbol"),
|
|
5075
|
+
newName: z.string().optional().describe("New name (required for action:'rename'). Example: { action: 'rename', newName: 'newFunctionName' }")
|
|
4508
5076
|
},
|
|
4509
5077
|
async (params) => {
|
|
4510
5078
|
try {
|
|
@@ -4612,7 +5180,23 @@ function registerAllTools(server) {
|
|
|
4612
5180
|
const result = searchSessionMemory(params);
|
|
4613
5181
|
return { content: [{ type: "text", text: result }] };
|
|
4614
5182
|
} catch (err) {
|
|
4615
|
-
return { content: [{ type: "text", text: `Error in search_session_memory: ${err}` }], isError: true };
|
|
5183
|
+
return { content: [{ type: "text", text: `Error in search_session_memory: ${err}` }], isError: true };
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
);
|
|
5187
|
+
server.tool(
|
|
5188
|
+
"kuma_guard",
|
|
5189
|
+
"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.",
|
|
5190
|
+
{
|
|
5191
|
+
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"),
|
|
5192
|
+
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'")
|
|
5193
|
+
},
|
|
5194
|
+
async (params) => {
|
|
5195
|
+
try {
|
|
5196
|
+
const result = await handleKumaGuard(params);
|
|
5197
|
+
return { content: [{ type: "text", text: result }] };
|
|
5198
|
+
} catch (err) {
|
|
5199
|
+
return { content: [{ type: "text", text: `Error in kuma_guard: ${err}` }], isError: true };
|
|
4616
5200
|
}
|
|
4617
5201
|
}
|
|
4618
5202
|
);
|
|
@@ -4634,7 +5218,827 @@ function registerAllTools(server) {
|
|
|
4634
5218
|
}
|
|
4635
5219
|
}
|
|
4636
5220
|
);
|
|
4637
|
-
|
|
5221
|
+
server.tool(
|
|
5222
|
+
"kuma_context",
|
|
5223
|
+
"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.",
|
|
5224
|
+
{
|
|
5225
|
+
action: z.enum(["save", "list"]).describe("Action: save=create a snapshot, list=show all snapshots"),
|
|
5226
|
+
goal: z.string().optional().describe("Optional goal to associate with the snapshot")
|
|
5227
|
+
},
|
|
5228
|
+
async (params) => {
|
|
5229
|
+
try {
|
|
5230
|
+
const result = await handleKumaContext(params);
|
|
5231
|
+
return { content: [{ type: "text", text: result }] };
|
|
5232
|
+
} catch (err) {
|
|
5233
|
+
return { content: [{ type: "text", text: `Error in kuma_context: ${err}` }], isError: true };
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
);
|
|
5237
|
+
console.error("[Manifest] Registered 17 tools.");
|
|
5238
|
+
}
|
|
5239
|
+
|
|
5240
|
+
// src/cli/init.ts
|
|
5241
|
+
import fs16 from "fs";
|
|
5242
|
+
import path16 from "path";
|
|
5243
|
+
var ALL_CONFIG_TYPES = [
|
|
5244
|
+
"claude",
|
|
5245
|
+
"cursor",
|
|
5246
|
+
"windsurf",
|
|
5247
|
+
"copilot",
|
|
5248
|
+
"cline",
|
|
5249
|
+
"aider",
|
|
5250
|
+
"antigravity",
|
|
5251
|
+
"opencode",
|
|
5252
|
+
"codex",
|
|
5253
|
+
"qwen",
|
|
5254
|
+
"kiro",
|
|
5255
|
+
"openclaw",
|
|
5256
|
+
"codewhale"
|
|
5257
|
+
];
|
|
5258
|
+
var CONFIG_LABELS = {
|
|
5259
|
+
claude: "Claude Code (CLAUDE.md / plugin)",
|
|
5260
|
+
cursor: "Cursor (.cursor/rules/*.mdc)",
|
|
5261
|
+
windsurf: "Windsurf (.windsurfrules)",
|
|
5262
|
+
copilot: "GitHub Copilot Editor (AGENTS.md + Skill)",
|
|
5263
|
+
cline: "Cline (.clinerules/*.md)",
|
|
5264
|
+
aider: "Aider (CONVENTIONS.md via .aider.conf.yml)",
|
|
5265
|
+
antigravity: "Antigravity CLI (.agents/skills/)",
|
|
5266
|
+
opencode: "OpenCode (opencode.json)",
|
|
5267
|
+
codex: "Codex CLI (AGENTS.md + .codex/config.toml)",
|
|
5268
|
+
qwen: "Qwen Code (AGENTS.md + settings.json)",
|
|
5269
|
+
kiro: "Kiro (.kiro/steering/*.md)",
|
|
5270
|
+
openclaw: "OpenClaw (skills/)",
|
|
5271
|
+
codewhale: "CodeWhale (skills/ + .codewhale/mcp.json)"
|
|
5272
|
+
};
|
|
5273
|
+
function configFilePath(type) {
|
|
5274
|
+
switch (type) {
|
|
5275
|
+
case "claude":
|
|
5276
|
+
return "CLAUDE.md";
|
|
5277
|
+
case "cursor":
|
|
5278
|
+
return ".cursor/rules/kuma.mdc";
|
|
5279
|
+
case "windsurf":
|
|
5280
|
+
return ".windsurfrules";
|
|
5281
|
+
case "copilot":
|
|
5282
|
+
return "AGENTS.md";
|
|
5283
|
+
case "cline":
|
|
5284
|
+
return ".clinerules/kuma.md";
|
|
5285
|
+
case "aider":
|
|
5286
|
+
return "CONVENTIONS.md";
|
|
5287
|
+
case "antigravity":
|
|
5288
|
+
return ".agents/skills/kuma/SKILL.md";
|
|
5289
|
+
case "opencode":
|
|
5290
|
+
return "opencode.json";
|
|
5291
|
+
case "codex":
|
|
5292
|
+
return "AGENTS.md";
|
|
5293
|
+
case "qwen":
|
|
5294
|
+
return "AGENTS.md";
|
|
5295
|
+
case "kiro":
|
|
5296
|
+
return ".kiro/steering/kuma.md";
|
|
5297
|
+
case "openclaw":
|
|
5298
|
+
return "skills/kuma/SKILL.md";
|
|
5299
|
+
case "codewhale":
|
|
5300
|
+
return "skills/kuma/SKILL.md";
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
var CORE_RULES = [
|
|
5304
|
+
"## AI Agent Usage Guidelines",
|
|
5305
|
+
"",
|
|
5306
|
+
"Kuma MCP tools are available. Use them correctly:",
|
|
5307
|
+
"",
|
|
5308
|
+
"### Code Search",
|
|
5309
|
+
"- Use the **smart_grep** tool to search code - NOT bash grep/ripgrep manually",
|
|
5310
|
+
"- smart_grep returns line numbers + context, caches results, respects .gitignore",
|
|
5311
|
+
`- **Example:** smart_grep({ query: "function handleAuth", extensions: ['ts'] })`,
|
|
5312
|
+
"",
|
|
5313
|
+
"### Reading Code",
|
|
5314
|
+
"- Use the **smart_file_picker** tool to read files with smart chunking",
|
|
5315
|
+
"- For large files, use startLine/endLine to read specific ranges",
|
|
5316
|
+
'- **Example:** smart_file_picker({ filePath: "src/index.ts", chunkStrategy: "outline" })',
|
|
5317
|
+
'- **Example:** smart_file_picker({ filePath: "src/index.ts", startLine: 10, endLine: 30 })',
|
|
5318
|
+
"",
|
|
5319
|
+
"### Editing Code",
|
|
5320
|
+
"- Use the **precise_diff_editor** tool to edit files (fuzzy matching + auto-backup)",
|
|
5321
|
+
"- DO NOT create Python/Node scripts to patch files; use precise_diff_editor directly",
|
|
5322
|
+
"- DO NOT use bash sed/cat/awk to modify source files",
|
|
5323
|
+
'- **Example:** precise_diff_editor({ filePath: "src/app.ts", edits: [{ searchBlock: "old code", replaceBlock: "new code" }] })',
|
|
5324
|
+
'- **Example:** precise_diff_editor({ filePath: "src/app.ts", dryRun: true, edits: [...] })',
|
|
5325
|
+
'- **Example:** precise_diff_editor({ filePath: "src/app.ts", action: "rollback" })',
|
|
5326
|
+
"",
|
|
5327
|
+
"### Creating Files",
|
|
5328
|
+
"- Use the **batch_file_writer** tool to create new files (up to 15 at once)",
|
|
5329
|
+
'- **Example:** batch_file_writer({ files: [{ filePath: "src/util.ts", content: "// code", instructions: "reason for creating" }] })',
|
|
5330
|
+
"",
|
|
5331
|
+
"### Running Tasks",
|
|
5332
|
+
"- Use the **execute_safe_test** tool for test/build/lint/typecheck",
|
|
5333
|
+
"- Always run typecheck after editing TypeScript files",
|
|
5334
|
+
'- **Example:** execute_safe_test({ task: "typecheck" })',
|
|
5335
|
+
'- **Example:** execute_safe_test({ task: "custom", customCommand: "npm run lint" })',
|
|
5336
|
+
"",
|
|
5337
|
+
"### Code Review",
|
|
5338
|
+
"- Use the **code_reviewer** tool after changes",
|
|
5339
|
+
"- Supports focus: correctness, security, performance, over-engineering",
|
|
5340
|
+
'- **Example:** code_reviewer({ focus: "security" })',
|
|
5341
|
+
'- **Example:** code_reviewer({ files: ["src/auth.ts"], format: "json" })',
|
|
5342
|
+
"",
|
|
5343
|
+
"### Git Operations",
|
|
5344
|
+
"- Use the **git_diff** tool for structured diff output",
|
|
5345
|
+
"- Use the **git_log** tool for commit history",
|
|
5346
|
+
"- **Example:** git_log({ maxCount: 5 })",
|
|
5347
|
+
"- **Example:** git_diff({ staged: true })",
|
|
5348
|
+
"",
|
|
5349
|
+
"### Session Awareness",
|
|
5350
|
+
"- Use the **kuma_reflect** tool to check on-track/drift/loops",
|
|
5351
|
+
"- Use the **kuma_guard** tool for deeper safety checks (anti-patterns, auto-detection)",
|
|
5352
|
+
"- Use the **get_session_memory** tool to recall session state",
|
|
5353
|
+
'- **Example:** kuma_reflect({ goal: "refactor auth" })',
|
|
5354
|
+
'- **Example:** kuma_guard({ check: "all", goal: "refactor auth" })',
|
|
5355
|
+
"",
|
|
5356
|
+
"### LSP / Code Intelligence",
|
|
5357
|
+
"- Use the **lsp_query** tool for go-to-definition, find references, type info",
|
|
5358
|
+
'- **Example:** lsp_query({ filePath: "src/index.ts", line: 5, character: 10, action: "def" })',
|
|
5359
|
+
'- **Example:** lsp_query({ filePath: "src/index.ts", line: 5, character: 10, action: "refs" })',
|
|
5360
|
+
"",
|
|
5361
|
+
"### Static Analysis",
|
|
5362
|
+
"- Use the **static_analysis** tool to run ESLint/TSC/Prettier/Ruff",
|
|
5363
|
+
'- **Example:** static_analysis({ tool: "eslint", autoFix: true })',
|
|
5364
|
+
"",
|
|
5365
|
+
"### Project Structure",
|
|
5366
|
+
"- Use the **project_structure** tool to see project layout",
|
|
5367
|
+
"- **Example:** project_structure({ depth: 2, folderOnly: true })",
|
|
5368
|
+
"",
|
|
5369
|
+
"### Write Memory",
|
|
5370
|
+
"- Use the **write_memory** tool to persist decisions and glossary",
|
|
5371
|
+
'- **Example:** write_memory({ topic: "decisions", content: "## Reason for using X" })',
|
|
5372
|
+
"",
|
|
5373
|
+
"### General Rules",
|
|
5374
|
+
"- When you error, READ the error carefully before acting",
|
|
5375
|
+
"- After 3+ edits without running tests, stop and verify",
|
|
5376
|
+
"- If a tool fails, check the message - don't retry blindly",
|
|
5377
|
+
"- Detect conventions first with the **project_conventions** tool",
|
|
5378
|
+
"- **Example:** project_conventions({ forceRescan: true })"
|
|
5379
|
+
].join("\n");
|
|
5380
|
+
var KUMA_CORE_INSTRUCTIONS = CORE_RULES;
|
|
5381
|
+
function claudeTemplate() {
|
|
5382
|
+
return [
|
|
5383
|
+
"# Kuma AI Agent Guidelines",
|
|
5384
|
+
"",
|
|
5385
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5386
|
+
"",
|
|
5387
|
+
"## Workflow Pipeline",
|
|
5388
|
+
"",
|
|
5389
|
+
"For best results:",
|
|
5390
|
+
"1. **project_conventions** - detect stack",
|
|
5391
|
+
"2. **smart_grep** / **smart_file_picker** - understand code",
|
|
5392
|
+
"3. **precise_diff_editor** / **batch_file_writer** - make changes",
|
|
5393
|
+
"4. **execute_safe_test** - verify (typecheck + test)",
|
|
5394
|
+
"5. **code_reviewer** - review changes"
|
|
5395
|
+
].join("\n");
|
|
5396
|
+
}
|
|
5397
|
+
function cursorRulesTemplate() {
|
|
5398
|
+
return [
|
|
5399
|
+
"---",
|
|
5400
|
+
"description: Kuma MCP tool usage rules for AI coding agents",
|
|
5401
|
+
"alwaysApply: true",
|
|
5402
|
+
"---",
|
|
5403
|
+
"",
|
|
5404
|
+
"You are an expert engineer. Kuma MCP tools are available.",
|
|
5405
|
+
"",
|
|
5406
|
+
"## Critical Rules",
|
|
5407
|
+
"",
|
|
5408
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5409
|
+
"",
|
|
5410
|
+
"## NEVER",
|
|
5411
|
+
"- Never create Python/Node scripts to patch code",
|
|
5412
|
+
"- Never use bash sed/cat/awk to edit source files",
|
|
5413
|
+
"- Never run git push/git commit through bash"
|
|
5414
|
+
].join("\n");
|
|
5415
|
+
}
|
|
5416
|
+
function windsurfRulesTemplate() {
|
|
5417
|
+
return [
|
|
5418
|
+
"# Windsurf Cascade Rules with Kuma",
|
|
5419
|
+
"",
|
|
5420
|
+
KUMA_CORE_INSTRUCTIONS
|
|
5421
|
+
].join("\n");
|
|
5422
|
+
}
|
|
5423
|
+
function copilotTemplate() {
|
|
5424
|
+
return [
|
|
5425
|
+
"## GitHub Copilot Editor",
|
|
5426
|
+
"",
|
|
5427
|
+
"Kuma MCP tools are available. Use them correctly:",
|
|
5428
|
+
"",
|
|
5429
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5430
|
+
"",
|
|
5431
|
+
"### Copilot Editor-Specific",
|
|
5432
|
+
"- Copilot Editor reads AGENTS.md at project root for persistent instructions",
|
|
5433
|
+
"- Configure MCP servers via VS Code settings (cmd+shift+P \u2192 Developer: Reload Window after adding Kuma)",
|
|
5434
|
+
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5435
|
+
].join("\n");
|
|
5436
|
+
}
|
|
5437
|
+
function clineRulesTemplate() {
|
|
5438
|
+
return [
|
|
5439
|
+
"---",
|
|
5440
|
+
"description: Kuma MCP tool usage rules for AI coding agents",
|
|
5441
|
+
"paths:",
|
|
5442
|
+
' - "*"',
|
|
5443
|
+
"---",
|
|
5444
|
+
"",
|
|
5445
|
+
KUMA_CORE_INSTRUCTIONS
|
|
5446
|
+
].join("\n");
|
|
5447
|
+
}
|
|
5448
|
+
function aiderTemplate() {
|
|
5449
|
+
return [
|
|
5450
|
+
"# Kuma MCP - Aider Coding Conventions",
|
|
5451
|
+
"",
|
|
5452
|
+
"These conventions are loaded by Aider via the `read:` field in .aider.conf.yml",
|
|
5453
|
+
"",
|
|
5454
|
+
KUMA_CORE_INSTRUCTIONS
|
|
5455
|
+
].join("\n");
|
|
5456
|
+
}
|
|
5457
|
+
function opencodeTemplate() {
|
|
5458
|
+
const config = {
|
|
5459
|
+
mcp: {
|
|
5460
|
+
kuma: {
|
|
5461
|
+
type: "local",
|
|
5462
|
+
command: ["npx", "-y", "@plumpslabs/kuma"],
|
|
5463
|
+
enabled: true
|
|
5464
|
+
}
|
|
5465
|
+
},
|
|
5466
|
+
instructions: ["CLAUDE.md"]
|
|
5467
|
+
};
|
|
5468
|
+
const header = [
|
|
5469
|
+
"// Generated by Kuma MCP - https://github.com/plumpslabs/kuma",
|
|
5470
|
+
"// OpenCode config with Kuma MCP tools. Edit opencode.json to customize.",
|
|
5471
|
+
""
|
|
5472
|
+
].join("\n");
|
|
5473
|
+
return header + JSON.stringify(config, null, 2) + "\n";
|
|
5474
|
+
}
|
|
5475
|
+
function codexTemplate() {
|
|
5476
|
+
return [
|
|
5477
|
+
"## Codex CLI (OpenAI)",
|
|
5478
|
+
"",
|
|
5479
|
+
"Kuma MCP tools are available. Use them correctly:",
|
|
5480
|
+
"",
|
|
5481
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5482
|
+
"",
|
|
5483
|
+
"### Codex-Specific",
|
|
5484
|
+
"- Codex uses cascading AGENTS.md files (global ~/.codex/AGENTS.md -> project AGENTS.md)",
|
|
5485
|
+
"- MCP config is in .codex/config.toml (auto-generated by kuma init)",
|
|
5486
|
+
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5487
|
+
].join("\n");
|
|
5488
|
+
}
|
|
5489
|
+
function codexConfigTomlTemplate() {
|
|
5490
|
+
return [
|
|
5491
|
+
"# Generated by Kuma MCP - https://github.com/plumpslabs/kuma",
|
|
5492
|
+
"# Kuma MCP server config for Codex CLI",
|
|
5493
|
+
"",
|
|
5494
|
+
"[mcp_servers.kuma]",
|
|
5495
|
+
'command = "npx"',
|
|
5496
|
+
'args = ["-y", "@plumpslabs/kuma"]',
|
|
5497
|
+
""
|
|
5498
|
+
].join("\n");
|
|
5499
|
+
}
|
|
5500
|
+
function qwenTemplate() {
|
|
5501
|
+
return [
|
|
5502
|
+
"## Qwen Code",
|
|
5503
|
+
"",
|
|
5504
|
+
"Kuma MCP tools are available. Use them correctly:",
|
|
5505
|
+
"",
|
|
5506
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5507
|
+
"",
|
|
5508
|
+
"### Qwen-Specific",
|
|
5509
|
+
"- Qwen reads AGENTS.md at project root for persistent instructions",
|
|
5510
|
+
"- MCP config is in settings.json (auto-generated by kuma init)",
|
|
5511
|
+
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5512
|
+
].join("\n");
|
|
5513
|
+
}
|
|
5514
|
+
function qwenSettingsTemplate() {
|
|
5515
|
+
const config = {
|
|
5516
|
+
mcpServers: {
|
|
5517
|
+
kuma: {
|
|
5518
|
+
command: "npx",
|
|
5519
|
+
args: ["-y", "@plumpslabs/kuma"],
|
|
5520
|
+
env: {}
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
};
|
|
5524
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
5525
|
+
}
|
|
5526
|
+
function kiroRulesTemplate() {
|
|
5527
|
+
return [
|
|
5528
|
+
"---",
|
|
5529
|
+
"name: kuma-mcp",
|
|
5530
|
+
"description: Kuma safety toolkit - use smart_grep for search, precise_diff_editor for edits",
|
|
5531
|
+
"inclusion: always",
|
|
5532
|
+
"---",
|
|
5533
|
+
"",
|
|
5534
|
+
"# Kuma MCP - Kiro Steering",
|
|
5535
|
+
"",
|
|
5536
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5537
|
+
"",
|
|
5538
|
+
"## Kiro-Specific",
|
|
5539
|
+
"- Kiro reads steering files from .kiro/steering/ for project instructions",
|
|
5540
|
+
"- Configure MCP servers via IDE settings or global Kiro config",
|
|
5541
|
+
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5542
|
+
].join("\n");
|
|
5543
|
+
}
|
|
5544
|
+
function openclawSkillTemplate() {
|
|
5545
|
+
return [
|
|
5546
|
+
"---",
|
|
5547
|
+
"name: kuma-mcp",
|
|
5548
|
+
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5549
|
+
"---",
|
|
5550
|
+
"",
|
|
5551
|
+
"# Kuma MCP - OpenClaw Skill",
|
|
5552
|
+
"",
|
|
5553
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5554
|
+
"",
|
|
5555
|
+
"## OpenClaw-Specific",
|
|
5556
|
+
"- OpenClaw loads skills/ from workspace root or ~/.openclaw/skills for global",
|
|
5557
|
+
"- Configure Kuma MCP server via ~/.openclaw/openclaw.json or agents standard",
|
|
5558
|
+
"- Use kuma_guard periodically to check for anti-patterns",
|
|
5559
|
+
"",
|
|
5560
|
+
"## Verification",
|
|
5561
|
+
"- After edits, run execute_safe_test to verify no breakage",
|
|
5562
|
+
"- Use code_reviewer for correctness/security review",
|
|
5563
|
+
"- Check kuma_reflect to confirm on-track"
|
|
5564
|
+
].join("\n");
|
|
5565
|
+
}
|
|
5566
|
+
function codewhaleTemplate() {
|
|
5567
|
+
return [
|
|
5568
|
+
"---",
|
|
5569
|
+
"name: kuma-mcp",
|
|
5570
|
+
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5571
|
+
"---",
|
|
5572
|
+
"",
|
|
5573
|
+
"# Kuma MCP - CodeWhale Skill",
|
|
5574
|
+
"",
|
|
5575
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5576
|
+
"",
|
|
5577
|
+
"## CodeWhale-Specific",
|
|
5578
|
+
"- CodeWhale loads SKILL.md from skills/ (workspace-local), .agents/skills/, or ~/.codewhale/skills/",
|
|
5579
|
+
"- MCP config is in ~/.codewhale/mcp.json or ~/.deepseek/mcp.json",
|
|
5580
|
+
"- Use kuma_guard periodically to check for anti-patterns",
|
|
5581
|
+
"",
|
|
5582
|
+
"## Verification",
|
|
5583
|
+
"- After edits, run execute_safe_test to verify no breakage",
|
|
5584
|
+
"- Use code_reviewer for correctness/security review",
|
|
5585
|
+
"- Check kuma_reflect to confirm on-track"
|
|
5586
|
+
].join("\n");
|
|
5587
|
+
}
|
|
5588
|
+
function antigravitySkillTemplate() {
|
|
5589
|
+
return [
|
|
5590
|
+
"---",
|
|
5591
|
+
"name: kuma-mcp",
|
|
5592
|
+
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5593
|
+
"---",
|
|
5594
|
+
"",
|
|
5595
|
+
"# Kuma MCP - Antigravity Skill",
|
|
5596
|
+
"",
|
|
5597
|
+
KUMA_CORE_INSTRUCTIONS,
|
|
5598
|
+
"",
|
|
5599
|
+
"## Verification",
|
|
5600
|
+
"- After edits, run execute_safe_test to verify no breakage",
|
|
5601
|
+
"- Use code_reviewer for correctness/security review",
|
|
5602
|
+
"- Check kuma_reflect to confirm on-track"
|
|
5603
|
+
].join("\n");
|
|
5604
|
+
}
|
|
5605
|
+
function antigravityMcpConfigTemplate() {
|
|
5606
|
+
const config = {
|
|
5607
|
+
mcpServers: {
|
|
5608
|
+
kuma: {
|
|
5609
|
+
command: "npx",
|
|
5610
|
+
args: ["-y", "@plumpslabs/kuma"],
|
|
5611
|
+
env: {}
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
};
|
|
5615
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
5616
|
+
}
|
|
5617
|
+
var TEMPLATES = {
|
|
5618
|
+
claude: claudeTemplate,
|
|
5619
|
+
cursor: cursorRulesTemplate,
|
|
5620
|
+
windsurf: windsurfRulesTemplate,
|
|
5621
|
+
copilot: copilotTemplate,
|
|
5622
|
+
cline: clineRulesTemplate,
|
|
5623
|
+
aider: aiderTemplate,
|
|
5624
|
+
antigravity: antigravitySkillTemplate,
|
|
5625
|
+
opencode: opencodeTemplate,
|
|
5626
|
+
codex: codexTemplate,
|
|
5627
|
+
qwen: qwenTemplate,
|
|
5628
|
+
kiro: kiroRulesTemplate,
|
|
5629
|
+
openclaw: openclawSkillTemplate,
|
|
5630
|
+
codewhale: codewhaleTemplate
|
|
5631
|
+
};
|
|
5632
|
+
var APPEND_SEPARATOR = "\n\n---\n_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_\n\n";
|
|
5633
|
+
function handleOpencodeSecondary(root, results) {
|
|
5634
|
+
const claudePath = path16.resolve(root, "CLAUDE.md");
|
|
5635
|
+
if (!fs16.existsSync(claudePath)) {
|
|
5636
|
+
try {
|
|
5637
|
+
fs16.writeFileSync(claudePath, [
|
|
5638
|
+
"# Kuma MCP - OpenCode Instructions",
|
|
5639
|
+
"",
|
|
5640
|
+
KUMA_CORE_INSTRUCTIONS
|
|
5641
|
+
].join("\n"), "utf-8");
|
|
5642
|
+
results.push({ type: "opencode", filePath: "CLAUDE.md", action: "created" });
|
|
5643
|
+
} catch (err) {
|
|
5644
|
+
results.push({
|
|
5645
|
+
type: "opencode",
|
|
5646
|
+
filePath: "CLAUDE.md",
|
|
5647
|
+
action: "error",
|
|
5648
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5649
|
+
});
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
5653
|
+
function handleCodexSecondary(root, results) {
|
|
5654
|
+
const tomlPath = path16.resolve(root, ".codex/config.toml");
|
|
5655
|
+
if (results.some((r) => r.filePath === ".codex/config.toml")) return;
|
|
5656
|
+
try {
|
|
5657
|
+
const dir = path16.dirname(tomlPath);
|
|
5658
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5659
|
+
if (fs16.existsSync(tomlPath)) {
|
|
5660
|
+
const existingContent = fs16.readFileSync(tomlPath, "utf-8");
|
|
5661
|
+
if (existingContent.includes("kuma")) {
|
|
5662
|
+
results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
|
|
5663
|
+
return;
|
|
5664
|
+
}
|
|
5665
|
+
fs16.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
|
|
5666
|
+
results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
|
|
5667
|
+
} else {
|
|
5668
|
+
fs16.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
|
|
5669
|
+
results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
|
|
5670
|
+
}
|
|
5671
|
+
} catch (err) {
|
|
5672
|
+
results.push({
|
|
5673
|
+
type: "codex",
|
|
5674
|
+
filePath: ".codex/config.toml",
|
|
5675
|
+
action: "error",
|
|
5676
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5677
|
+
});
|
|
5678
|
+
}
|
|
5679
|
+
}
|
|
5680
|
+
function handleQwenSecondary(root, results) {
|
|
5681
|
+
const settingsPath = path16.resolve(root, "settings.json");
|
|
5682
|
+
if (results.some((r) => r.filePath === "settings.json")) return;
|
|
5683
|
+
try {
|
|
5684
|
+
if (fs16.existsSync(settingsPath)) {
|
|
5685
|
+
const existingContent = fs16.readFileSync(settingsPath, "utf-8");
|
|
5686
|
+
if (existingContent.includes("kuma")) {
|
|
5687
|
+
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5688
|
+
try {
|
|
5689
|
+
const parsed = JSON.parse(existingContent);
|
|
5690
|
+
parsed.mcpServers = parsed.mcpServers || {};
|
|
5691
|
+
if (!parsed.mcpServers.kuma) {
|
|
5692
|
+
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5693
|
+
fs16.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5694
|
+
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
5695
|
+
return;
|
|
5696
|
+
}
|
|
5697
|
+
} catch {
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
results.push({ type: "qwen", filePath: "settings.json", action: "skipped" });
|
|
5701
|
+
return;
|
|
5702
|
+
}
|
|
5703
|
+
try {
|
|
5704
|
+
const parsed = JSON.parse(existingContent);
|
|
5705
|
+
parsed.mcpServers = parsed.mcpServers || {};
|
|
5706
|
+
if (!parsed.mcpServers.kuma) {
|
|
5707
|
+
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5708
|
+
fs16.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5709
|
+
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
5710
|
+
}
|
|
5711
|
+
} catch {
|
|
5712
|
+
}
|
|
5713
|
+
} else {
|
|
5714
|
+
fs16.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
|
|
5715
|
+
results.push({ type: "qwen", filePath: "settings.json", action: "created" });
|
|
5716
|
+
}
|
|
5717
|
+
} catch (err) {
|
|
5718
|
+
results.push({
|
|
5719
|
+
type: "qwen",
|
|
5720
|
+
filePath: "settings.json",
|
|
5721
|
+
action: "error",
|
|
5722
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5723
|
+
});
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
var AGENTS_MD_TYPES = ["codex", "qwen", "copilot"];
|
|
5727
|
+
function getAgentsMdHeader() {
|
|
5728
|
+
return [
|
|
5729
|
+
"# Kuma MCP - Combined Agent Instructions",
|
|
5730
|
+
"",
|
|
5731
|
+
"This file contains instructions for AI coding agents that read AGENTS.md.",
|
|
5732
|
+
"Each section applies to a specific agent. Unused sections can be safely removed.",
|
|
5733
|
+
"",
|
|
5734
|
+
"---",
|
|
5735
|
+
"_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_",
|
|
5736
|
+
""
|
|
5737
|
+
].join("\n");
|
|
5738
|
+
}
|
|
5739
|
+
function getCombinedAgentsMd(selectedTypes) {
|
|
5740
|
+
const sections = [getAgentsMdHeader()];
|
|
5741
|
+
const agentOrder = ["codex", "qwen", "copilot"];
|
|
5742
|
+
for (const t of agentOrder) {
|
|
5743
|
+
if (selectedTypes.has(t)) {
|
|
5744
|
+
sections.push(TEMPLATES[t]());
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
return sections.join("\n\n---\n\n");
|
|
5748
|
+
}
|
|
5749
|
+
function handleAntigravityMcpConfig(root, results) {
|
|
5750
|
+
const mcpPath = path16.resolve(root, ".agents/mcp_config.json");
|
|
5751
|
+
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
5752
|
+
try {
|
|
5753
|
+
const mcpDir = path16.dirname(mcpPath);
|
|
5754
|
+
if (fs16.existsSync(mcpPath)) {
|
|
5755
|
+
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5756
|
+
if (existingContent.includes("kuma")) {
|
|
5757
|
+
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5758
|
+
const trimmed = existingContent.trimEnd();
|
|
5759
|
+
if (trimmed.endsWith("}")) {
|
|
5760
|
+
const updated = trimmed.slice(0, -1).trimEnd() + ',\n "_kuma_note": "Kuma MCP - Generated by kuma init"\n}\n';
|
|
5761
|
+
fs16.writeFileSync(mcpPath, updated, "utf-8");
|
|
5762
|
+
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
return;
|
|
5766
|
+
}
|
|
5767
|
+
const parsed = JSON.parse(existingContent);
|
|
5768
|
+
parsed.mcpServers = parsed.mcpServers || {};
|
|
5769
|
+
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5770
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5771
|
+
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5772
|
+
} else {
|
|
5773
|
+
if (!fs16.existsSync(mcpDir)) fs16.mkdirSync(mcpDir, { recursive: true });
|
|
5774
|
+
fs16.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
5775
|
+
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
|
|
5776
|
+
}
|
|
5777
|
+
} catch (err) {
|
|
5778
|
+
results.push({
|
|
5779
|
+
type: "antigravity",
|
|
5780
|
+
filePath: ".agents/mcp_config.json",
|
|
5781
|
+
action: "error",
|
|
5782
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5783
|
+
});
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
function handleAiderSecondary(root, results) {
|
|
5787
|
+
const ymlPath = path16.resolve(root, ".aider.conf.yml");
|
|
5788
|
+
if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
|
|
5789
|
+
try {
|
|
5790
|
+
const conventionsRef = "read: CONVENTIONS.md";
|
|
5791
|
+
if (fs16.existsSync(ymlPath)) {
|
|
5792
|
+
const existingContent = fs16.readFileSync(ymlPath, "utf-8");
|
|
5793
|
+
if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
|
|
5794
|
+
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
|
|
5795
|
+
return;
|
|
5796
|
+
}
|
|
5797
|
+
const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
|
|
5798
|
+
fs16.writeFileSync(ymlPath, newContent, "utf-8");
|
|
5799
|
+
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
|
|
5800
|
+
} else {
|
|
5801
|
+
const content = [
|
|
5802
|
+
"# Generated by Kuma MCP - https://github.com/plumpslabs/kuma",
|
|
5803
|
+
"# Aider will read CONVENTIONS.md for coding conventions",
|
|
5804
|
+
"",
|
|
5805
|
+
conventionsRef,
|
|
5806
|
+
""
|
|
5807
|
+
].join("\n");
|
|
5808
|
+
fs16.writeFileSync(ymlPath, content, "utf-8");
|
|
5809
|
+
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
|
|
5810
|
+
}
|
|
5811
|
+
} catch (err) {
|
|
5812
|
+
results.push({
|
|
5813
|
+
type: "aider",
|
|
5814
|
+
filePath: ".aider.conf.yml",
|
|
5815
|
+
action: "error",
|
|
5816
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5817
|
+
});
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5820
|
+
function handleCopilotSecondary(root, results) {
|
|
5821
|
+
const skillPath = path16.resolve(root, ".github/skills/kuma/SKILL.md");
|
|
5822
|
+
if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
|
|
5823
|
+
try {
|
|
5824
|
+
const dir = path16.dirname(skillPath);
|
|
5825
|
+
const content = [
|
|
5826
|
+
"---",
|
|
5827
|
+
"name: kuma-mcp",
|
|
5828
|
+
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5829
|
+
"---",
|
|
5830
|
+
"",
|
|
5831
|
+
"# Kuma MCP - Copilot Editor Skill",
|
|
5832
|
+
"",
|
|
5833
|
+
KUMA_CORE_INSTRUCTIONS
|
|
5834
|
+
].join("\n");
|
|
5835
|
+
if (fs16.existsSync(skillPath)) {
|
|
5836
|
+
const existingContent = fs16.readFileSync(skillPath, "utf-8");
|
|
5837
|
+
if (existingContent.includes("kuma")) {
|
|
5838
|
+
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
|
|
5839
|
+
return;
|
|
5840
|
+
}
|
|
5841
|
+
fs16.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
|
|
5842
|
+
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
|
|
5843
|
+
} else {
|
|
5844
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5845
|
+
fs16.writeFileSync(skillPath, content, "utf-8");
|
|
5846
|
+
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
|
|
5847
|
+
}
|
|
5848
|
+
} catch (err) {
|
|
5849
|
+
results.push({
|
|
5850
|
+
type: "copilot",
|
|
5851
|
+
filePath: ".github/skills/kuma/SKILL.md",
|
|
5852
|
+
action: "error",
|
|
5853
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5854
|
+
});
|
|
5855
|
+
}
|
|
5856
|
+
}
|
|
5857
|
+
function handleOpenclawSecondary(root, results) {
|
|
5858
|
+
const mcpPath = path16.resolve(root, ".agents/mcp_config.json");
|
|
5859
|
+
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
5860
|
+
try {
|
|
5861
|
+
const dir = path16.dirname(mcpPath);
|
|
5862
|
+
if (fs16.existsSync(mcpPath)) {
|
|
5863
|
+
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5864
|
+
if (existingContent.includes("kuma")) return;
|
|
5865
|
+
const parsed = JSON.parse(existingContent);
|
|
5866
|
+
parsed.mcpServers = parsed.mcpServers || {};
|
|
5867
|
+
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5868
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5869
|
+
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5870
|
+
} else {
|
|
5871
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5872
|
+
fs16.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
5873
|
+
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
|
|
5874
|
+
}
|
|
5875
|
+
} catch (err) {
|
|
5876
|
+
results.push({
|
|
5877
|
+
type: "openclaw",
|
|
5878
|
+
filePath: ".agents/mcp_config.json",
|
|
5879
|
+
action: "error",
|
|
5880
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5881
|
+
});
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
function handleCodewhaleSecondary(root, results) {
|
|
5885
|
+
const mcpPath = path16.resolve(root, ".codewhale/mcp.json");
|
|
5886
|
+
if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
|
|
5887
|
+
try {
|
|
5888
|
+
const dir = path16.dirname(mcpPath);
|
|
5889
|
+
if (fs16.existsSync(mcpPath)) {
|
|
5890
|
+
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5891
|
+
if (existingContent.includes("kuma")) return;
|
|
5892
|
+
const parsed = JSON.parse(existingContent);
|
|
5893
|
+
parsed.mcpServers = parsed.mcpServers || {};
|
|
5894
|
+
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5895
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5896
|
+
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
|
|
5897
|
+
} else {
|
|
5898
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5899
|
+
const config = {
|
|
5900
|
+
mcpServers: {
|
|
5901
|
+
kuma: {
|
|
5902
|
+
command: "npx",
|
|
5903
|
+
args: ["-y", "@plumpslabs/kuma"],
|
|
5904
|
+
env: {}
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5907
|
+
};
|
|
5908
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5909
|
+
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
|
|
5910
|
+
}
|
|
5911
|
+
} catch (err) {
|
|
5912
|
+
results.push({
|
|
5913
|
+
type: "codewhale",
|
|
5914
|
+
filePath: ".codewhale/mcp.json",
|
|
5915
|
+
action: "error",
|
|
5916
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5917
|
+
});
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
function runInit(types, projectRoot) {
|
|
5921
|
+
const root = projectRoot ?? getProjectRoot();
|
|
5922
|
+
const selected = types.length > 0 ? types : ALL_CONFIG_TYPES;
|
|
5923
|
+
const results = [];
|
|
5924
|
+
const selectedSet = new Set(selected);
|
|
5925
|
+
const agentsMdSelected = AGENTS_MD_TYPES.filter((t) => selectedSet.has(t));
|
|
5926
|
+
let agentsMdHandled = false;
|
|
5927
|
+
for (const type of selected) {
|
|
5928
|
+
const relativePath = configFilePath(type);
|
|
5929
|
+
const fullPath = path16.resolve(root, relativePath);
|
|
5930
|
+
const getTemplate = TEMPLATES[type];
|
|
5931
|
+
try {
|
|
5932
|
+
if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
|
|
5933
|
+
agentsMdHandled = true;
|
|
5934
|
+
const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
|
|
5935
|
+
if (fs16.existsSync(fullPath)) {
|
|
5936
|
+
const existingContent = fs16.readFileSync(fullPath, "utf-8");
|
|
5937
|
+
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5938
|
+
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
5939
|
+
} else {
|
|
5940
|
+
fs16.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
|
|
5941
|
+
results.push({ type, filePath: relativePath, action: "appended" });
|
|
5942
|
+
}
|
|
5943
|
+
} else {
|
|
5944
|
+
const dir = path16.dirname(fullPath);
|
|
5945
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5946
|
+
fs16.writeFileSync(fullPath, combinedContent, "utf-8");
|
|
5947
|
+
results.push({ type, filePath: relativePath, action: "created" });
|
|
5948
|
+
}
|
|
5949
|
+
if (selectedSet.has("codex")) handleCodexSecondary(root, results);
|
|
5950
|
+
if (selectedSet.has("qwen")) handleQwenSecondary(root, results);
|
|
5951
|
+
if (selectedSet.has("copilot")) handleCopilotSecondary(root, results);
|
|
5952
|
+
} else if (AGENTS_MD_TYPES.includes(type) && agentsMdHandled) {
|
|
5953
|
+
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
5954
|
+
continue;
|
|
5955
|
+
} else {
|
|
5956
|
+
const template = getTemplate();
|
|
5957
|
+
if (fs16.existsSync(fullPath)) {
|
|
5958
|
+
const existingContent = fs16.readFileSync(fullPath, "utf-8");
|
|
5959
|
+
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5960
|
+
if (type === "antigravity") {
|
|
5961
|
+
handleAntigravityMcpConfig(root, results);
|
|
5962
|
+
} else if (type === "openclaw") {
|
|
5963
|
+
handleOpenclawSecondary(root, results);
|
|
5964
|
+
} else if (type === "codewhale") {
|
|
5965
|
+
handleCodewhaleSecondary(root, results);
|
|
5966
|
+
}
|
|
5967
|
+
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
5968
|
+
continue;
|
|
5969
|
+
}
|
|
5970
|
+
const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
|
|
5971
|
+
fs16.writeFileSync(fullPath, newContent, "utf-8");
|
|
5972
|
+
results.push({ type, filePath: relativePath, action: "appended" });
|
|
5973
|
+
} else {
|
|
5974
|
+
const dir = path16.dirname(fullPath);
|
|
5975
|
+
if (!fs16.existsSync(dir)) {
|
|
5976
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
5977
|
+
}
|
|
5978
|
+
fs16.writeFileSync(fullPath, template, "utf-8");
|
|
5979
|
+
results.push({ type, filePath: relativePath, action: "created" });
|
|
5980
|
+
}
|
|
5981
|
+
if (type === "antigravity") {
|
|
5982
|
+
handleAntigravityMcpConfig(root, results);
|
|
5983
|
+
} else if (type === "openclaw") {
|
|
5984
|
+
handleOpenclawSecondary(root, results);
|
|
5985
|
+
} else if (type === "codewhale") {
|
|
5986
|
+
handleCodewhaleSecondary(root, results);
|
|
5987
|
+
} else if (type === "aider") {
|
|
5988
|
+
handleAiderSecondary(root, results);
|
|
5989
|
+
} else if (type === "opencode") {
|
|
5990
|
+
handleOpencodeSecondary(root, results);
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
} catch (err) {
|
|
5994
|
+
results.push({
|
|
5995
|
+
type,
|
|
5996
|
+
filePath: relativePath,
|
|
5997
|
+
action: "error",
|
|
5998
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5999
|
+
});
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
return results;
|
|
6003
|
+
}
|
|
6004
|
+
function formatInitResults(results) {
|
|
6005
|
+
const lines = [
|
|
6006
|
+
"\u{1F43B} **Kuma Init - AI Agent Config Generator**",
|
|
6007
|
+
""
|
|
6008
|
+
];
|
|
6009
|
+
for (const r of results) {
|
|
6010
|
+
const label = CONFIG_LABELS[r.type];
|
|
6011
|
+
switch (r.action) {
|
|
6012
|
+
case "created":
|
|
6013
|
+
lines.push(" \u2705 " + label);
|
|
6014
|
+
lines.push(" \u2192 Created: " + r.filePath);
|
|
6015
|
+
break;
|
|
6016
|
+
case "appended":
|
|
6017
|
+
lines.push(" \u2795 " + label);
|
|
6018
|
+
lines.push(" \u2192 Appended to: " + r.filePath);
|
|
6019
|
+
break;
|
|
6020
|
+
case "skipped":
|
|
6021
|
+
lines.push(" \u23ED " + label);
|
|
6022
|
+
lines.push(" \u2192 Skipped (already has Kuma): " + r.filePath);
|
|
6023
|
+
break;
|
|
6024
|
+
case "error":
|
|
6025
|
+
lines.push(" \u274C " + label);
|
|
6026
|
+
lines.push(" \u2192 Error: " + (r.error ?? "unknown"));
|
|
6027
|
+
break;
|
|
6028
|
+
}
|
|
6029
|
+
}
|
|
6030
|
+
const created = results.filter((r) => r.action === "created").length;
|
|
6031
|
+
const appended = results.filter((r) => r.action === "appended").length;
|
|
6032
|
+
const skipped = results.filter((r) => r.action === "skipped").length;
|
|
6033
|
+
const errors = results.filter((r) => r.action === "error").length;
|
|
6034
|
+
lines.push(
|
|
6035
|
+
"",
|
|
6036
|
+
"\u{1F4CA} Summary: " + created + " created, " + appended + " appended, " + skipped + " skipped, " + errors + " errors",
|
|
6037
|
+
"",
|
|
6038
|
+
"\u{1F4A1} Config files teach your AI how to use Kuma tools.",
|
|
6039
|
+
"\u{1F4A1} Run again to generate additional config files anytime."
|
|
6040
|
+
);
|
|
6041
|
+
return lines.join("\n");
|
|
4638
6042
|
}
|
|
4639
6043
|
|
|
4640
6044
|
// src/index.ts
|
|
@@ -4642,7 +6046,92 @@ var SERVER_NAME = "kuma";
|
|
|
4642
6046
|
var SERVER_VERSION = JSON.parse(
|
|
4643
6047
|
readFileSync(new URL("../package.json", import.meta.url), "utf-8")
|
|
4644
6048
|
).version;
|
|
6049
|
+
function printHelp() {
|
|
6050
|
+
console.error(`
|
|
6051
|
+
\u{1F43B} Kuma v${SERVER_VERSION} \u2014 Zero-setup safety toolkit for AI coding agents
|
|
6052
|
+
|
|
6053
|
+
Usage:
|
|
6054
|
+
npx @plumpslabs/kuma Start MCP server (default)
|
|
6055
|
+
npx @plumpslabs/kuma init Generate AI agent config files
|
|
6056
|
+
npx @plumpslabs/kuma init --all Generate ALL config files
|
|
6057
|
+
npx @plumpslabs/kuma init --claude --cursor Generate specific files
|
|
6058
|
+
npx @plumpslabs/kuma init --help Show this help
|
|
6059
|
+
|
|
6060
|
+
Available config files:
|
|
6061
|
+
--claude CLAUDE.md (Claude Code)
|
|
6062
|
+
--cursor .cursor/rules/kuma.mdc (Cursor)
|
|
6063
|
+
--windsurf .windsurfrules (Windsurf)
|
|
6064
|
+
--copilot AGENTS.md + .github/skills/ (GitHub Copilot Editor)
|
|
6065
|
+
--cline .clinerules/kuma.md (Cline)
|
|
6066
|
+
--aider CONVENTIONS.md + .aider.conf.yml (Aider)
|
|
6067
|
+
--antigravity .agents/skills/kuma/SKILL.md (Antigravity CLI)
|
|
6068
|
+
--opencode opencode.json (OpenCode)
|
|
6069
|
+
--codex AGENTS.md + .codex/ (Codex CLI - OpenAI)
|
|
6070
|
+
--qwen AGENTS.md + settings.json (Qwen Code)
|
|
6071
|
+
--kiro .kiro/steering/kuma.md (Kiro)
|
|
6072
|
+
--openclaw skills/kuma/SKILL.md (OpenClaw)
|
|
6073
|
+
--codewhale skills/kuma/SKILL.md + .codewhale/ (CodeWhale)
|
|
6074
|
+
|
|
6075
|
+
If no flags specified, you'll be prompted to select files interactively.
|
|
6076
|
+
`);
|
|
6077
|
+
}
|
|
4645
6078
|
async function main() {
|
|
6079
|
+
const args = process.argv.slice(2);
|
|
6080
|
+
if (args[0] === "init") {
|
|
6081
|
+
const flags = args.slice(1);
|
|
6082
|
+
if (flags.includes("--help") || flags.includes("-h")) {
|
|
6083
|
+
printHelp();
|
|
6084
|
+
process.exit(0);
|
|
6085
|
+
}
|
|
6086
|
+
const requestedFlags = flags.filter((f) => f.startsWith("--"));
|
|
6087
|
+
let selectedTypes;
|
|
6088
|
+
if (requestedFlags.length === 0) {
|
|
6089
|
+
console.error("\u{1F43B} Kuma Init \u2014 AI Agent Config Generator");
|
|
6090
|
+
console.error("");
|
|
6091
|
+
console.error("Select config files to generate. Press Ctrl+C to skip.");
|
|
6092
|
+
console.error("");
|
|
6093
|
+
selectedTypes = await interactiveSelect();
|
|
6094
|
+
if (selectedTypes.length === 0) {
|
|
6095
|
+
console.error("\n\u26A0\uFE0F No files selected. Exiting.");
|
|
6096
|
+
process.exit(0);
|
|
6097
|
+
}
|
|
6098
|
+
} else {
|
|
6099
|
+
if (requestedFlags.includes("--all")) {
|
|
6100
|
+
selectedTypes = ALL_CONFIG_TYPES;
|
|
6101
|
+
} else {
|
|
6102
|
+
const flagToType = {
|
|
6103
|
+
"--claude": "claude",
|
|
6104
|
+
"--cursor": "cursor",
|
|
6105
|
+
"--windsurf": "windsurf",
|
|
6106
|
+
"--copilot": "copilot",
|
|
6107
|
+
"--cline": "cline",
|
|
6108
|
+
"--aider": "aider",
|
|
6109
|
+
"--antigravity": "antigravity",
|
|
6110
|
+
"--opencode": "opencode",
|
|
6111
|
+
"--codex": "codex",
|
|
6112
|
+
"--qwen": "qwen",
|
|
6113
|
+
"--kiro": "kiro",
|
|
6114
|
+
"--openclaw": "openclaw",
|
|
6115
|
+
"--codewhale": "codewhale"
|
|
6116
|
+
};
|
|
6117
|
+
selectedTypes = [];
|
|
6118
|
+
for (const flag of requestedFlags) {
|
|
6119
|
+
const type = flagToType[flag];
|
|
6120
|
+
if (type) {
|
|
6121
|
+
selectedTypes.push(type);
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
6124
|
+
if (selectedTypes.length === 0) {
|
|
6125
|
+
console.error("\u26A0\uFE0F No valid flags provided. Use --help to see options.");
|
|
6126
|
+
process.exit(1);
|
|
6127
|
+
}
|
|
6128
|
+
}
|
|
6129
|
+
}
|
|
6130
|
+
const results = runInit(selectedTypes, process.cwd());
|
|
6131
|
+
const output = formatInitResults(results);
|
|
6132
|
+
console.log(output);
|
|
6133
|
+
process.exit(0);
|
|
6134
|
+
}
|
|
4646
6135
|
sessionMemory.init({
|
|
4647
6136
|
projectRoot: process.cwd(),
|
|
4648
6137
|
startTime: Date.now()
|
|
@@ -4653,7 +6142,6 @@ async function main() {
|
|
|
4653
6142
|
version: SERVER_VERSION
|
|
4654
6143
|
},
|
|
4655
6144
|
{
|
|
4656
|
-
// Capabilities declaration
|
|
4657
6145
|
capabilities: {
|
|
4658
6146
|
tools: {},
|
|
4659
6147
|
resources: {}
|
|
@@ -4672,6 +6160,66 @@ async function main() {
|
|
|
4672
6160
|
`[${SERVER_NAME}] Server connected via stdio. Waiting for requests...`
|
|
4673
6161
|
);
|
|
4674
6162
|
}
|
|
6163
|
+
function interactiveSelect() {
|
|
6164
|
+
const labels = [
|
|
6165
|
+
{ type: "claude", label: "1) Claude Code (CLAUDE.md)" },
|
|
6166
|
+
{ type: "cursor", label: "2) Cursor (.cursor/rules/kuma.mdc)" },
|
|
6167
|
+
{ type: "windsurf", label: "3) Windsurf (.windsurfrules)" },
|
|
6168
|
+
{ type: "copilot", label: "4) GitHub Copilot Editor (AGENTS.md + Skill)" },
|
|
6169
|
+
{ type: "cline", label: "5) Cline (.clinerules/kuma.md)" },
|
|
6170
|
+
{ type: "aider", label: "6) Aider (CONVENTIONS.md via .aider.conf.yml)" },
|
|
6171
|
+
{ type: "antigravity", label: "7) Antigravity CLI (.agents/skills/)" },
|
|
6172
|
+
{ type: "opencode", label: "8) OpenCode (opencode.json)" },
|
|
6173
|
+
{ type: "codex", label: "9) Codex CLI - OpenAI (AGENTS.md + .codex/config.toml)" },
|
|
6174
|
+
{ type: "qwen", label: "10) Qwen Code (AGENTS.md + settings.json)" },
|
|
6175
|
+
{ type: "kiro", label: "11) Kiro (.kiro/steering/kuma.md)" },
|
|
6176
|
+
{ type: "openclaw", label: "12) OpenClaw (skills/kuma/SKILL.md)" },
|
|
6177
|
+
{ type: "codewhale", label: "13) CodeWhale (skills/kuma/SKILL.md + .codewhale/mcp.json)" }
|
|
6178
|
+
];
|
|
6179
|
+
const rl = readline.createInterface({
|
|
6180
|
+
input: process.stdin,
|
|
6181
|
+
output: process.stderr
|
|
6182
|
+
});
|
|
6183
|
+
return new Promise((resolve) => {
|
|
6184
|
+
console.error("");
|
|
6185
|
+
for (const l of labels) {
|
|
6186
|
+
console.error(l.label);
|
|
6187
|
+
}
|
|
6188
|
+
console.error("");
|
|
6189
|
+
rl.question("Enter numbers separated by space (e.g. '1 3 5'), or 'all': ", (answer) => {
|
|
6190
|
+
rl.close();
|
|
6191
|
+
const input = answer.trim().toLowerCase();
|
|
6192
|
+
if (input === "all") {
|
|
6193
|
+
resolve(ALL_CONFIG_TYPES);
|
|
6194
|
+
return;
|
|
6195
|
+
}
|
|
6196
|
+
const nums = input.split(/\s+/).map(Number).filter((n) => n >= 1 && n <= 13);
|
|
6197
|
+
const typeMap = {
|
|
6198
|
+
1: "claude",
|
|
6199
|
+
2: "cursor",
|
|
6200
|
+
3: "windsurf",
|
|
6201
|
+
4: "copilot",
|
|
6202
|
+
5: "cline",
|
|
6203
|
+
6: "aider",
|
|
6204
|
+
7: "antigravity",
|
|
6205
|
+
8: "opencode",
|
|
6206
|
+
9: "codex",
|
|
6207
|
+
10: "qwen",
|
|
6208
|
+
11: "kiro",
|
|
6209
|
+
12: "openclaw",
|
|
6210
|
+
13: "codewhale"
|
|
6211
|
+
};
|
|
6212
|
+
const selected = [];
|
|
6213
|
+
for (const n of nums) {
|
|
6214
|
+
const t = typeMap[n];
|
|
6215
|
+
if (t && !selected.includes(t)) {
|
|
6216
|
+
selected.push(t);
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
resolve(selected);
|
|
6220
|
+
});
|
|
6221
|
+
});
|
|
6222
|
+
}
|
|
4675
6223
|
main().catch((err) => {
|
|
4676
6224
|
console.error(`[${SERVER_NAME}] Fatal error:`, err);
|
|
4677
6225
|
process.exit(1);
|