@plumpslabs/kuma 2.1.0 → 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/dist/index.js +826 -731
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -75,6 +75,21 @@ function validateFilePath(filePath, projectRoot) {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
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
|
+
}
|
|
78
93
|
if (normalizedPath.includes("node_modules")) {
|
|
79
94
|
return {
|
|
80
95
|
valid: true,
|
|
@@ -580,6 +595,10 @@ No unresolved issues.`;
|
|
|
580
595
|
});
|
|
581
596
|
return results.slice(0, limit);
|
|
582
597
|
}
|
|
598
|
+
getConventions() {
|
|
599
|
+
this.ensureInit();
|
|
600
|
+
return this.state.conventions;
|
|
601
|
+
}
|
|
583
602
|
pruneMemory() {
|
|
584
603
|
this.ensureInit();
|
|
585
604
|
this.state.toolCalls = this.state.toolCalls.slice(-3);
|
|
@@ -1594,156 +1613,565 @@ function formatBatchResult(results, totalRequested) {
|
|
|
1594
1613
|
return lines.join("\n");
|
|
1595
1614
|
}
|
|
1596
1615
|
|
|
1597
|
-
// src/utils/processRunner.ts
|
|
1598
|
-
import { spawn } from "child_process";
|
|
1599
|
-
var DEFAULT_MAX_STDOUT = 5e3;
|
|
1600
|
-
var DEFAULT_MAX_STDERR = 2e3;
|
|
1601
|
-
var DEFAULT_TIMEOUT = 60;
|
|
1602
|
-
function spawnProcess(command, options) {
|
|
1603
|
-
const {
|
|
1604
|
-
cwd,
|
|
1605
|
-
timeoutSeconds = DEFAULT_TIMEOUT,
|
|
1606
|
-
useShell = false,
|
|
1607
|
-
maxStdout = DEFAULT_MAX_STDOUT,
|
|
1608
|
-
maxStderr = DEFAULT_MAX_STDERR
|
|
1609
|
-
} = options;
|
|
1610
|
-
return new Promise((resolve) => {
|
|
1611
|
-
const parts = command.split(" ");
|
|
1612
|
-
const cmd = parts[0];
|
|
1613
|
-
const args = parts.slice(1);
|
|
1614
|
-
const proc = spawn(cmd, args, {
|
|
1615
|
-
cwd,
|
|
1616
|
-
shell: useShell,
|
|
1617
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1618
|
-
});
|
|
1619
|
-
let stdout = "";
|
|
1620
|
-
let stderr = "";
|
|
1621
|
-
let timedOut = false;
|
|
1622
|
-
const timeoutId = setTimeout(() => {
|
|
1623
|
-
timedOut = true;
|
|
1624
|
-
proc.kill("SIGTERM");
|
|
1625
|
-
if (process.platform === "win32") {
|
|
1626
|
-
try {
|
|
1627
|
-
spawn("taskkill", ["/pid", String(proc.pid), "/f", "/t"], {
|
|
1628
|
-
stdio: "ignore"
|
|
1629
|
-
});
|
|
1630
|
-
} catch {
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
}, timeoutSeconds * 1e3);
|
|
1634
|
-
proc.stdout?.on("data", (data) => {
|
|
1635
|
-
stdout += data.toString();
|
|
1636
|
-
});
|
|
1637
|
-
proc.stderr?.on("data", (data) => {
|
|
1638
|
-
stderr += data.toString();
|
|
1639
|
-
});
|
|
1640
|
-
proc.on("close", (code) => {
|
|
1641
|
-
clearTimeout(timeoutId);
|
|
1642
|
-
resolve({
|
|
1643
|
-
stdout: truncateOutput(stdout, maxStdout),
|
|
1644
|
-
stderr: truncateOutput(stderr, maxStderr),
|
|
1645
|
-
exitCode: code ?? -1,
|
|
1646
|
-
timedOut
|
|
1647
|
-
});
|
|
1648
|
-
});
|
|
1649
|
-
proc.on("error", () => {
|
|
1650
|
-
clearTimeout(timeoutId);
|
|
1651
|
-
resolve({
|
|
1652
|
-
stdout,
|
|
1653
|
-
stderr: `Failed to spawn process: ${cmd}`,
|
|
1654
|
-
exitCode: -1,
|
|
1655
|
-
timedOut: false
|
|
1656
|
-
});
|
|
1657
|
-
});
|
|
1658
|
-
});
|
|
1659
|
-
}
|
|
1660
|
-
function spawnShell(command, options) {
|
|
1661
|
-
return spawnProcess(command, { ...options, useShell: true });
|
|
1662
|
-
}
|
|
1663
|
-
function truncateOutput(output, maxChars) {
|
|
1664
|
-
if (output.length <= maxChars) return output;
|
|
1665
|
-
return output.slice(0, maxChars) + `
|
|
1666
|
-
|
|
1667
|
-
[...truncated, ${output.length - maxChars} more characters]`;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
1616
|
// src/tools/safeTerminalExec.ts
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
build: "npm run build",
|
|
1674
|
-
lint: "npm run lint",
|
|
1675
|
-
typecheck: "npx tsc --noEmit"
|
|
1676
|
-
};
|
|
1677
|
-
var DANGEROUS_PATTERNS = [
|
|
1678
|
-
"rm -rf",
|
|
1679
|
-
"rm -fr",
|
|
1680
|
-
"del /f",
|
|
1681
|
-
"rd /s",
|
|
1682
|
-
"rmdir /s",
|
|
1683
|
-
"git push",
|
|
1684
|
-
"git commit",
|
|
1685
|
-
"npm publish",
|
|
1686
|
-
"npx publish",
|
|
1687
|
-
"yarn publish",
|
|
1688
|
-
"pnpm publish",
|
|
1689
|
-
"> /dev/sda",
|
|
1690
|
-
"format",
|
|
1691
|
-
"mkfs",
|
|
1692
|
-
"dd if=",
|
|
1693
|
-
":(){ :|:& };:",
|
|
1694
|
-
"curl ",
|
|
1695
|
-
"wget "
|
|
1696
|
-
];
|
|
1697
|
-
async function handleSafeTerminalExec(params) {
|
|
1698
|
-
const { task, customCommand, timeout = 60 } = params;
|
|
1699
|
-
if (task === "custom" && !customCommand) {
|
|
1700
|
-
return "Error: Task 'custom' requires the 'customCommand' parameter.";
|
|
1701
|
-
}
|
|
1702
|
-
const command = task === "custom" ? customCommand : TASK_COMMANDS[task];
|
|
1703
|
-
const cbResult = circuitBreaker.check("safe_terminal_exec", { task, command });
|
|
1704
|
-
if (!cbResult.allowed) {
|
|
1705
|
-
return `\u26A0\uFE0F Circuit breaker: ${cbResult.reason}
|
|
1617
|
+
import fs8 from "fs";
|
|
1618
|
+
import path8 from "path";
|
|
1706
1619
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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;
|
|
1713
1627
|
}
|
|
1714
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 = [];
|
|
1715
1692
|
try {
|
|
1716
|
-
|
|
1717
|
-
const
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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");
|
|
1724
1710
|
}
|
|
1725
|
-
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
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";
|
|
1730
1724
|
}
|
|
1731
|
-
|
|
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";
|
|
1732
1733
|
}
|
|
1734
|
+
const subFrameworks = scanSubProjectFrameworks(root);
|
|
1735
|
+
if (subFrameworks.length > 0) return "web-app";
|
|
1736
|
+
return "unknown";
|
|
1733
1737
|
}
|
|
1734
|
-
function
|
|
1735
|
-
const
|
|
1736
|
-
const
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
""
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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
|
+
}
|
|
1745
1751
|
}
|
|
1746
|
-
if (
|
|
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)
|
|
1780
|
+
});
|
|
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;
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
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()) {
|
|
1747
2175
|
lines.push("\u{1F4E4} STDERR:", "```", result.stderr, "```", "");
|
|
1748
2176
|
}
|
|
1749
2177
|
if (result.exitCode !== 0) {
|
|
@@ -1776,8 +2204,8 @@ function formatTimeoutResult(command, timeout) {
|
|
|
1776
2204
|
}
|
|
1777
2205
|
|
|
1778
2206
|
// src/agents/codeReviewer.ts
|
|
1779
|
-
import
|
|
1780
|
-
import
|
|
2207
|
+
import fs9 from "fs";
|
|
2208
|
+
import path9 from "path";
|
|
1781
2209
|
import { execSync } from "child_process";
|
|
1782
2210
|
var CODE_EXTENSIONS = [
|
|
1783
2211
|
".ts",
|
|
@@ -1809,7 +2237,7 @@ function getGitChangedFiles() {
|
|
|
1809
2237
|
if (filePath.startsWith('"') && filePath.endsWith('"')) {
|
|
1810
2238
|
filePath = filePath.substring(1, filePath.length - 1);
|
|
1811
2239
|
}
|
|
1812
|
-
const ext =
|
|
2240
|
+
const ext = path9.extname(filePath).toLowerCase();
|
|
1813
2241
|
if (CODE_EXTENSIONS.includes(ext)) files.push(filePath);
|
|
1814
2242
|
}
|
|
1815
2243
|
return files.slice(0, 10);
|
|
@@ -1846,7 +2274,7 @@ Pass an explicit 'files' array to review specific files.`;
|
|
|
1846
2274
|
continue;
|
|
1847
2275
|
}
|
|
1848
2276
|
const resolvedPath = validation.resolvedPath;
|
|
1849
|
-
if (!
|
|
2277
|
+
if (!fs9.existsSync(resolvedPath)) {
|
|
1850
2278
|
allIssues.push({
|
|
1851
2279
|
file: filePath,
|
|
1852
2280
|
line: 0,
|
|
@@ -1858,7 +2286,7 @@ Pass an explicit 'files' array to review specific files.`;
|
|
|
1858
2286
|
continue;
|
|
1859
2287
|
}
|
|
1860
2288
|
try {
|
|
1861
|
-
const content =
|
|
2289
|
+
const content = fs9.readFileSync(resolvedPath, "utf-8");
|
|
1862
2290
|
filesReviewed++;
|
|
1863
2291
|
checkGeneral(filePath, content, allIssues);
|
|
1864
2292
|
switch (focus) {
|
|
@@ -1952,7 +2380,7 @@ function isTestFile(filePath) {
|
|
|
1952
2380
|
return /\.(test|spec)\.[a-z]+$/i.test(filePath) || /__tests__/.test(filePath);
|
|
1953
2381
|
}
|
|
1954
2382
|
function isTsLike(filePath) {
|
|
1955
|
-
const ext =
|
|
2383
|
+
const ext = path9.extname(filePath).toLowerCase();
|
|
1956
2384
|
return ext === ".ts" || ext === ".tsx";
|
|
1957
2385
|
}
|
|
1958
2386
|
function checkGeneral(filePath, content, issues) {
|
|
@@ -2391,460 +2819,105 @@ function checkOverEngineering(filePath, content, issues) {
|
|
|
2391
2819
|
line: lineNum,
|
|
2392
2820
|
severity: "info",
|
|
2393
2821
|
rule: sp.rule,
|
|
2394
|
-
message: sp.msg,
|
|
2395
|
-
suggestion: sp.suggestion
|
|
2396
|
-
});
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
const exportDecls = [];
|
|
2400
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2401
|
-
const ed = lines[i].match(/^\s*export\s+(const|let|var|function|class)\s+(\w+)/);
|
|
2402
|
-
if (ed) exportDecls.push({ name: ed[2], line: i + 1 });
|
|
2403
|
-
}
|
|
2404
|
-
for (const decl of exportDecls) {
|
|
2405
|
-
const refCount = (text.match(new RegExp(`\\b${decl.name}\\b`, "g")) || []).length;
|
|
2406
|
-
const isReactComponent = /^[A-Z]/.test(decl.name) && /return\s+<|React\./.test(text);
|
|
2407
|
-
if (refCount <= 1 && decl.name !== "default" && !isReactComponent) {
|
|
2408
|
-
issues.push({
|
|
2409
|
-
file: filePath,
|
|
2410
|
-
line: decl.line,
|
|
2411
|
-
severity: "info",
|
|
2412
|
-
rule: "over-engineering/unused-export",
|
|
2413
|
-
message: `"${decl.name}" is exported but referenced only in its declaration`,
|
|
2414
|
-
suggestion: "Check if it is imported elsewhere; if not, remove or inline"
|
|
2415
|
-
});
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
function findBlockBodies(lines, startPattern) {
|
|
2420
|
-
const results = [];
|
|
2421
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2422
|
-
const m = lines[i].match(startPattern);
|
|
2423
|
-
if (!m) continue;
|
|
2424
|
-
const name = m[0].match(/class\s+(\w+)/)?.[1] ?? "Unknown";
|
|
2425
|
-
const body = collectBlockLines(lines, i);
|
|
2426
|
-
results.push({ body: body.join("\n"), nameLine: i + 1, name });
|
|
2427
|
-
}
|
|
2428
|
-
return results;
|
|
2429
|
-
}
|
|
2430
|
-
function collectBlockLines(lines, startIdx) {
|
|
2431
|
-
let depth = 0;
|
|
2432
|
-
let started = false;
|
|
2433
|
-
const block = [];
|
|
2434
|
-
for (let j = startIdx; j < lines.length; j++) {
|
|
2435
|
-
const line = lines[j];
|
|
2436
|
-
block.push(line);
|
|
2437
|
-
for (const ch of line) {
|
|
2438
|
-
if (ch === "{") {
|
|
2439
|
-
depth++;
|
|
2440
|
-
started = true;
|
|
2441
|
-
} else if (ch === "}") depth--;
|
|
2442
|
-
}
|
|
2443
|
-
if (started && depth <= 0) break;
|
|
2444
|
-
}
|
|
2445
|
-
return block;
|
|
2446
|
-
}
|
|
2447
|
-
function extractBodyAfter(lines, match) {
|
|
2448
|
-
const startIdx = lines.findIndex((l) => l.includes(match[0].trim().split(/\s+/).pop() ?? ""));
|
|
2449
|
-
if (startIdx < 0) return "";
|
|
2450
|
-
return collectBlockLines(lines, startIdx).join("\n");
|
|
2451
|
-
}
|
|
2452
|
-
function formatReviewOutput(issues, filesReviewed, totalFiles, focus, customCriteria, autoDetected) {
|
|
2453
|
-
const errors = issues.filter((i) => i.severity === "error");
|
|
2454
|
-
const warnings = issues.filter((i) => i.severity === "warning");
|
|
2455
|
-
const infos = issues.filter((i) => i.severity === "info");
|
|
2456
|
-
const lines = [
|
|
2457
|
-
`\u{1F50D} **Code Review \u2014 Focus: ${focus}**`,
|
|
2458
|
-
...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."] : [],
|
|
2459
|
-
...autoDetected ? [`\u{1F4C2} Auto-detected ${totalFiles} changed file(s) from git`] : [],
|
|
2460
|
-
...customCriteria ? [`\u{1F4CB} **Custom Criteria:** ${customCriteria}`] : [],
|
|
2461
|
-
`\u{1F4C1} ${filesReviewed}/${totalFiles} files reviewed`,
|
|
2462
|
-
`\u{1F534} ${errors.length} errors | \u{1F7E1} ${warnings.length} warnings | \u{1F535} ${infos.length} info`,
|
|
2463
|
-
""
|
|
2464
|
-
];
|
|
2465
|
-
if (issues.length === 0) {
|
|
2466
|
-
lines.push("\u2705 No issues found.");
|
|
2467
|
-
return lines.join("\n");
|
|
2468
|
-
}
|
|
2469
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
2470
|
-
for (const issue of issues) {
|
|
2471
|
-
const existing = grouped.get(issue.file) ?? [];
|
|
2472
|
-
existing.push(issue);
|
|
2473
|
-
grouped.set(issue.file, existing);
|
|
2474
|
-
}
|
|
2475
|
-
for (const [file, fileIssues] of grouped) {
|
|
2476
|
-
lines.push(`**\u{1F4C4} ${file}:**`);
|
|
2477
|
-
for (const issue of fileIssues) {
|
|
2478
|
-
const icon = issue.severity === "error" ? "\u{1F534}" : issue.severity === "warning" ? "\u{1F7E1}" : "\u{1F535}";
|
|
2479
|
-
lines.push(` ${icon} [L${issue.line}] [${issue.rule}] ${issue.message}`);
|
|
2480
|
-
if (issue.suggestion) lines.push(` \u{1F4A1} ${issue.suggestion}`);
|
|
2481
|
-
}
|
|
2482
|
-
lines.push("");
|
|
2483
|
-
}
|
|
2484
|
-
if (errors.length > 0) {
|
|
2485
|
-
lines.push("\u26A0\uFE0F Fix errors first, then warnings.");
|
|
2486
|
-
lines.push("\u{1F4A1} Use precise_diff_editor to apply fixes.");
|
|
2487
|
-
} else if (warnings.length > 0) {
|
|
2488
|
-
lines.push("\u{1F4A1} Address warnings before merging.");
|
|
2489
|
-
} else {
|
|
2490
|
-
lines.push("\u2705 Only informational issues \u2014 ready for testing.");
|
|
2491
|
-
}
|
|
2492
|
-
return lines.join("\n");
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
// src/utils/conventionsDetector.ts
|
|
2496
|
-
import fs8 from "fs";
|
|
2497
|
-
import path8 from "path";
|
|
2498
|
-
var cachedConventions = null;
|
|
2499
|
-
async function detectConventions(forceRescan = false) {
|
|
2500
|
-
if (cachedConventions && !forceRescan) {
|
|
2501
|
-
return cachedConventions;
|
|
2502
|
-
}
|
|
2503
|
-
const projectRoot = getProjectRoot();
|
|
2504
|
-
const workspaces = detectWorkspaces(projectRoot);
|
|
2505
|
-
const conventions = {
|
|
2506
|
-
framework: detectFramework(projectRoot),
|
|
2507
|
-
projectType: detectProjectType(projectRoot),
|
|
2508
|
-
testRunner: detectTestRunner(projectRoot),
|
|
2509
|
-
styling: detectStyling(projectRoot),
|
|
2510
|
-
importAlias: detectImportAlias(projectRoot),
|
|
2511
|
-
lintRules: detectLintRules(projectRoot),
|
|
2512
|
-
packageManager: detectPackageManager(),
|
|
2513
|
-
moduleSystem: detectModuleSystem(projectRoot),
|
|
2514
|
-
language: detectLanguage(projectRoot),
|
|
2515
|
-
features: detectFeatures(projectRoot),
|
|
2516
|
-
isMonorepo: workspaces.length > 0,
|
|
2517
|
-
workspaces
|
|
2518
|
-
};
|
|
2519
|
-
cachedConventions = conventions;
|
|
2520
|
-
return conventions;
|
|
2521
|
-
}
|
|
2522
|
-
function detectFramework(root) {
|
|
2523
|
-
const pkg = readJsonSafe(path8.join(root, "package.json"));
|
|
2524
|
-
if (pkg) {
|
|
2525
|
-
const pkgDeps = pkg.dependencies ?? {};
|
|
2526
|
-
const pkgDevDeps = pkg.devDependencies ?? {};
|
|
2527
|
-
const deps = { ...pkgDeps, ...pkgDevDeps };
|
|
2528
|
-
if (deps.next) return "Next.js";
|
|
2529
|
-
if (deps["@remix-run/react"]) return "Remix";
|
|
2530
|
-
if (deps.gatsby) return "Gatsby";
|
|
2531
|
-
if (deps.nuxt || deps["@nuxt/core"]) return "Nuxt.js";
|
|
2532
|
-
if (deps.vue) return "Vue";
|
|
2533
|
-
if (deps.react) return "React";
|
|
2534
|
-
if (deps.svelte) return "Svelte";
|
|
2535
|
-
if (deps.astro) return "Astro";
|
|
2536
|
-
if (deps.solid || deps["solid-js"]) return "SolidJS";
|
|
2537
|
-
if (deps.qwik || deps["@builder.io/qwik"]) return "Qwik";
|
|
2538
|
-
if (deps.vite) return "Vite";
|
|
2539
|
-
if (deps["@nestjs/core"]) return "NestJS";
|
|
2540
|
-
if (deps.fastify) return "Fastify";
|
|
2541
|
-
if (deps.express) return "Express";
|
|
2542
|
-
if (deps.koa) return "Koa";
|
|
2543
|
-
if (deps.hono) return "Hono";
|
|
2544
|
-
if (deps["@hapi/hapi"]) return "Hapi";
|
|
2545
|
-
if (deps["@modelcontextprotocol/sdk"]) return "MCP Server";
|
|
2546
|
-
if (deps["@anthropic-ai/claude-agent-sdk"]) return "Claude Agent SDK";
|
|
2547
|
-
if (deps.commander) return "Commander CLI";
|
|
2548
|
-
if (deps.yargs) return "Yargs CLI";
|
|
2549
|
-
if (deps["@oclif/core"] || deps.oclif) return "Oclif CLI";
|
|
2550
|
-
if (deps.ink) return "Ink (React CLI)";
|
|
2551
|
-
if (pkg.bin) return "CLI Tool";
|
|
2552
|
-
if (pkg.main || pkg.exports) return "Library";
|
|
2553
|
-
}
|
|
2554
|
-
const subFrameworks = scanSubProjectFrameworks(root);
|
|
2555
|
-
if (subFrameworks.length > 0) {
|
|
2556
|
-
const freq = /* @__PURE__ */ new Map();
|
|
2557
|
-
for (const fw of subFrameworks) {
|
|
2558
|
-
freq.set(fw, (freq.get(fw) ?? 0) + 1);
|
|
2559
|
-
}
|
|
2560
|
-
const [topFw] = [...freq.entries()].sort((a, b) => b[1] - a[1])[0] ?? [];
|
|
2561
|
-
if (topFw) return `${topFw} (monorepo)`;
|
|
2562
|
-
}
|
|
2563
|
-
return "unknown";
|
|
2564
|
-
}
|
|
2565
|
-
function scanSubProjectFrameworks(root) {
|
|
2566
|
-
const frameworks = [];
|
|
2567
|
-
try {
|
|
2568
|
-
const entries = fs8.readdirSync(root, { withFileTypes: true });
|
|
2569
|
-
for (const entry of entries) {
|
|
2570
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
2571
|
-
const subDir = path8.join(root, entry.name);
|
|
2572
|
-
const subPkg = readJsonSafe(path8.join(subDir, "package.json"));
|
|
2573
|
-
if (!subPkg) continue;
|
|
2574
|
-
const subDeps = { ...subPkg.dependencies ?? {}, ...subPkg.devDependencies ?? {} };
|
|
2575
|
-
if (subDeps.next) frameworks.push("Next.js");
|
|
2576
|
-
else if (subDeps.react) frameworks.push("React");
|
|
2577
|
-
else if (subDeps.vue) frameworks.push("Vue");
|
|
2578
|
-
else if (subDeps.svelte) frameworks.push("Svelte");
|
|
2579
|
-
else if (subDeps.astro) frameworks.push("Astro");
|
|
2580
|
-
else if (subDeps["@nestjs/core"]) frameworks.push("NestJS");
|
|
2581
|
-
else if (subDeps.express) frameworks.push("Express");
|
|
2582
|
-
else if (subDeps.hono) frameworks.push("Hono");
|
|
2583
|
-
else if (subDeps.vite) frameworks.push("Vite");
|
|
2584
|
-
else if (subDeps["@modelcontextprotocol/sdk"]) frameworks.push("MCP Server");
|
|
2585
|
-
}
|
|
2586
|
-
} catch {
|
|
2587
|
-
}
|
|
2588
|
-
return frameworks;
|
|
2589
|
-
}
|
|
2590
|
-
function detectProjectType(root) {
|
|
2591
|
-
const pkg = readJsonSafe(path8.join(root, "package.json"));
|
|
2592
|
-
if (pkg) {
|
|
2593
|
-
const pkgDeps = pkg.dependencies ?? {};
|
|
2594
|
-
const pkgDevDeps = pkg.devDependencies ?? {};
|
|
2595
|
-
const deps = { ...pkgDeps, ...pkgDevDeps };
|
|
2596
|
-
if (deps["@modelcontextprotocol/sdk"]) return "mcp-server";
|
|
2597
|
-
if (deps.next || deps.react || deps.vue || deps.svelte || deps.astro || deps.gatsby || deps.nuxt || deps["@remix-run/react"] || deps.solid) {
|
|
2598
|
-
return "web-app";
|
|
2599
|
-
}
|
|
2600
|
-
if (deps.express || deps.fastify || deps.koa || deps.hono || deps["@nestjs/core"] || deps["@hapi/hapi"]) {
|
|
2601
|
-
return "backend";
|
|
2602
|
-
}
|
|
2603
|
-
if (deps.commander || deps.yargs || deps["@oclif/core"] || deps.oclif || deps.ink) {
|
|
2604
|
-
return "cli";
|
|
2605
|
-
}
|
|
2606
|
-
if (pkg.bin) return "cli";
|
|
2607
|
-
if (pkg.main || pkg.exports) return "library";
|
|
2608
|
-
}
|
|
2609
|
-
const subFrameworks = scanSubProjectFrameworks(root);
|
|
2610
|
-
if (subFrameworks.length > 0) return "web-app";
|
|
2611
|
-
return "unknown";
|
|
2612
|
-
}
|
|
2613
|
-
function detectWorkspaces(root) {
|
|
2614
|
-
const results = [];
|
|
2615
|
-
const pkg = readJsonSafe(path8.join(root, "package.json"));
|
|
2616
|
-
const pnpmWorkspace = readYamlLite(path8.join(root, "pnpm-workspace.yaml"));
|
|
2617
|
-
const patterns = /* @__PURE__ */ new Set();
|
|
2618
|
-
const pkgWorkspaces = pkg?.workspaces;
|
|
2619
|
-
if (Array.isArray(pkgWorkspaces)) {
|
|
2620
|
-
for (const p of pkgWorkspaces) if (typeof p === "string") patterns.add(p);
|
|
2621
|
-
} else if (pkgWorkspaces && typeof pkgWorkspaces === "object") {
|
|
2622
|
-
const packages = pkgWorkspaces.packages;
|
|
2623
|
-
if (Array.isArray(packages)) {
|
|
2624
|
-
for (const p of packages) if (typeof p === "string") patterns.add(p);
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
|
-
if (Array.isArray(pnpmWorkspace?.packages)) {
|
|
2628
|
-
for (const p of pnpmWorkspace.packages) if (typeof p === "string") patterns.add(p);
|
|
2629
|
-
}
|
|
2630
|
-
patterns.add("apps/*");
|
|
2631
|
-
patterns.add("packages/*");
|
|
2632
|
-
patterns.add("services/*");
|
|
2633
|
-
for (const pattern of patterns) {
|
|
2634
|
-
const match = pattern.match(/^([^*]+)\/\*$/);
|
|
2635
|
-
if (!match) continue;
|
|
2636
|
-
const dir = path8.join(root, match[1]);
|
|
2637
|
-
if (!fs8.existsSync(dir)) continue;
|
|
2638
|
-
let entries = [];
|
|
2639
|
-
try {
|
|
2640
|
-
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
2641
|
-
} catch {
|
|
2642
|
-
continue;
|
|
2643
|
-
}
|
|
2644
|
-
for (const entry of entries) {
|
|
2645
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
2646
|
-
const pkgPath = path8.join(dir, entry.name, "package.json");
|
|
2647
|
-
const subPkg = readJsonSafe(pkgPath);
|
|
2648
|
-
if (!subPkg) continue;
|
|
2649
|
-
results.push({
|
|
2650
|
-
path: path8.relative(root, path8.join(dir, entry.name)),
|
|
2651
|
-
name: subPkg.name ?? entry.name,
|
|
2652
|
-
framework: detectFramework(path8.join(dir, entry.name))
|
|
2822
|
+
message: sp.msg,
|
|
2823
|
+
suggestion: sp.suggestion
|
|
2653
2824
|
});
|
|
2654
2825
|
}
|
|
2655
2826
|
}
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
return true;
|
|
2661
|
-
});
|
|
2662
|
-
}
|
|
2663
|
-
function readYamlLite(filePath) {
|
|
2664
|
-
try {
|
|
2665
|
-
if (!fs8.existsSync(filePath)) return null;
|
|
2666
|
-
const text = fs8.readFileSync(filePath, "utf-8");
|
|
2667
|
-
const packages = [];
|
|
2668
|
-
let inPackages = false;
|
|
2669
|
-
for (const rawLine of text.split("\n")) {
|
|
2670
|
-
const line = rawLine.replace(/#.*$/, "").trimEnd();
|
|
2671
|
-
if (/^packages:\s*$/.test(line)) {
|
|
2672
|
-
inPackages = true;
|
|
2673
|
-
continue;
|
|
2674
|
-
}
|
|
2675
|
-
if (inPackages) {
|
|
2676
|
-
const m = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/);
|
|
2677
|
-
if (m) packages.push(m[1]);
|
|
2678
|
-
else if (/^\S/.test(line)) inPackages = false;
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
return { packages };
|
|
2682
|
-
} catch {
|
|
2683
|
-
return null;
|
|
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 });
|
|
2684
2831
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
if (deps["@testing-library/react"]) return "React Testing Library";
|
|
2699
|
-
const scripts = pkg.scripts ?? {};
|
|
2700
|
-
if (scripts.test) {
|
|
2701
|
-
if (scripts.test.includes("vitest")) return "Vitest";
|
|
2702
|
-
if (scripts.test.includes("jest")) return "Jest";
|
|
2703
|
-
if (scripts.test.includes("mocha")) return "Mocha";
|
|
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
|
+
}
|
|
2704
2845
|
}
|
|
2705
|
-
return "unknown";
|
|
2706
2846
|
}
|
|
2707
|
-
function
|
|
2708
|
-
const
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
if (deps["@emotion/react"]) return "Emotion";
|
|
2716
|
-
if (deps.linaria) return "Linaria";
|
|
2717
|
-
if (deps.sass || deps["node-sass"]) return "SCSS";
|
|
2718
|
-
if (deps.less) return "Less";
|
|
2719
|
-
if (deps["@vanilla-extract/css"]) return "Vanilla Extract";
|
|
2720
|
-
if (deps.cssmodules || hasCSSModules(root)) return "CSS Modules";
|
|
2721
|
-
const srcDir = path8.join(root, "src");
|
|
2722
|
-
if (fs8.existsSync(srcDir)) {
|
|
2723
|
-
const cssFiles = findFiles(srcDir, [".css", ".scss", ".less"]);
|
|
2724
|
-
if (cssFiles.length > 0) return "Plain CSS/SCSS";
|
|
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 });
|
|
2725
2855
|
}
|
|
2726
|
-
return
|
|
2856
|
+
return results;
|
|
2727
2857
|
}
|
|
2728
|
-
function
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
const
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
if (
|
|
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--;
|
|
2870
|
+
}
|
|
2871
|
+
if (started && depth <= 0) break;
|
|
2742
2872
|
}
|
|
2743
|
-
return
|
|
2744
|
-
}
|
|
2745
|
-
function detectLintRules(root) {
|
|
2746
|
-
const rules = [];
|
|
2747
|
-
if (fs8.existsSync(path8.join(root, ".eslintrc"))) rules.push("ESLint (.eslintrc)");
|
|
2748
|
-
if (fs8.existsSync(path8.join(root, ".eslintrc.js"))) rules.push("ESLint (.eslintrc.js)");
|
|
2749
|
-
if (fs8.existsSync(path8.join(root, ".eslintrc.json"))) rules.push("ESLint (.eslintrc.json)");
|
|
2750
|
-
if (fs8.existsSync(path8.join(root, "eslint.config.js"))) rules.push("ESLint (flat config)");
|
|
2751
|
-
if (fs8.existsSync(path8.join(root, ".prettierrc"))) rules.push("Prettier");
|
|
2752
|
-
if (fs8.existsSync(path8.join(root, ".prettierrc.json"))) rules.push("Prettier");
|
|
2753
|
-
if (fs8.existsSync(path8.join(root, ".prettierrc.js"))) rules.push("Prettier");
|
|
2754
|
-
if (fs8.existsSync(path8.join(root, ".stylelintrc"))) rules.push("StyleLint");
|
|
2755
|
-
if (fs8.existsSync(path8.join(root, ".stylelintrc.json"))) rules.push("StyleLint");
|
|
2756
|
-
return rules;
|
|
2873
|
+
return block;
|
|
2757
2874
|
}
|
|
2758
|
-
function
|
|
2759
|
-
const
|
|
2760
|
-
if (
|
|
2761
|
-
|
|
2762
|
-
if (fs8.existsSync(path8.join(root, "package-lock.json"))) return "npm";
|
|
2763
|
-
if (fs8.existsSync(path8.join(root, "bun.lockb"))) return "bun";
|
|
2764
|
-
return "npm";
|
|
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");
|
|
2765
2879
|
}
|
|
2766
|
-
function
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
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");
|
|
2780
2896
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
if (fs8.existsSync(srcDir)) {
|
|
2787
|
-
const tsFiles = findFiles(srcDir, [".ts", ".tsx"]);
|
|
2788
|
-
if (tsFiles.length > 0) return "typescript";
|
|
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);
|
|
2789
2902
|
}
|
|
2790
|
-
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
const pkgDeps4 = pkg.dependencies ?? {};
|
|
2797
|
-
const pkgDevDeps4 = pkg.devDependencies ?? {};
|
|
2798
|
-
const deps = { ...pkgDeps4, ...pkgDevDeps4 };
|
|
2799
|
-
if (deps["@prisma/client"] || deps.prisma) features.push("Prisma ORM");
|
|
2800
|
-
if (deps.typeorm) features.push("TypeORM");
|
|
2801
|
-
if (deps["drizzle-orm"]) features.push("Drizzle ORM");
|
|
2802
|
-
if (deps.graphql || deps["@graphql-tools"]) features.push("GraphQL");
|
|
2803
|
-
if (deps.trpc || deps["@trpc/client"]) features.push("tRPC");
|
|
2804
|
-
if (deps["socket.io"] || deps.ws) features.push("WebSockets");
|
|
2805
|
-
if (deps["@reduxjs/toolkit"] || deps.redux) features.push("Redux");
|
|
2806
|
-
if (deps.zustand) features.push("Zustand");
|
|
2807
|
-
if (deps["react-router"] || deps["react-router-dom"]) features.push("React Router");
|
|
2808
|
-
if (deps["next-auth"] || deps["next-auth/react"]) features.push("NextAuth");
|
|
2809
|
-
if (deps["@tanstack/react-query"]) features.push("React Query");
|
|
2810
|
-
if (deps.jest || deps.vitest) features.push("Unit Testing");
|
|
2811
|
-
if (deps.cypress || deps.playwright) features.push("E2E Testing");
|
|
2812
|
-
if (deps.storybook || deps["@storybook/react"]) features.push("Storybook");
|
|
2813
|
-
if (deps.i18next || deps["react-i18next"]) features.push("i18n");
|
|
2814
|
-
return features;
|
|
2815
|
-
}
|
|
2816
|
-
function readJsonSafe(filePath) {
|
|
2817
|
-
try {
|
|
2818
|
-
if (fs8.existsSync(filePath)) {
|
|
2819
|
-
return JSON.parse(fs8.readFileSync(filePath, "utf-8"));
|
|
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}`);
|
|
2820
2909
|
}
|
|
2821
|
-
|
|
2910
|
+
lines.push("");
|
|
2822
2911
|
}
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
}
|
|
2831
|
-
function findFiles(dir, extensions) {
|
|
2832
|
-
const results = [];
|
|
2833
|
-
try {
|
|
2834
|
-
const entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
2835
|
-
for (const entry of entries) {
|
|
2836
|
-
const fullPath = path8.join(dir, entry.name);
|
|
2837
|
-
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2838
|
-
results.push(...findFiles(fullPath, extensions));
|
|
2839
|
-
} else if (entry.isFile()) {
|
|
2840
|
-
if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
2841
|
-
results.push(fullPath);
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
|
-
} catch {
|
|
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.");
|
|
2846
2919
|
}
|
|
2847
|
-
return
|
|
2920
|
+
return lines.join("\n");
|
|
2848
2921
|
}
|
|
2849
2922
|
|
|
2850
2923
|
// src/agents/projectConventions.ts
|
|
@@ -3035,8 +3108,8 @@ async function handleGitDiff(params) {
|
|
|
3035
3108
|
}
|
|
3036
3109
|
|
|
3037
3110
|
// src/tools/projectStructure.ts
|
|
3038
|
-
import
|
|
3039
|
-
import
|
|
3111
|
+
import fs10 from "fs";
|
|
3112
|
+
import path10 from "path";
|
|
3040
3113
|
var DEFAULT_IGNORE = [
|
|
3041
3114
|
"node_modules",
|
|
3042
3115
|
".git",
|
|
@@ -3073,7 +3146,7 @@ async function handleProjectStructure(params) {
|
|
|
3073
3146
|
sessionMemory.recordToolCall("project_structure", { depth: clampedDepth, folderOnly });
|
|
3074
3147
|
try {
|
|
3075
3148
|
const tree = buildTree(root, root, clampedDepth, 0, folderOnly, includePattern, excludePattern);
|
|
3076
|
-
const projectName =
|
|
3149
|
+
const projectName = path10.basename(root);
|
|
3077
3150
|
const lines = [
|
|
3078
3151
|
"[Project Structure] - " + projectName,
|
|
3079
3152
|
"Depth: " + clampedDepth + " | " + (folderOnly ? "Folders only" : "Files and folders"),
|
|
@@ -3096,7 +3169,7 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3096
3169
|
const lines = [];
|
|
3097
3170
|
let entries = [];
|
|
3098
3171
|
try {
|
|
3099
|
-
entries =
|
|
3172
|
+
entries = fs10.readdirSync(currentDir, { withFileTypes: true });
|
|
3100
3173
|
} catch {
|
|
3101
3174
|
return lines;
|
|
3102
3175
|
}
|
|
@@ -3105,12 +3178,12 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3105
3178
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
3106
3179
|
return a.name.localeCompare(b.name);
|
|
3107
3180
|
});
|
|
3108
|
-
const relativeDir =
|
|
3181
|
+
const relativeDir = path10.relative(root, currentDir) || ".";
|
|
3109
3182
|
const prefix = getPrefix(currentDepth);
|
|
3110
3183
|
for (const entry of entries) {
|
|
3111
3184
|
if (shouldIgnore(entry.name, relativeDir, root)) continue;
|
|
3112
|
-
const fullPath =
|
|
3113
|
-
const relativePath =
|
|
3185
|
+
const fullPath = path10.join(currentDir, entry.name);
|
|
3186
|
+
const relativePath = path10.relative(root, fullPath);
|
|
3114
3187
|
if (includePattern && !entry.name.includes(includePattern) && !relativePath.includes(includePattern)) {
|
|
3115
3188
|
if (!entry.isDirectory()) continue;
|
|
3116
3189
|
}
|
|
@@ -3121,11 +3194,11 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3121
3194
|
lines.push(prefix + "[D] " + entry.name + "/");
|
|
3122
3195
|
let subEntries = [];
|
|
3123
3196
|
try {
|
|
3124
|
-
subEntries =
|
|
3197
|
+
subEntries = fs10.readdirSync(fullPath, { withFileTypes: true });
|
|
3125
3198
|
} catch {
|
|
3126
3199
|
}
|
|
3127
3200
|
const hasVisibleContent = subEntries.some(
|
|
3128
|
-
(e) => !shouldIgnore(e.name,
|
|
3201
|
+
(e) => !shouldIgnore(e.name, path10.relative(root, fullPath), root)
|
|
3129
3202
|
);
|
|
3130
3203
|
if (hasVisibleContent) {
|
|
3131
3204
|
const subLines = buildTree(root, fullPath, maxDepth, currentDepth + 1, folderOnly, includePattern, excludePattern);
|
|
@@ -3156,7 +3229,7 @@ function getPrefix(depth) {
|
|
|
3156
3229
|
}
|
|
3157
3230
|
function getFileSize(fullPath) {
|
|
3158
3231
|
try {
|
|
3159
|
-
const stat =
|
|
3232
|
+
const stat = fs10.statSync(fullPath);
|
|
3160
3233
|
if (stat.size < 1024) return stat.size + "B";
|
|
3161
3234
|
if (stat.size < 1024 * 1024) return (stat.size / 1024).toFixed(0) + "KB";
|
|
3162
3235
|
const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
|
|
@@ -3167,8 +3240,8 @@ function getFileSize(fullPath) {
|
|
|
3167
3240
|
}
|
|
3168
3241
|
|
|
3169
3242
|
// src/tools/staticAnalysis.ts
|
|
3170
|
-
import
|
|
3171
|
-
import
|
|
3243
|
+
import fs11 from "fs";
|
|
3244
|
+
import path11 from "path";
|
|
3172
3245
|
async function handleStaticAnalysis(params) {
|
|
3173
3246
|
const { tool = "all", files, autoFix = false, timeout = 60 } = params;
|
|
3174
3247
|
const root = getProjectRoot();
|
|
@@ -3223,11 +3296,11 @@ function detectAvailableTools(root) {
|
|
|
3223
3296
|
"eslint.config.js",
|
|
3224
3297
|
"eslint.config.mjs"
|
|
3225
3298
|
];
|
|
3226
|
-
const hasEslintConfig = eslintConfigs.some((cfg) =>
|
|
3299
|
+
const hasEslintConfig = eslintConfigs.some((cfg) => fs11.existsSync(path11.join(root, cfg)));
|
|
3227
3300
|
if (hasEslintConfig && depNames.has("eslint")) {
|
|
3228
3301
|
tools.push("eslint");
|
|
3229
3302
|
}
|
|
3230
|
-
if (
|
|
3303
|
+
if (fs11.existsSync(path11.join(root, "tsconfig.json")) && depNames.has("typescript")) {
|
|
3231
3304
|
tools.push("tsc");
|
|
3232
3305
|
}
|
|
3233
3306
|
const prettierConfigs = [
|
|
@@ -3238,20 +3311,20 @@ function detectAvailableTools(root) {
|
|
|
3238
3311
|
".prettierrc.toml",
|
|
3239
3312
|
"prettier.config.js"
|
|
3240
3313
|
];
|
|
3241
|
-
const hasPrettierConfig = prettierConfigs.some((cfg) =>
|
|
3314
|
+
const hasPrettierConfig = prettierConfigs.some((cfg) => fs11.existsSync(path11.join(root, cfg)));
|
|
3242
3315
|
if (hasPrettierConfig && depNames.has("prettier")) {
|
|
3243
3316
|
tools.push("prettier");
|
|
3244
3317
|
}
|
|
3245
|
-
if (
|
|
3318
|
+
if (fs11.existsSync(path11.join(root, "ruff.toml")) || fs11.existsSync(path11.join(root, ".ruff.toml"))) {
|
|
3246
3319
|
tools.push("ruff");
|
|
3247
3320
|
}
|
|
3248
3321
|
return tools;
|
|
3249
3322
|
}
|
|
3250
3323
|
function readPackageJson(root) {
|
|
3251
3324
|
try {
|
|
3252
|
-
const pkgPath =
|
|
3253
|
-
if (
|
|
3254
|
-
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"));
|
|
3255
3328
|
}
|
|
3256
3329
|
} catch {
|
|
3257
3330
|
}
|
|
@@ -3296,8 +3369,8 @@ function buildToolCommand(tool, files, autoFix) {
|
|
|
3296
3369
|
}
|
|
3297
3370
|
function detectPackageManagerPrefix() {
|
|
3298
3371
|
const root = getProjectRoot();
|
|
3299
|
-
if (
|
|
3300
|
-
if (
|
|
3372
|
+
if (fs11.existsSync(path11.join(root, "pnpm-lock.yaml"))) return "pnpm ";
|
|
3373
|
+
if (fs11.existsSync(path11.join(root, "yarn.lock"))) return "yarn ";
|
|
3301
3374
|
return "npx --no-install ";
|
|
3302
3375
|
}
|
|
3303
3376
|
function parseToolOutput(tool, stdout, stderr, projectRoot) {
|
|
@@ -3316,7 +3389,7 @@ function parseToolOutput(tool, stdout, stderr, projectRoot) {
|
|
|
3316
3389
|
}
|
|
3317
3390
|
function resolveToolPath(filePath, projectRoot) {
|
|
3318
3391
|
const trimmed = filePath.trim();
|
|
3319
|
-
return
|
|
3392
|
+
return path11.isAbsolute(trimmed) ? path11.relative(projectRoot, trimmed) : trimmed.replace(/^\.\//, "");
|
|
3320
3393
|
}
|
|
3321
3394
|
function parseEslintOutput(output, projectRoot) {
|
|
3322
3395
|
const issues = [];
|
|
@@ -3593,8 +3666,8 @@ async function handleReflect(params) {
|
|
|
3593
3666
|
import { execSync as execSync6 } from "child_process";
|
|
3594
3667
|
|
|
3595
3668
|
// src/guards/antiPatternDetector.ts
|
|
3596
|
-
import
|
|
3597
|
-
import
|
|
3669
|
+
import fs12 from "fs";
|
|
3670
|
+
import path12 from "path";
|
|
3598
3671
|
import { execSync as execSync4 } from "child_process";
|
|
3599
3672
|
var SCRIPT_PATCH_PATTERNS = [
|
|
3600
3673
|
"writeFileSync",
|
|
@@ -3621,20 +3694,20 @@ function findPatchScripts(projectRoot) {
|
|
|
3621
3694
|
const warnings = [];
|
|
3622
3695
|
const recentFiles = scanRecentFiles(projectRoot);
|
|
3623
3696
|
for (const file of recentFiles) {
|
|
3624
|
-
const ext =
|
|
3697
|
+
const ext = path12.extname(file).toLowerCase();
|
|
3625
3698
|
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
3626
|
-
const relativePath =
|
|
3699
|
+
const relativePath = path12.relative(projectRoot, file);
|
|
3627
3700
|
if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".")) {
|
|
3628
3701
|
continue;
|
|
3629
3702
|
}
|
|
3630
3703
|
try {
|
|
3631
|
-
const content =
|
|
3704
|
+
const content = fs12.readFileSync(file, "utf-8").toLowerCase();
|
|
3632
3705
|
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
3633
3706
|
if (matchedPattern) {
|
|
3634
3707
|
warnings.push({
|
|
3635
3708
|
severity: "high",
|
|
3636
3709
|
pattern: "script-patching",
|
|
3637
|
-
message: `Created script file that modifies other files: ${
|
|
3710
|
+
message: `Created script file that modifies other files: ${path12.basename(file)}`,
|
|
3638
3711
|
suggestion: "Use **precise_diff_editor** instead \u2014 it has fuzzy matching, auto-backup, and rollback support",
|
|
3639
3712
|
evidence: `File: ${relativePath} contains '${matchedPattern}'`,
|
|
3640
3713
|
filePath: relativePath
|
|
@@ -3668,15 +3741,15 @@ function detectBashGrepUsage() {
|
|
|
3668
3741
|
function scanRecentFiles(projectRoot) {
|
|
3669
3742
|
const recent = [];
|
|
3670
3743
|
try {
|
|
3671
|
-
const entries =
|
|
3744
|
+
const entries = fs12.readdirSync(projectRoot, { withFileTypes: true });
|
|
3672
3745
|
const now = Date.now();
|
|
3673
3746
|
const recentThreshold = 30 * 60 * 1e3;
|
|
3674
3747
|
for (const entry of entries) {
|
|
3675
3748
|
if (!entry.isFile()) continue;
|
|
3676
3749
|
try {
|
|
3677
|
-
const stat =
|
|
3750
|
+
const stat = fs12.statSync(path12.join(projectRoot, entry.name));
|
|
3678
3751
|
if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
|
|
3679
|
-
recent.push(
|
|
3752
|
+
recent.push(path12.join(projectRoot, entry.name));
|
|
3680
3753
|
}
|
|
3681
3754
|
} catch {
|
|
3682
3755
|
continue;
|
|
@@ -3700,14 +3773,14 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
3700
3773
|
const prefix = line.substring(0, 2);
|
|
3701
3774
|
if (prefix !== "??" && prefix !== "A ") continue;
|
|
3702
3775
|
const file = line.substring(3).trim();
|
|
3703
|
-
const ext =
|
|
3776
|
+
const ext = path12.extname(file).toLowerCase();
|
|
3704
3777
|
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
3705
3778
|
const isRootLevel = !file.includes("/");
|
|
3706
3779
|
const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
|
|
3707
3780
|
if (!isRootLevel && !isScriptsDir) continue;
|
|
3708
|
-
const fullPath =
|
|
3709
|
-
if (
|
|
3710
|
-
const content =
|
|
3781
|
+
const fullPath = path12.join(projectRoot, file);
|
|
3782
|
+
if (fs12.existsSync(fullPath)) {
|
|
3783
|
+
const content = fs12.readFileSync(fullPath, "utf-8").toLowerCase();
|
|
3711
3784
|
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
3712
3785
|
if (matchedPattern) {
|
|
3713
3786
|
warnings.push({
|
|
@@ -3730,7 +3803,7 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
3730
3803
|
if (deletedStdout) {
|
|
3731
3804
|
const deletedFiles = deletedStdout.split("\n").filter(Boolean);
|
|
3732
3805
|
for (const file of deletedFiles) {
|
|
3733
|
-
const ext =
|
|
3806
|
+
const ext = path12.extname(file).toLowerCase();
|
|
3734
3807
|
if (SCRIPT_EXTENSIONS.includes(ext)) {
|
|
3735
3808
|
warnings.push({
|
|
3736
3809
|
severity: "high",
|
|
@@ -3790,21 +3863,21 @@ function detectAllAntiPatterns() {
|
|
|
3790
3863
|
}
|
|
3791
3864
|
|
|
3792
3865
|
// src/engine/contextSnapshot.ts
|
|
3793
|
-
import
|
|
3794
|
-
import
|
|
3866
|
+
import fs13 from "fs";
|
|
3867
|
+
import path13 from "path";
|
|
3795
3868
|
import { execSync as execSync5 } from "child_process";
|
|
3796
3869
|
var SNAPSHOT_DIR = "context-snapshots";
|
|
3797
3870
|
function snapshotDir() {
|
|
3798
|
-
return
|
|
3871
|
+
return path13.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
|
|
3799
3872
|
}
|
|
3800
3873
|
function ensureSnapshotDir() {
|
|
3801
3874
|
const dir = snapshotDir();
|
|
3802
|
-
if (!
|
|
3803
|
-
|
|
3875
|
+
if (!fs13.existsSync(dir)) {
|
|
3876
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
3804
3877
|
}
|
|
3805
3878
|
}
|
|
3806
3879
|
function snapshotFilePath(timestamp) {
|
|
3807
|
-
return
|
|
3880
|
+
return path13.join(snapshotDir(), `${timestamp}.json`);
|
|
3808
3881
|
}
|
|
3809
3882
|
function saveSnapshot(goal) {
|
|
3810
3883
|
try {
|
|
@@ -3825,7 +3898,7 @@ function saveSnapshot(goal) {
|
|
|
3825
3898
|
hasConventions: !!summary.hasConventions
|
|
3826
3899
|
};
|
|
3827
3900
|
try {
|
|
3828
|
-
|
|
3901
|
+
fs13.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
|
|
3829
3902
|
} catch {
|
|
3830
3903
|
}
|
|
3831
3904
|
sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
|
|
@@ -3833,12 +3906,12 @@ function saveSnapshot(goal) {
|
|
|
3833
3906
|
}
|
|
3834
3907
|
function listSnapshots() {
|
|
3835
3908
|
const dir = snapshotDir();
|
|
3836
|
-
if (!
|
|
3909
|
+
if (!fs13.existsSync(dir)) return [];
|
|
3837
3910
|
try {
|
|
3838
|
-
const files =
|
|
3911
|
+
const files = fs13.readdirSync(dir).filter((f) => f.endsWith(".json")).sort((a, b) => b.localeCompare(a));
|
|
3839
3912
|
return files.map((f) => {
|
|
3840
3913
|
try {
|
|
3841
|
-
const content =
|
|
3914
|
+
const content = fs13.readFileSync(path13.join(dir, f), "utf-8");
|
|
3842
3915
|
return JSON.parse(content);
|
|
3843
3916
|
} catch {
|
|
3844
3917
|
return null;
|
|
@@ -4046,8 +4119,8 @@ async function handleKumaContext(params) {
|
|
|
4046
4119
|
|
|
4047
4120
|
// src/engine/lspClient.ts
|
|
4048
4121
|
import { spawn as spawn2 } from "child_process";
|
|
4049
|
-
import
|
|
4050
|
-
import
|
|
4122
|
+
import path14 from "path";
|
|
4123
|
+
import fs14 from "fs";
|
|
4051
4124
|
var LSPClient = class {
|
|
4052
4125
|
process = null;
|
|
4053
4126
|
requestId = 0;
|
|
@@ -4085,8 +4158,8 @@ var LSPClient = class {
|
|
|
4085
4158
|
console.error(`[LSP] typescript-language-server not found. LSP features will fallback to regex.`);
|
|
4086
4159
|
return;
|
|
4087
4160
|
}
|
|
4088
|
-
const tsconfigPath =
|
|
4089
|
-
if (!
|
|
4161
|
+
const tsconfigPath = path14.join(root, "tsconfig.json");
|
|
4162
|
+
if (!fs14.existsSync(tsconfigPath)) {
|
|
4090
4163
|
console.error(`[LSP] Warning: tsconfig.json not found at "${root}". Running in implicit project mode.`);
|
|
4091
4164
|
}
|
|
4092
4165
|
this.process = spawn2(lspBinary, ["--stdio", "--log-level=4"], {
|
|
@@ -4148,19 +4221,19 @@ var LSPClient = class {
|
|
|
4148
4221
|
/** Resolve the typescript-language-server binary from local or global install */
|
|
4149
4222
|
resolveLspBinary(projectRoot) {
|
|
4150
4223
|
const candidates = [
|
|
4151
|
-
|
|
4152
|
-
|
|
4224
|
+
path14.join(projectRoot, "node_modules", ".bin", "typescript-language-server"),
|
|
4225
|
+
path14.join(projectRoot, "..", "node_modules", ".bin", "typescript-language-server")
|
|
4153
4226
|
];
|
|
4154
4227
|
for (const candidate of candidates) {
|
|
4155
|
-
if (
|
|
4228
|
+
if (fs14.existsSync(candidate)) {
|
|
4156
4229
|
return candidate;
|
|
4157
4230
|
}
|
|
4158
4231
|
}
|
|
4159
4232
|
try {
|
|
4160
4233
|
const envPath = process.env.PATH ?? "";
|
|
4161
|
-
for (const dir of envPath.split(
|
|
4162
|
-
const binPath =
|
|
4163
|
-
if (
|
|
4234
|
+
for (const dir of envPath.split(path14.delimiter)) {
|
|
4235
|
+
const binPath = path14.join(dir, "typescript-language-server");
|
|
4236
|
+
if (fs14.existsSync(binPath)) {
|
|
4164
4237
|
return binPath;
|
|
4165
4238
|
}
|
|
4166
4239
|
}
|
|
@@ -4181,7 +4254,7 @@ var LSPClient = class {
|
|
|
4181
4254
|
this.openDocuments.add(filePath);
|
|
4182
4255
|
let content;
|
|
4183
4256
|
try {
|
|
4184
|
-
content =
|
|
4257
|
+
content = fs14.readFileSync(filePath, "utf-8");
|
|
4185
4258
|
} catch {
|
|
4186
4259
|
content = "";
|
|
4187
4260
|
}
|
|
@@ -4398,8 +4471,8 @@ var LSPClient = class {
|
|
|
4398
4471
|
var lspClient = new LSPClient();
|
|
4399
4472
|
|
|
4400
4473
|
// src/tools/lspTools.ts
|
|
4401
|
-
import
|
|
4402
|
-
import
|
|
4474
|
+
import fs15 from "fs";
|
|
4475
|
+
import path15 from "path";
|
|
4403
4476
|
async function handleFindReferences(params) {
|
|
4404
4477
|
const { filePath, line, character } = params;
|
|
4405
4478
|
const validation = validateFilePath(filePath);
|
|
@@ -4407,7 +4480,7 @@ async function handleFindReferences(params) {
|
|
|
4407
4480
|
return `Error: ${validation.error.message}`;
|
|
4408
4481
|
}
|
|
4409
4482
|
const resolvedPath = validation.resolvedPath;
|
|
4410
|
-
if (!
|
|
4483
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
4411
4484
|
return `Error: File not found: "${filePath}".`;
|
|
4412
4485
|
}
|
|
4413
4486
|
if (!lspClient.isAvailable()) {
|
|
@@ -4429,7 +4502,7 @@ async function handleFindReferences(params) {
|
|
|
4429
4502
|
const enrichedRefs = references.map((ref) => {
|
|
4430
4503
|
let lineContent = "";
|
|
4431
4504
|
try {
|
|
4432
|
-
const content =
|
|
4505
|
+
const content = fs15.readFileSync(ref.filePath, "utf-8");
|
|
4433
4506
|
const lines2 = content.split("\n");
|
|
4434
4507
|
lineContent = lines2[ref.line]?.trim() ?? "";
|
|
4435
4508
|
} catch {
|
|
@@ -4445,11 +4518,11 @@ async function handleFindReferences(params) {
|
|
|
4445
4518
|
const projectRoot = getProjectRoot();
|
|
4446
4519
|
const lines = [
|
|
4447
4520
|
`\u{1F50D} **Find References** \u2014 ${enrichedRefs.length} references found`,
|
|
4448
|
-
`\u{1F4CD} File: ${
|
|
4521
|
+
`\u{1F4CD} File: ${path15.relative(projectRoot, resolvedPath)}:${line + 1}:${character + 1}`,
|
|
4449
4522
|
""
|
|
4450
4523
|
];
|
|
4451
4524
|
for (const [file, refs] of grouped) {
|
|
4452
|
-
const relPath =
|
|
4525
|
+
const relPath = path15.relative(projectRoot, file);
|
|
4453
4526
|
lines.push(`**\u{1F4C4} ${relPath}:**`);
|
|
4454
4527
|
for (const ref of refs) {
|
|
4455
4528
|
const loc = `L${ref.line + 1}:${ref.character + 1}`;
|
|
@@ -4470,7 +4543,7 @@ async function handleGoToDefinition(params) {
|
|
|
4470
4543
|
return `Error: ${validation.error.message}`;
|
|
4471
4544
|
}
|
|
4472
4545
|
const resolvedPath = validation.resolvedPath;
|
|
4473
|
-
if (!
|
|
4546
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
4474
4547
|
return `Error: File not found: "${filePath}".`;
|
|
4475
4548
|
}
|
|
4476
4549
|
if (!lspClient.isAvailable()) {
|
|
@@ -4490,10 +4563,10 @@ async function handleGoToDefinition(params) {
|
|
|
4490
4563
|
\u26A0\uFE0F Cannot find definition for symbol at this position.`;
|
|
4491
4564
|
}
|
|
4492
4565
|
const projectRoot = getProjectRoot();
|
|
4493
|
-
const relPath =
|
|
4566
|
+
const relPath = path15.relative(projectRoot, definition.filePath);
|
|
4494
4567
|
let lineContent = "";
|
|
4495
4568
|
try {
|
|
4496
|
-
const content =
|
|
4569
|
+
const content = fs15.readFileSync(definition.filePath, "utf-8");
|
|
4497
4570
|
const lines2 = content.split("\n");
|
|
4498
4571
|
lineContent = lines2[definition.line]?.trim() ?? "";
|
|
4499
4572
|
} catch {
|
|
@@ -4525,7 +4598,7 @@ async function handleRenameSymbol(params) {
|
|
|
4525
4598
|
return `Error: ${validation.error.message}`;
|
|
4526
4599
|
}
|
|
4527
4600
|
const resolvedPath = validation.resolvedPath;
|
|
4528
|
-
if (!
|
|
4601
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
4529
4602
|
return `Error: File not found: "${filePath}".`;
|
|
4530
4603
|
}
|
|
4531
4604
|
if (!lspClient.isAvailable()) {
|
|
@@ -4554,7 +4627,7 @@ Make sure:
|
|
|
4554
4627
|
const fileChanges = [];
|
|
4555
4628
|
for (const change of result.changes) {
|
|
4556
4629
|
try {
|
|
4557
|
-
const content =
|
|
4630
|
+
const content = fs15.readFileSync(change.filePath, "utf-8");
|
|
4558
4631
|
const lines2 = content.split("\n");
|
|
4559
4632
|
const sortedEdits = [...change.edits].sort((a, b) => {
|
|
4560
4633
|
if (b.line !== a.line) return b.line - a.line;
|
|
@@ -4568,10 +4641,10 @@ Make sure:
|
|
|
4568
4641
|
lines2[edit.line] = before + edit.newText + after;
|
|
4569
4642
|
}
|
|
4570
4643
|
}
|
|
4571
|
-
|
|
4644
|
+
fs15.writeFileSync(change.filePath, lines2.join("\n"), "utf-8");
|
|
4572
4645
|
totalEdits += change.edits.length;
|
|
4573
4646
|
fileChanges.push({
|
|
4574
|
-
filePath:
|
|
4647
|
+
filePath: path15.relative(projectRoot, change.filePath),
|
|
4575
4648
|
editCount: change.edits.length
|
|
4576
4649
|
});
|
|
4577
4650
|
} catch (err) {
|
|
@@ -4598,13 +4671,13 @@ async function handleGetTypeInfo(params) {
|
|
|
4598
4671
|
return `Error: ${validation.error.message}`;
|
|
4599
4672
|
}
|
|
4600
4673
|
const resolvedPath = validation.resolvedPath;
|
|
4601
|
-
if (!
|
|
4674
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
4602
4675
|
return `Error: File not found: "${filePath}".`;
|
|
4603
4676
|
}
|
|
4604
4677
|
if (!lspClient.isAvailable()) {
|
|
4605
4678
|
sessionMemory.recordToolCall("lsp_query", { action: "type", filePath, line, character, fallback: "line-context" });
|
|
4606
4679
|
try {
|
|
4607
|
-
const fileContent =
|
|
4680
|
+
const fileContent = fs15.readFileSync(resolvedPath, "utf-8");
|
|
4608
4681
|
const fileLines = fileContent.split("\n");
|
|
4609
4682
|
const targetLine = fileLines[line];
|
|
4610
4683
|
if (!targetLine) {
|
|
@@ -4612,7 +4685,7 @@ async function handleGetTypeInfo(params) {
|
|
|
4612
4685
|
}
|
|
4613
4686
|
const symbolName = extractSymbolAtPosition(resolvedPath, line, character);
|
|
4614
4687
|
const projectRoot = getProjectRoot();
|
|
4615
|
-
const relPath =
|
|
4688
|
+
const relPath = path15.relative(projectRoot, resolvedPath);
|
|
4616
4689
|
const contextStart = Math.max(0, line - 3);
|
|
4617
4690
|
const contextEnd = Math.min(fileLines.length, line + 4);
|
|
4618
4691
|
const contextLines = [];
|
|
@@ -4650,7 +4723,7 @@ async function handleGetTypeInfo(params) {
|
|
|
4650
4723
|
\u26A0\uFE0F No type info for this position.`;
|
|
4651
4724
|
}
|
|
4652
4725
|
const projectRoot = getProjectRoot();
|
|
4653
|
-
const relPath =
|
|
4726
|
+
const relPath = path15.relative(projectRoot, resolvedPath);
|
|
4654
4727
|
const lines = [
|
|
4655
4728
|
`\u{1F4CB} **Type Info** \u2014 \`${relPath}:${line + 1}:${character + 1}\``,
|
|
4656
4729
|
"",
|
|
@@ -4691,7 +4764,7 @@ async function handleLspQuery(params) {
|
|
|
4691
4764
|
}
|
|
4692
4765
|
function extractSymbolAtPosition(filePath, line, character) {
|
|
4693
4766
|
try {
|
|
4694
|
-
const content =
|
|
4767
|
+
const content = fs15.readFileSync(filePath, "utf-8");
|
|
4695
4768
|
const lines = content.split("\n");
|
|
4696
4769
|
const targetLine = lines[line];
|
|
4697
4770
|
if (!targetLine) return null;
|
|
@@ -4722,7 +4795,7 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4722
4795
|
const regex = new RegExp(`\\b${escapedSymbol}\\b`, "g");
|
|
4723
4796
|
for (const file of tsFiles.slice(0, 100)) {
|
|
4724
4797
|
try {
|
|
4725
|
-
const content =
|
|
4798
|
+
const content = fs15.readFileSync(file, "utf-8");
|
|
4726
4799
|
const lines2 = content.split("\n");
|
|
4727
4800
|
for (let i = 0; i < lines2.length; i++) {
|
|
4728
4801
|
if (regex.test(lines2[i])) {
|
|
@@ -4751,8 +4824,28 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4751
4824
|
`\u26A0\uFE0F Regex results may be less accurate than LSP (includes comments/strings).`,
|
|
4752
4825
|
""
|
|
4753
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
|
+
}
|
|
4754
4847
|
for (const [file, refs] of grouped) {
|
|
4755
|
-
const relPath =
|
|
4848
|
+
const relPath = path15.relative(projectRoot, file);
|
|
4756
4849
|
lines.push(`**\u{1F4C4} ${relPath}:**`);
|
|
4757
4850
|
for (const ref of refs) {
|
|
4758
4851
|
lines.push(` \u2514 L${ref.line} \u2014 ${ref.content}`);
|
|
@@ -4786,14 +4879,14 @@ async function fallbackGrepDefinition(symbolName) {
|
|
|
4786
4879
|
];
|
|
4787
4880
|
for (const file of tsFiles) {
|
|
4788
4881
|
try {
|
|
4789
|
-
const content =
|
|
4882
|
+
const content = fs15.readFileSync(file, "utf-8");
|
|
4790
4883
|
const lines = content.split("\n");
|
|
4791
4884
|
for (let i = 0; i < lines.length; i++) {
|
|
4792
4885
|
const trimmed = lines[i].trim();
|
|
4793
4886
|
for (const pattern of declPatterns) {
|
|
4794
4887
|
if (pattern.test(trimmed)) {
|
|
4795
4888
|
const projectRoot = getProjectRoot();
|
|
4796
|
-
const relPath =
|
|
4889
|
+
const relPath = path15.relative(projectRoot, file);
|
|
4797
4890
|
return [
|
|
4798
4891
|
`\u{1F4CD} **Go to Definition** (regex fallback)`,
|
|
4799
4892
|
`\u{1F4C4} File: \`${relPath}\``,
|
|
@@ -4906,11 +4999,13 @@ function registerAllTools(server) {
|
|
|
4906
4999
|
);
|
|
4907
5000
|
server.tool(
|
|
4908
5001
|
"execute_safe_test",
|
|
4909
|
-
"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.",
|
|
4910
5003
|
{
|
|
4911
5004
|
task: z.enum(["test", "build", "lint", "typecheck", "custom"]).describe("Task to execute: test, build, lint, typecheck, or custom"),
|
|
4912
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')"),
|
|
4913
|
-
timeout: z.number({ invalid_type_error: '"timeout" must be a number in seconds (5-180)' }).min(5).max(180).optional().default(60).describe("Timeout in seconds (default: 60s, max: 180s)")
|
|
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.")
|
|
4914
5009
|
},
|
|
4915
5010
|
async (params) => {
|
|
4916
5011
|
try {
|
|
@@ -5143,8 +5238,8 @@ function registerAllTools(server) {
|
|
|
5143
5238
|
}
|
|
5144
5239
|
|
|
5145
5240
|
// src/cli/init.ts
|
|
5146
|
-
import
|
|
5147
|
-
import
|
|
5241
|
+
import fs16 from "fs";
|
|
5242
|
+
import path16 from "path";
|
|
5148
5243
|
var ALL_CONFIG_TYPES = [
|
|
5149
5244
|
"claude",
|
|
5150
5245
|
"cursor",
|
|
@@ -5536,10 +5631,10 @@ var TEMPLATES = {
|
|
|
5536
5631
|
};
|
|
5537
5632
|
var APPEND_SEPARATOR = "\n\n---\n_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_\n\n";
|
|
5538
5633
|
function handleOpencodeSecondary(root, results) {
|
|
5539
|
-
const claudePath =
|
|
5540
|
-
if (!
|
|
5634
|
+
const claudePath = path16.resolve(root, "CLAUDE.md");
|
|
5635
|
+
if (!fs16.existsSync(claudePath)) {
|
|
5541
5636
|
try {
|
|
5542
|
-
|
|
5637
|
+
fs16.writeFileSync(claudePath, [
|
|
5543
5638
|
"# Kuma MCP - OpenCode Instructions",
|
|
5544
5639
|
"",
|
|
5545
5640
|
KUMA_CORE_INSTRUCTIONS
|
|
@@ -5556,21 +5651,21 @@ function handleOpencodeSecondary(root, results) {
|
|
|
5556
5651
|
}
|
|
5557
5652
|
}
|
|
5558
5653
|
function handleCodexSecondary(root, results) {
|
|
5559
|
-
const tomlPath =
|
|
5654
|
+
const tomlPath = path16.resolve(root, ".codex/config.toml");
|
|
5560
5655
|
if (results.some((r) => r.filePath === ".codex/config.toml")) return;
|
|
5561
5656
|
try {
|
|
5562
|
-
const dir =
|
|
5563
|
-
if (!
|
|
5564
|
-
if (
|
|
5565
|
-
const existingContent =
|
|
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");
|
|
5566
5661
|
if (existingContent.includes("kuma")) {
|
|
5567
5662
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
|
|
5568
5663
|
return;
|
|
5569
5664
|
}
|
|
5570
|
-
|
|
5665
|
+
fs16.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
|
|
5571
5666
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
|
|
5572
5667
|
} else {
|
|
5573
|
-
|
|
5668
|
+
fs16.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
|
|
5574
5669
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
|
|
5575
5670
|
}
|
|
5576
5671
|
} catch (err) {
|
|
@@ -5583,11 +5678,11 @@ function handleCodexSecondary(root, results) {
|
|
|
5583
5678
|
}
|
|
5584
5679
|
}
|
|
5585
5680
|
function handleQwenSecondary(root, results) {
|
|
5586
|
-
const settingsPath =
|
|
5681
|
+
const settingsPath = path16.resolve(root, "settings.json");
|
|
5587
5682
|
if (results.some((r) => r.filePath === "settings.json")) return;
|
|
5588
5683
|
try {
|
|
5589
|
-
if (
|
|
5590
|
-
const existingContent =
|
|
5684
|
+
if (fs16.existsSync(settingsPath)) {
|
|
5685
|
+
const existingContent = fs16.readFileSync(settingsPath, "utf-8");
|
|
5591
5686
|
if (existingContent.includes("kuma")) {
|
|
5592
5687
|
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5593
5688
|
try {
|
|
@@ -5595,7 +5690,7 @@ function handleQwenSecondary(root, results) {
|
|
|
5595
5690
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
5596
5691
|
if (!parsed.mcpServers.kuma) {
|
|
5597
5692
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5598
|
-
|
|
5693
|
+
fs16.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5599
5694
|
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
5600
5695
|
return;
|
|
5601
5696
|
}
|
|
@@ -5610,13 +5705,13 @@ function handleQwenSecondary(root, results) {
|
|
|
5610
5705
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
5611
5706
|
if (!parsed.mcpServers.kuma) {
|
|
5612
5707
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5613
|
-
|
|
5708
|
+
fs16.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5614
5709
|
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
5615
5710
|
}
|
|
5616
5711
|
} catch {
|
|
5617
5712
|
}
|
|
5618
5713
|
} else {
|
|
5619
|
-
|
|
5714
|
+
fs16.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
|
|
5620
5715
|
results.push({ type: "qwen", filePath: "settings.json", action: "created" });
|
|
5621
5716
|
}
|
|
5622
5717
|
} catch (err) {
|
|
@@ -5652,18 +5747,18 @@ function getCombinedAgentsMd(selectedTypes) {
|
|
|
5652
5747
|
return sections.join("\n\n---\n\n");
|
|
5653
5748
|
}
|
|
5654
5749
|
function handleAntigravityMcpConfig(root, results) {
|
|
5655
|
-
const mcpPath =
|
|
5750
|
+
const mcpPath = path16.resolve(root, ".agents/mcp_config.json");
|
|
5656
5751
|
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
5657
5752
|
try {
|
|
5658
|
-
const mcpDir =
|
|
5659
|
-
if (
|
|
5660
|
-
const existingContent =
|
|
5753
|
+
const mcpDir = path16.dirname(mcpPath);
|
|
5754
|
+
if (fs16.existsSync(mcpPath)) {
|
|
5755
|
+
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5661
5756
|
if (existingContent.includes("kuma")) {
|
|
5662
5757
|
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5663
5758
|
const trimmed = existingContent.trimEnd();
|
|
5664
5759
|
if (trimmed.endsWith("}")) {
|
|
5665
5760
|
const updated = trimmed.slice(0, -1).trimEnd() + ',\n "_kuma_note": "Kuma MCP - Generated by kuma init"\n}\n';
|
|
5666
|
-
|
|
5761
|
+
fs16.writeFileSync(mcpPath, updated, "utf-8");
|
|
5667
5762
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5668
5763
|
}
|
|
5669
5764
|
}
|
|
@@ -5672,11 +5767,11 @@ function handleAntigravityMcpConfig(root, results) {
|
|
|
5672
5767
|
const parsed = JSON.parse(existingContent);
|
|
5673
5768
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
5674
5769
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5675
|
-
|
|
5770
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5676
5771
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5677
5772
|
} else {
|
|
5678
|
-
if (!
|
|
5679
|
-
|
|
5773
|
+
if (!fs16.existsSync(mcpDir)) fs16.mkdirSync(mcpDir, { recursive: true });
|
|
5774
|
+
fs16.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
5680
5775
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
|
|
5681
5776
|
}
|
|
5682
5777
|
} catch (err) {
|
|
@@ -5689,18 +5784,18 @@ function handleAntigravityMcpConfig(root, results) {
|
|
|
5689
5784
|
}
|
|
5690
5785
|
}
|
|
5691
5786
|
function handleAiderSecondary(root, results) {
|
|
5692
|
-
const ymlPath =
|
|
5787
|
+
const ymlPath = path16.resolve(root, ".aider.conf.yml");
|
|
5693
5788
|
if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
|
|
5694
5789
|
try {
|
|
5695
5790
|
const conventionsRef = "read: CONVENTIONS.md";
|
|
5696
|
-
if (
|
|
5697
|
-
const existingContent =
|
|
5791
|
+
if (fs16.existsSync(ymlPath)) {
|
|
5792
|
+
const existingContent = fs16.readFileSync(ymlPath, "utf-8");
|
|
5698
5793
|
if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
|
|
5699
5794
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
|
|
5700
5795
|
return;
|
|
5701
5796
|
}
|
|
5702
5797
|
const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
|
|
5703
|
-
|
|
5798
|
+
fs16.writeFileSync(ymlPath, newContent, "utf-8");
|
|
5704
5799
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
|
|
5705
5800
|
} else {
|
|
5706
5801
|
const content = [
|
|
@@ -5710,7 +5805,7 @@ function handleAiderSecondary(root, results) {
|
|
|
5710
5805
|
conventionsRef,
|
|
5711
5806
|
""
|
|
5712
5807
|
].join("\n");
|
|
5713
|
-
|
|
5808
|
+
fs16.writeFileSync(ymlPath, content, "utf-8");
|
|
5714
5809
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
|
|
5715
5810
|
}
|
|
5716
5811
|
} catch (err) {
|
|
@@ -5723,10 +5818,10 @@ function handleAiderSecondary(root, results) {
|
|
|
5723
5818
|
}
|
|
5724
5819
|
}
|
|
5725
5820
|
function handleCopilotSecondary(root, results) {
|
|
5726
|
-
const skillPath =
|
|
5821
|
+
const skillPath = path16.resolve(root, ".github/skills/kuma/SKILL.md");
|
|
5727
5822
|
if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
|
|
5728
5823
|
try {
|
|
5729
|
-
const dir =
|
|
5824
|
+
const dir = path16.dirname(skillPath);
|
|
5730
5825
|
const content = [
|
|
5731
5826
|
"---",
|
|
5732
5827
|
"name: kuma-mcp",
|
|
@@ -5737,17 +5832,17 @@ function handleCopilotSecondary(root, results) {
|
|
|
5737
5832
|
"",
|
|
5738
5833
|
KUMA_CORE_INSTRUCTIONS
|
|
5739
5834
|
].join("\n");
|
|
5740
|
-
if (
|
|
5741
|
-
const existingContent =
|
|
5835
|
+
if (fs16.existsSync(skillPath)) {
|
|
5836
|
+
const existingContent = fs16.readFileSync(skillPath, "utf-8");
|
|
5742
5837
|
if (existingContent.includes("kuma")) {
|
|
5743
5838
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
|
|
5744
5839
|
return;
|
|
5745
5840
|
}
|
|
5746
|
-
|
|
5841
|
+
fs16.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
|
|
5747
5842
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
|
|
5748
5843
|
} else {
|
|
5749
|
-
if (!
|
|
5750
|
-
|
|
5844
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5845
|
+
fs16.writeFileSync(skillPath, content, "utf-8");
|
|
5751
5846
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
|
|
5752
5847
|
}
|
|
5753
5848
|
} catch (err) {
|
|
@@ -5760,21 +5855,21 @@ function handleCopilotSecondary(root, results) {
|
|
|
5760
5855
|
}
|
|
5761
5856
|
}
|
|
5762
5857
|
function handleOpenclawSecondary(root, results) {
|
|
5763
|
-
const mcpPath =
|
|
5858
|
+
const mcpPath = path16.resolve(root, ".agents/mcp_config.json");
|
|
5764
5859
|
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
5765
5860
|
try {
|
|
5766
|
-
const dir =
|
|
5767
|
-
if (
|
|
5768
|
-
const existingContent =
|
|
5861
|
+
const dir = path16.dirname(mcpPath);
|
|
5862
|
+
if (fs16.existsSync(mcpPath)) {
|
|
5863
|
+
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5769
5864
|
if (existingContent.includes("kuma")) return;
|
|
5770
5865
|
const parsed = JSON.parse(existingContent);
|
|
5771
5866
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
5772
5867
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5773
|
-
|
|
5868
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5774
5869
|
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5775
5870
|
} else {
|
|
5776
|
-
if (!
|
|
5777
|
-
|
|
5871
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5872
|
+
fs16.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
5778
5873
|
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
|
|
5779
5874
|
}
|
|
5780
5875
|
} catch (err) {
|
|
@@ -5787,20 +5882,20 @@ function handleOpenclawSecondary(root, results) {
|
|
|
5787
5882
|
}
|
|
5788
5883
|
}
|
|
5789
5884
|
function handleCodewhaleSecondary(root, results) {
|
|
5790
|
-
const mcpPath =
|
|
5885
|
+
const mcpPath = path16.resolve(root, ".codewhale/mcp.json");
|
|
5791
5886
|
if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
|
|
5792
5887
|
try {
|
|
5793
|
-
const dir =
|
|
5794
|
-
if (
|
|
5795
|
-
const existingContent =
|
|
5888
|
+
const dir = path16.dirname(mcpPath);
|
|
5889
|
+
if (fs16.existsSync(mcpPath)) {
|
|
5890
|
+
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5796
5891
|
if (existingContent.includes("kuma")) return;
|
|
5797
5892
|
const parsed = JSON.parse(existingContent);
|
|
5798
5893
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
5799
5894
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5800
|
-
|
|
5895
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5801
5896
|
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
|
|
5802
5897
|
} else {
|
|
5803
|
-
if (!
|
|
5898
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5804
5899
|
const config = {
|
|
5805
5900
|
mcpServers: {
|
|
5806
5901
|
kuma: {
|
|
@@ -5810,7 +5905,7 @@ function handleCodewhaleSecondary(root, results) {
|
|
|
5810
5905
|
}
|
|
5811
5906
|
}
|
|
5812
5907
|
};
|
|
5813
|
-
|
|
5908
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5814
5909
|
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
|
|
5815
5910
|
}
|
|
5816
5911
|
} catch (err) {
|
|
@@ -5831,24 +5926,24 @@ function runInit(types, projectRoot) {
|
|
|
5831
5926
|
let agentsMdHandled = false;
|
|
5832
5927
|
for (const type of selected) {
|
|
5833
5928
|
const relativePath = configFilePath(type);
|
|
5834
|
-
const fullPath =
|
|
5929
|
+
const fullPath = path16.resolve(root, relativePath);
|
|
5835
5930
|
const getTemplate = TEMPLATES[type];
|
|
5836
5931
|
try {
|
|
5837
5932
|
if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
|
|
5838
5933
|
agentsMdHandled = true;
|
|
5839
5934
|
const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
|
|
5840
|
-
if (
|
|
5841
|
-
const existingContent =
|
|
5935
|
+
if (fs16.existsSync(fullPath)) {
|
|
5936
|
+
const existingContent = fs16.readFileSync(fullPath, "utf-8");
|
|
5842
5937
|
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5843
5938
|
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
5844
5939
|
} else {
|
|
5845
|
-
|
|
5940
|
+
fs16.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
|
|
5846
5941
|
results.push({ type, filePath: relativePath, action: "appended" });
|
|
5847
5942
|
}
|
|
5848
5943
|
} else {
|
|
5849
|
-
const dir =
|
|
5850
|
-
if (!
|
|
5851
|
-
|
|
5944
|
+
const dir = path16.dirname(fullPath);
|
|
5945
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5946
|
+
fs16.writeFileSync(fullPath, combinedContent, "utf-8");
|
|
5852
5947
|
results.push({ type, filePath: relativePath, action: "created" });
|
|
5853
5948
|
}
|
|
5854
5949
|
if (selectedSet.has("codex")) handleCodexSecondary(root, results);
|
|
@@ -5859,8 +5954,8 @@ function runInit(types, projectRoot) {
|
|
|
5859
5954
|
continue;
|
|
5860
5955
|
} else {
|
|
5861
5956
|
const template = getTemplate();
|
|
5862
|
-
if (
|
|
5863
|
-
const existingContent =
|
|
5957
|
+
if (fs16.existsSync(fullPath)) {
|
|
5958
|
+
const existingContent = fs16.readFileSync(fullPath, "utf-8");
|
|
5864
5959
|
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5865
5960
|
if (type === "antigravity") {
|
|
5866
5961
|
handleAntigravityMcpConfig(root, results);
|
|
@@ -5873,14 +5968,14 @@ function runInit(types, projectRoot) {
|
|
|
5873
5968
|
continue;
|
|
5874
5969
|
}
|
|
5875
5970
|
const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
|
|
5876
|
-
|
|
5971
|
+
fs16.writeFileSync(fullPath, newContent, "utf-8");
|
|
5877
5972
|
results.push({ type, filePath: relativePath, action: "appended" });
|
|
5878
5973
|
} else {
|
|
5879
|
-
const dir =
|
|
5880
|
-
if (!
|
|
5881
|
-
|
|
5974
|
+
const dir = path16.dirname(fullPath);
|
|
5975
|
+
if (!fs16.existsSync(dir)) {
|
|
5976
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
5882
5977
|
}
|
|
5883
|
-
|
|
5978
|
+
fs16.writeFileSync(fullPath, template, "utf-8");
|
|
5884
5979
|
results.push({ type, filePath: relativePath, action: "created" });
|
|
5885
5980
|
}
|
|
5886
5981
|
if (type === "antigravity") {
|