@plumpslabs/kuma 2.1.0 → 2.1.2
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 +884 -733
- 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,159 +1613,624 @@ 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 (
|
|
1747
|
-
|
|
1752
|
+
if (Array.isArray(pnpmWorkspace?.packages)) {
|
|
1753
|
+
for (const p of pnpmWorkspace.packages) if (typeof p === "string") patterns.add(p);
|
|
1748
1754
|
}
|
|
1749
|
-
|
|
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 stripShellObfuscation(cmd) {
|
|
2061
|
+
let result = cmd;
|
|
2062
|
+
let prev;
|
|
2063
|
+
do {
|
|
2064
|
+
prev = result;
|
|
2065
|
+
result = result.replace(/\$\([^()]*\)/g, "");
|
|
2066
|
+
} while (result !== prev);
|
|
2067
|
+
do {
|
|
2068
|
+
prev = result;
|
|
2069
|
+
result = result.replace(/\$\(\([^()]*\)\)/g, "");
|
|
2070
|
+
} while (result !== prev);
|
|
2071
|
+
result = result.replace(/\$\{[^}]*\}/g, " ");
|
|
2072
|
+
result = result.replace(/\$'[^']*'/g, "");
|
|
2073
|
+
result = result.replace(/\$"[^"]*"/g, "");
|
|
2074
|
+
result = result.replace(/\$[a-zA-Z_][a-zA-Z0-9_]*/g, " ");
|
|
2075
|
+
result = result.replace(/\s+/g, " ").trim();
|
|
2076
|
+
return result;
|
|
2077
|
+
}
|
|
2078
|
+
function buildTaskCommand(task, cwd) {
|
|
2079
|
+
const pm = detectPackageManagerForDir(cwd);
|
|
2080
|
+
const prefix = pm === "pnpm" ? "pnpm" : pm === "yarn" ? "yarn" : pm === "bun" ? "bun" : "npm";
|
|
2081
|
+
const npx = pm === "pnpm" ? "pnpm" : pm === "bun" ? "bunx" : "npx";
|
|
2082
|
+
switch (task) {
|
|
2083
|
+
case "test":
|
|
2084
|
+
return `${prefix} test`;
|
|
2085
|
+
case "build":
|
|
2086
|
+
return `${prefix} run build`;
|
|
2087
|
+
case "lint":
|
|
2088
|
+
return `${prefix} run lint`;
|
|
2089
|
+
case "typecheck":
|
|
2090
|
+
return `${npx} tsc --noEmit`;
|
|
2091
|
+
default:
|
|
2092
|
+
return `${prefix} test`;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
var DANGEROUS_PATTERNS = [
|
|
2096
|
+
// RM variants
|
|
2097
|
+
"rm -rf",
|
|
2098
|
+
"rm -fr",
|
|
2099
|
+
"rm --recursive",
|
|
2100
|
+
"rm --force",
|
|
2101
|
+
"del /f",
|
|
2102
|
+
"rd /s",
|
|
2103
|
+
"rmdir /s",
|
|
2104
|
+
// Git dangerous
|
|
2105
|
+
"git push",
|
|
2106
|
+
"git commit",
|
|
2107
|
+
"git reset --hard",
|
|
2108
|
+
"git clean -fd",
|
|
2109
|
+
"git checkout -- .",
|
|
2110
|
+
// Publish to registries
|
|
2111
|
+
"npm publish",
|
|
2112
|
+
"npx publish",
|
|
2113
|
+
"yarn publish",
|
|
2114
|
+
"pnpm publish",
|
|
2115
|
+
// System destruction
|
|
2116
|
+
"> /dev/sda",
|
|
2117
|
+
"mkfs",
|
|
2118
|
+
"dd if=",
|
|
2119
|
+
"dd of=",
|
|
2120
|
+
"fdisk",
|
|
2121
|
+
"parted",
|
|
2122
|
+
"mkswap",
|
|
2123
|
+
// Fork bomb
|
|
2124
|
+
":(){ :|:& };:",
|
|
2125
|
+
":(){",
|
|
2126
|
+
// Pipe to shell (remote code execution vector)
|
|
2127
|
+
"| bash",
|
|
2128
|
+
"| sh",
|
|
2129
|
+
// Dangerous eval/exec
|
|
2130
|
+
"eval ",
|
|
2131
|
+
"exec ",
|
|
2132
|
+
"source /dev/stdin",
|
|
2133
|
+
". /dev/stdin",
|
|
2134
|
+
// Find destruction
|
|
2135
|
+
"find . -delete",
|
|
2136
|
+
"find / -delete",
|
|
2137
|
+
"find . -exec rm",
|
|
2138
|
+
"find / -exec rm",
|
|
2139
|
+
// Chmod abuse
|
|
2140
|
+
"chmod -R 777",
|
|
2141
|
+
"chmod 777 /",
|
|
2142
|
+
"chmod 000",
|
|
2143
|
+
// Alias hijacking
|
|
2144
|
+
"alias rm=",
|
|
2145
|
+
"alias cd=",
|
|
2146
|
+
"alias sudo=",
|
|
2147
|
+
// Overwrite protection
|
|
2148
|
+
"shred"
|
|
2149
|
+
];
|
|
2150
|
+
async function handleSafeTerminalExec(params) {
|
|
2151
|
+
const { task, customCommand, timeout = 60, cwd: inputCwd, workspace } = params;
|
|
2152
|
+
if (task === "custom" && !customCommand) {
|
|
2153
|
+
return "Error: Task 'custom' requires the 'customCommand' parameter.";
|
|
2154
|
+
}
|
|
2155
|
+
const projectRoot = getProjectRoot();
|
|
2156
|
+
let workingDir = projectRoot;
|
|
2157
|
+
let resolvedFrom = "root";
|
|
2158
|
+
if (workspace) {
|
|
2159
|
+
const conventions = sessionMemory.getConventions();
|
|
2160
|
+
const workspaces = conventions?.workspaces;
|
|
2161
|
+
const matched = workspaces?.find((w) => w.name === workspace || w.path === workspace);
|
|
2162
|
+
if (matched) {
|
|
2163
|
+
workingDir = path8.resolve(projectRoot, matched.path);
|
|
2164
|
+
resolvedFrom = `workspace "${matched.name}" \u2192 ${matched.path}`;
|
|
2165
|
+
} else {
|
|
2166
|
+
return `\u26A0\uFE0F Workspace "${workspace}" not found. Run project_conventions first to detect workspaces, or use 'cwd' parameter with a direct path.
|
|
2167
|
+
|
|
2168
|
+
Available workspaces: ${workspaces?.map((w) => `"${w.name}" (${w.path})`).join(", ") || "none detected"}`;
|
|
2169
|
+
}
|
|
2170
|
+
} else if (inputCwd) {
|
|
2171
|
+
const resolved = path8.resolve(projectRoot, inputCwd);
|
|
2172
|
+
const normalizedResolved = path8.normalize(resolved).toLowerCase();
|
|
2173
|
+
const normalizedRoot = path8.normalize(projectRoot).toLowerCase();
|
|
2174
|
+
if (!normalizedResolved.startsWith(normalizedRoot)) {
|
|
2175
|
+
return `\u{1F6AB} BLOCKED: Path "${inputCwd}" resolves outside project root "${projectRoot}".`;
|
|
2176
|
+
}
|
|
2177
|
+
if (!fs8.existsSync(resolved)) {
|
|
2178
|
+
return `\u26A0\uFE0F Directory "${inputCwd}" does not exist at "${resolved}".`;
|
|
2179
|
+
}
|
|
2180
|
+
workingDir = resolved;
|
|
2181
|
+
resolvedFrom = `cwd: ${inputCwd}`;
|
|
2182
|
+
}
|
|
2183
|
+
const command = task === "custom" ? customCommand : buildTaskCommand(task, workingDir);
|
|
2184
|
+
const cbResult = circuitBreaker.check("safe_terminal_exec", { task, command });
|
|
2185
|
+
if (!cbResult.allowed) {
|
|
2186
|
+
return `\u26A0\uFE0F Circuit breaker: ${cbResult.reason}
|
|
2187
|
+
|
|
2188
|
+
Fix the code first before running the task again.`;
|
|
2189
|
+
}
|
|
2190
|
+
const deobfuscated = stripShellObfuscation(command);
|
|
2191
|
+
const dangerousPattern = DANGEROUS_PATTERNS.find(
|
|
2192
|
+
(p) => deobfuscated.toLowerCase().includes(p.toLowerCase())
|
|
2193
|
+
);
|
|
2194
|
+
if (dangerousPattern) {
|
|
2195
|
+
return `\u{1F6AB} BLOCKED: Command contains a dangerous pattern: "${dangerousPattern}".
|
|
2196
|
+
This command is not permitted.`;
|
|
2197
|
+
}
|
|
2198
|
+
try {
|
|
2199
|
+
sessionMemory.recordToolCall("execute_safe_test", { task, command, cwd: workingDir });
|
|
2200
|
+
const result = await spawnShell(command, {
|
|
2201
|
+
cwd: workingDir,
|
|
2202
|
+
timeoutSeconds: timeout
|
|
2203
|
+
});
|
|
2204
|
+
const output = formatExecResult(result, command, task, resolvedFrom);
|
|
2205
|
+
if (result.exitCode !== 0) {
|
|
2206
|
+
sessionMemory.addFailedFile(task, result.stderr || result.stdout);
|
|
2207
|
+
}
|
|
2208
|
+
return output;
|
|
2209
|
+
} catch (err) {
|
|
2210
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2211
|
+
if (errorMsg.toLowerCase().includes("timeout")) {
|
|
2212
|
+
return formatTimeoutResult(command, timeout);
|
|
2213
|
+
}
|
|
2214
|
+
return `Error running "${command}": ${errorMsg}`;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
function formatExecResult(result, command, task, resolvedFrom) {
|
|
2218
|
+
const status = result.exitCode === 0 ? "\u2705 PASS" : "\u274C FAIL";
|
|
2219
|
+
const lines = [
|
|
2220
|
+
`\u{1F4BB} ${status} \u2014 Task: ${task}`,
|
|
2221
|
+
`$ ${command}`,
|
|
2222
|
+
...resolvedFrom ? [`\u{1F4CD} ${resolvedFrom}`] : [],
|
|
2223
|
+
`Exit code: ${result.exitCode}`,
|
|
2224
|
+
`Duration: ${result.timedOut ? "TIMEOUT" : "completed"}`,
|
|
2225
|
+
""
|
|
2226
|
+
];
|
|
2227
|
+
if (result.stdout.trim()) {
|
|
2228
|
+
lines.push("\u{1F4E4} STDOUT:", "```", result.stdout, "```", "");
|
|
2229
|
+
}
|
|
2230
|
+
if (result.stderr.trim()) {
|
|
2231
|
+
lines.push("\u{1F4E4} STDERR:", "```", result.stderr, "```", "");
|
|
2232
|
+
}
|
|
2233
|
+
if (result.exitCode !== 0) {
|
|
1750
2234
|
lines.push(
|
|
1751
2235
|
"\u{1F4A1} Recovery steps:",
|
|
1752
2236
|
" 1. Read the error above \u2014 which file is failing?",
|
|
@@ -1776,8 +2260,8 @@ function formatTimeoutResult(command, timeout) {
|
|
|
1776
2260
|
}
|
|
1777
2261
|
|
|
1778
2262
|
// src/agents/codeReviewer.ts
|
|
1779
|
-
import
|
|
1780
|
-
import
|
|
2263
|
+
import fs9 from "fs";
|
|
2264
|
+
import path9 from "path";
|
|
1781
2265
|
import { execSync } from "child_process";
|
|
1782
2266
|
var CODE_EXTENSIONS = [
|
|
1783
2267
|
".ts",
|
|
@@ -1809,7 +2293,7 @@ function getGitChangedFiles() {
|
|
|
1809
2293
|
if (filePath.startsWith('"') && filePath.endsWith('"')) {
|
|
1810
2294
|
filePath = filePath.substring(1, filePath.length - 1);
|
|
1811
2295
|
}
|
|
1812
|
-
const ext =
|
|
2296
|
+
const ext = path9.extname(filePath).toLowerCase();
|
|
1813
2297
|
if (CODE_EXTENSIONS.includes(ext)) files.push(filePath);
|
|
1814
2298
|
}
|
|
1815
2299
|
return files.slice(0, 10);
|
|
@@ -1846,7 +2330,7 @@ Pass an explicit 'files' array to review specific files.`;
|
|
|
1846
2330
|
continue;
|
|
1847
2331
|
}
|
|
1848
2332
|
const resolvedPath = validation.resolvedPath;
|
|
1849
|
-
if (!
|
|
2333
|
+
if (!fs9.existsSync(resolvedPath)) {
|
|
1850
2334
|
allIssues.push({
|
|
1851
2335
|
file: filePath,
|
|
1852
2336
|
line: 0,
|
|
@@ -1858,7 +2342,7 @@ Pass an explicit 'files' array to review specific files.`;
|
|
|
1858
2342
|
continue;
|
|
1859
2343
|
}
|
|
1860
2344
|
try {
|
|
1861
|
-
const content =
|
|
2345
|
+
const content = fs9.readFileSync(resolvedPath, "utf-8");
|
|
1862
2346
|
filesReviewed++;
|
|
1863
2347
|
checkGeneral(filePath, content, allIssues);
|
|
1864
2348
|
switch (focus) {
|
|
@@ -1952,7 +2436,7 @@ function isTestFile(filePath) {
|
|
|
1952
2436
|
return /\.(test|spec)\.[a-z]+$/i.test(filePath) || /__tests__/.test(filePath);
|
|
1953
2437
|
}
|
|
1954
2438
|
function isTsLike(filePath) {
|
|
1955
|
-
const ext =
|
|
2439
|
+
const ext = path9.extname(filePath).toLowerCase();
|
|
1956
2440
|
return ext === ".ts" || ext === ".tsx";
|
|
1957
2441
|
}
|
|
1958
2442
|
function checkGeneral(filePath, content, issues) {
|
|
@@ -2391,460 +2875,105 @@ function checkOverEngineering(filePath, content, issues) {
|
|
|
2391
2875
|
line: lineNum,
|
|
2392
2876
|
severity: "info",
|
|
2393
2877
|
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))
|
|
2878
|
+
message: sp.msg,
|
|
2879
|
+
suggestion: sp.suggestion
|
|
2653
2880
|
});
|
|
2654
2881
|
}
|
|
2655
2882
|
}
|
|
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;
|
|
2883
|
+
const exportDecls = [];
|
|
2884
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2885
|
+
const ed = lines[i].match(/^\s*export\s+(const|let|var|function|class)\s+(\w+)/);
|
|
2886
|
+
if (ed) exportDecls.push({ name: ed[2], line: i + 1 });
|
|
2684
2887
|
}
|
|
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";
|
|
2888
|
+
for (const decl of exportDecls) {
|
|
2889
|
+
const refCount = (text.match(new RegExp(`\\b${decl.name}\\b`, "g")) || []).length;
|
|
2890
|
+
const isReactComponent = /^[A-Z]/.test(decl.name) && /return\s+<|React\./.test(text);
|
|
2891
|
+
if (refCount <= 1 && decl.name !== "default" && !isReactComponent) {
|
|
2892
|
+
issues.push({
|
|
2893
|
+
file: filePath,
|
|
2894
|
+
line: decl.line,
|
|
2895
|
+
severity: "info",
|
|
2896
|
+
rule: "over-engineering/unused-export",
|
|
2897
|
+
message: `"${decl.name}" is exported but referenced only in its declaration`,
|
|
2898
|
+
suggestion: "Check if it is imported elsewhere; if not, remove or inline"
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2704
2901
|
}
|
|
2705
|
-
return "unknown";
|
|
2706
2902
|
}
|
|
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";
|
|
2903
|
+
function findBlockBodies(lines, startPattern) {
|
|
2904
|
+
const results = [];
|
|
2905
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2906
|
+
const m = lines[i].match(startPattern);
|
|
2907
|
+
if (!m) continue;
|
|
2908
|
+
const name = m[0].match(/class\s+(\w+)/)?.[1] ?? "Unknown";
|
|
2909
|
+
const body = collectBlockLines(lines, i);
|
|
2910
|
+
results.push({ body: body.join("\n"), nameLine: i + 1, name });
|
|
2725
2911
|
}
|
|
2726
|
-
return
|
|
2912
|
+
return results;
|
|
2727
2913
|
}
|
|
2728
|
-
function
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
const
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
if (
|
|
2914
|
+
function collectBlockLines(lines, startIdx) {
|
|
2915
|
+
let depth = 0;
|
|
2916
|
+
let started = false;
|
|
2917
|
+
const block = [];
|
|
2918
|
+
for (let j = startIdx; j < lines.length; j++) {
|
|
2919
|
+
const line = lines[j];
|
|
2920
|
+
block.push(line);
|
|
2921
|
+
for (const ch of line) {
|
|
2922
|
+
if (ch === "{") {
|
|
2923
|
+
depth++;
|
|
2924
|
+
started = true;
|
|
2925
|
+
} else if (ch === "}") depth--;
|
|
2926
|
+
}
|
|
2927
|
+
if (started && depth <= 0) break;
|
|
2742
2928
|
}
|
|
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;
|
|
2929
|
+
return block;
|
|
2757
2930
|
}
|
|
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";
|
|
2931
|
+
function extractBodyAfter(lines, match) {
|
|
2932
|
+
const startIdx = lines.findIndex((l) => l.includes(match[0].trim().split(/\s+/).pop() ?? ""));
|
|
2933
|
+
if (startIdx < 0) return "";
|
|
2934
|
+
return collectBlockLines(lines, startIdx).join("\n");
|
|
2765
2935
|
}
|
|
2766
|
-
function
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2936
|
+
function formatReviewOutput(issues, filesReviewed, totalFiles, focus, customCriteria, autoDetected) {
|
|
2937
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
2938
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
2939
|
+
const infos = issues.filter((i) => i.severity === "info");
|
|
2940
|
+
const lines = [
|
|
2941
|
+
`\u{1F50D} **Code Review \u2014 Focus: ${focus}**`,
|
|
2942
|
+
...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."] : [],
|
|
2943
|
+
...autoDetected ? [`\u{1F4C2} Auto-detected ${totalFiles} changed file(s) from git`] : [],
|
|
2944
|
+
...customCriteria ? [`\u{1F4CB} **Custom Criteria:** ${customCriteria}`] : [],
|
|
2945
|
+
`\u{1F4C1} ${filesReviewed}/${totalFiles} files reviewed`,
|
|
2946
|
+
`\u{1F534} ${errors.length} errors | \u{1F7E1} ${warnings.length} warnings | \u{1F535} ${infos.length} info`,
|
|
2947
|
+
""
|
|
2948
|
+
];
|
|
2949
|
+
if (issues.length === 0) {
|
|
2950
|
+
lines.push("\u2705 No issues found.");
|
|
2951
|
+
return lines.join("\n");
|
|
2780
2952
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
if (fs8.existsSync(srcDir)) {
|
|
2787
|
-
const tsFiles = findFiles(srcDir, [".ts", ".tsx"]);
|
|
2788
|
-
if (tsFiles.length > 0) return "typescript";
|
|
2953
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2954
|
+
for (const issue of issues) {
|
|
2955
|
+
const existing = grouped.get(issue.file) ?? [];
|
|
2956
|
+
existing.push(issue);
|
|
2957
|
+
grouped.set(issue.file, existing);
|
|
2789
2958
|
}
|
|
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"));
|
|
2959
|
+
for (const [file, fileIssues] of grouped) {
|
|
2960
|
+
lines.push(`**\u{1F4C4} ${file}:**`);
|
|
2961
|
+
for (const issue of fileIssues) {
|
|
2962
|
+
const icon = issue.severity === "error" ? "\u{1F534}" : issue.severity === "warning" ? "\u{1F7E1}" : "\u{1F535}";
|
|
2963
|
+
lines.push(` ${icon} [L${issue.line}] [${issue.rule}] ${issue.message}`);
|
|
2964
|
+
if (issue.suggestion) lines.push(` \u{1F4A1} ${issue.suggestion}`);
|
|
2820
2965
|
}
|
|
2821
|
-
|
|
2966
|
+
lines.push("");
|
|
2822
2967
|
}
|
|
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 {
|
|
2968
|
+
if (errors.length > 0) {
|
|
2969
|
+
lines.push("\u26A0\uFE0F Fix errors first, then warnings.");
|
|
2970
|
+
lines.push("\u{1F4A1} Use precise_diff_editor to apply fixes.");
|
|
2971
|
+
} else if (warnings.length > 0) {
|
|
2972
|
+
lines.push("\u{1F4A1} Address warnings before merging.");
|
|
2973
|
+
} else {
|
|
2974
|
+
lines.push("\u2705 Only informational issues \u2014 ready for testing.");
|
|
2846
2975
|
}
|
|
2847
|
-
return
|
|
2976
|
+
return lines.join("\n");
|
|
2848
2977
|
}
|
|
2849
2978
|
|
|
2850
2979
|
// src/agents/projectConventions.ts
|
|
@@ -3035,8 +3164,8 @@ async function handleGitDiff(params) {
|
|
|
3035
3164
|
}
|
|
3036
3165
|
|
|
3037
3166
|
// src/tools/projectStructure.ts
|
|
3038
|
-
import
|
|
3039
|
-
import
|
|
3167
|
+
import fs10 from "fs";
|
|
3168
|
+
import path10 from "path";
|
|
3040
3169
|
var DEFAULT_IGNORE = [
|
|
3041
3170
|
"node_modules",
|
|
3042
3171
|
".git",
|
|
@@ -3073,7 +3202,7 @@ async function handleProjectStructure(params) {
|
|
|
3073
3202
|
sessionMemory.recordToolCall("project_structure", { depth: clampedDepth, folderOnly });
|
|
3074
3203
|
try {
|
|
3075
3204
|
const tree = buildTree(root, root, clampedDepth, 0, folderOnly, includePattern, excludePattern);
|
|
3076
|
-
const projectName =
|
|
3205
|
+
const projectName = path10.basename(root);
|
|
3077
3206
|
const lines = [
|
|
3078
3207
|
"[Project Structure] - " + projectName,
|
|
3079
3208
|
"Depth: " + clampedDepth + " | " + (folderOnly ? "Folders only" : "Files and folders"),
|
|
@@ -3096,7 +3225,7 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3096
3225
|
const lines = [];
|
|
3097
3226
|
let entries = [];
|
|
3098
3227
|
try {
|
|
3099
|
-
entries =
|
|
3228
|
+
entries = fs10.readdirSync(currentDir, { withFileTypes: true });
|
|
3100
3229
|
} catch {
|
|
3101
3230
|
return lines;
|
|
3102
3231
|
}
|
|
@@ -3105,12 +3234,12 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3105
3234
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
3106
3235
|
return a.name.localeCompare(b.name);
|
|
3107
3236
|
});
|
|
3108
|
-
const relativeDir =
|
|
3237
|
+
const relativeDir = path10.relative(root, currentDir) || ".";
|
|
3109
3238
|
const prefix = getPrefix(currentDepth);
|
|
3110
3239
|
for (const entry of entries) {
|
|
3111
3240
|
if (shouldIgnore(entry.name, relativeDir, root)) continue;
|
|
3112
|
-
const fullPath =
|
|
3113
|
-
const relativePath =
|
|
3241
|
+
const fullPath = path10.join(currentDir, entry.name);
|
|
3242
|
+
const relativePath = path10.relative(root, fullPath);
|
|
3114
3243
|
if (includePattern && !entry.name.includes(includePattern) && !relativePath.includes(includePattern)) {
|
|
3115
3244
|
if (!entry.isDirectory()) continue;
|
|
3116
3245
|
}
|
|
@@ -3121,11 +3250,11 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3121
3250
|
lines.push(prefix + "[D] " + entry.name + "/");
|
|
3122
3251
|
let subEntries = [];
|
|
3123
3252
|
try {
|
|
3124
|
-
subEntries =
|
|
3253
|
+
subEntries = fs10.readdirSync(fullPath, { withFileTypes: true });
|
|
3125
3254
|
} catch {
|
|
3126
3255
|
}
|
|
3127
3256
|
const hasVisibleContent = subEntries.some(
|
|
3128
|
-
(e) => !shouldIgnore(e.name,
|
|
3257
|
+
(e) => !shouldIgnore(e.name, path10.relative(root, fullPath), root)
|
|
3129
3258
|
);
|
|
3130
3259
|
if (hasVisibleContent) {
|
|
3131
3260
|
const subLines = buildTree(root, fullPath, maxDepth, currentDepth + 1, folderOnly, includePattern, excludePattern);
|
|
@@ -3156,7 +3285,7 @@ function getPrefix(depth) {
|
|
|
3156
3285
|
}
|
|
3157
3286
|
function getFileSize(fullPath) {
|
|
3158
3287
|
try {
|
|
3159
|
-
const stat =
|
|
3288
|
+
const stat = fs10.statSync(fullPath);
|
|
3160
3289
|
if (stat.size < 1024) return stat.size + "B";
|
|
3161
3290
|
if (stat.size < 1024 * 1024) return (stat.size / 1024).toFixed(0) + "KB";
|
|
3162
3291
|
const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
|
|
@@ -3167,8 +3296,8 @@ function getFileSize(fullPath) {
|
|
|
3167
3296
|
}
|
|
3168
3297
|
|
|
3169
3298
|
// src/tools/staticAnalysis.ts
|
|
3170
|
-
import
|
|
3171
|
-
import
|
|
3299
|
+
import fs11 from "fs";
|
|
3300
|
+
import path11 from "path";
|
|
3172
3301
|
async function handleStaticAnalysis(params) {
|
|
3173
3302
|
const { tool = "all", files, autoFix = false, timeout = 60 } = params;
|
|
3174
3303
|
const root = getProjectRoot();
|
|
@@ -3223,11 +3352,11 @@ function detectAvailableTools(root) {
|
|
|
3223
3352
|
"eslint.config.js",
|
|
3224
3353
|
"eslint.config.mjs"
|
|
3225
3354
|
];
|
|
3226
|
-
const hasEslintConfig = eslintConfigs.some((cfg) =>
|
|
3355
|
+
const hasEslintConfig = eslintConfigs.some((cfg) => fs11.existsSync(path11.join(root, cfg)));
|
|
3227
3356
|
if (hasEslintConfig && depNames.has("eslint")) {
|
|
3228
3357
|
tools.push("eslint");
|
|
3229
3358
|
}
|
|
3230
|
-
if (
|
|
3359
|
+
if (fs11.existsSync(path11.join(root, "tsconfig.json")) && depNames.has("typescript")) {
|
|
3231
3360
|
tools.push("tsc");
|
|
3232
3361
|
}
|
|
3233
3362
|
const prettierConfigs = [
|
|
@@ -3238,20 +3367,20 @@ function detectAvailableTools(root) {
|
|
|
3238
3367
|
".prettierrc.toml",
|
|
3239
3368
|
"prettier.config.js"
|
|
3240
3369
|
];
|
|
3241
|
-
const hasPrettierConfig = prettierConfigs.some((cfg) =>
|
|
3370
|
+
const hasPrettierConfig = prettierConfigs.some((cfg) => fs11.existsSync(path11.join(root, cfg)));
|
|
3242
3371
|
if (hasPrettierConfig && depNames.has("prettier")) {
|
|
3243
3372
|
tools.push("prettier");
|
|
3244
3373
|
}
|
|
3245
|
-
if (
|
|
3374
|
+
if (fs11.existsSync(path11.join(root, "ruff.toml")) || fs11.existsSync(path11.join(root, ".ruff.toml"))) {
|
|
3246
3375
|
tools.push("ruff");
|
|
3247
3376
|
}
|
|
3248
3377
|
return tools;
|
|
3249
3378
|
}
|
|
3250
3379
|
function readPackageJson(root) {
|
|
3251
3380
|
try {
|
|
3252
|
-
const pkgPath =
|
|
3253
|
-
if (
|
|
3254
|
-
return JSON.parse(
|
|
3381
|
+
const pkgPath = path11.join(root, "package.json");
|
|
3382
|
+
if (fs11.existsSync(pkgPath)) {
|
|
3383
|
+
return JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
|
|
3255
3384
|
}
|
|
3256
3385
|
} catch {
|
|
3257
3386
|
}
|
|
@@ -3296,8 +3425,8 @@ function buildToolCommand(tool, files, autoFix) {
|
|
|
3296
3425
|
}
|
|
3297
3426
|
function detectPackageManagerPrefix() {
|
|
3298
3427
|
const root = getProjectRoot();
|
|
3299
|
-
if (
|
|
3300
|
-
if (
|
|
3428
|
+
if (fs11.existsSync(path11.join(root, "pnpm-lock.yaml"))) return "pnpm ";
|
|
3429
|
+
if (fs11.existsSync(path11.join(root, "yarn.lock"))) return "yarn ";
|
|
3301
3430
|
return "npx --no-install ";
|
|
3302
3431
|
}
|
|
3303
3432
|
function parseToolOutput(tool, stdout, stderr, projectRoot) {
|
|
@@ -3316,7 +3445,7 @@ function parseToolOutput(tool, stdout, stderr, projectRoot) {
|
|
|
3316
3445
|
}
|
|
3317
3446
|
function resolveToolPath(filePath, projectRoot) {
|
|
3318
3447
|
const trimmed = filePath.trim();
|
|
3319
|
-
return
|
|
3448
|
+
return path11.isAbsolute(trimmed) ? path11.relative(projectRoot, trimmed) : trimmed.replace(/^\.\//, "");
|
|
3320
3449
|
}
|
|
3321
3450
|
function parseEslintOutput(output, projectRoot) {
|
|
3322
3451
|
const issues = [];
|
|
@@ -3593,8 +3722,8 @@ async function handleReflect(params) {
|
|
|
3593
3722
|
import { execSync as execSync6 } from "child_process";
|
|
3594
3723
|
|
|
3595
3724
|
// src/guards/antiPatternDetector.ts
|
|
3596
|
-
import
|
|
3597
|
-
import
|
|
3725
|
+
import fs12 from "fs";
|
|
3726
|
+
import path12 from "path";
|
|
3598
3727
|
import { execSync as execSync4 } from "child_process";
|
|
3599
3728
|
var SCRIPT_PATCH_PATTERNS = [
|
|
3600
3729
|
"writeFileSync",
|
|
@@ -3621,20 +3750,20 @@ function findPatchScripts(projectRoot) {
|
|
|
3621
3750
|
const warnings = [];
|
|
3622
3751
|
const recentFiles = scanRecentFiles(projectRoot);
|
|
3623
3752
|
for (const file of recentFiles) {
|
|
3624
|
-
const ext =
|
|
3753
|
+
const ext = path12.extname(file).toLowerCase();
|
|
3625
3754
|
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
3626
|
-
const relativePath =
|
|
3755
|
+
const relativePath = path12.relative(projectRoot, file);
|
|
3627
3756
|
if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".")) {
|
|
3628
3757
|
continue;
|
|
3629
3758
|
}
|
|
3630
3759
|
try {
|
|
3631
|
-
const content =
|
|
3760
|
+
const content = fs12.readFileSync(file, "utf-8").toLowerCase();
|
|
3632
3761
|
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
3633
3762
|
if (matchedPattern) {
|
|
3634
3763
|
warnings.push({
|
|
3635
3764
|
severity: "high",
|
|
3636
3765
|
pattern: "script-patching",
|
|
3637
|
-
message: `Created script file that modifies other files: ${
|
|
3766
|
+
message: `Created script file that modifies other files: ${path12.basename(file)}`,
|
|
3638
3767
|
suggestion: "Use **precise_diff_editor** instead \u2014 it has fuzzy matching, auto-backup, and rollback support",
|
|
3639
3768
|
evidence: `File: ${relativePath} contains '${matchedPattern}'`,
|
|
3640
3769
|
filePath: relativePath
|
|
@@ -3668,15 +3797,15 @@ function detectBashGrepUsage() {
|
|
|
3668
3797
|
function scanRecentFiles(projectRoot) {
|
|
3669
3798
|
const recent = [];
|
|
3670
3799
|
try {
|
|
3671
|
-
const entries =
|
|
3800
|
+
const entries = fs12.readdirSync(projectRoot, { withFileTypes: true });
|
|
3672
3801
|
const now = Date.now();
|
|
3673
3802
|
const recentThreshold = 30 * 60 * 1e3;
|
|
3674
3803
|
for (const entry of entries) {
|
|
3675
3804
|
if (!entry.isFile()) continue;
|
|
3676
3805
|
try {
|
|
3677
|
-
const stat =
|
|
3806
|
+
const stat = fs12.statSync(path12.join(projectRoot, entry.name));
|
|
3678
3807
|
if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
|
|
3679
|
-
recent.push(
|
|
3808
|
+
recent.push(path12.join(projectRoot, entry.name));
|
|
3680
3809
|
}
|
|
3681
3810
|
} catch {
|
|
3682
3811
|
continue;
|
|
@@ -3700,14 +3829,14 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
3700
3829
|
const prefix = line.substring(0, 2);
|
|
3701
3830
|
if (prefix !== "??" && prefix !== "A ") continue;
|
|
3702
3831
|
const file = line.substring(3).trim();
|
|
3703
|
-
const ext =
|
|
3832
|
+
const ext = path12.extname(file).toLowerCase();
|
|
3704
3833
|
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
3705
3834
|
const isRootLevel = !file.includes("/");
|
|
3706
3835
|
const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
|
|
3707
3836
|
if (!isRootLevel && !isScriptsDir) continue;
|
|
3708
|
-
const fullPath =
|
|
3709
|
-
if (
|
|
3710
|
-
const content =
|
|
3837
|
+
const fullPath = path12.join(projectRoot, file);
|
|
3838
|
+
if (fs12.existsSync(fullPath)) {
|
|
3839
|
+
const content = fs12.readFileSync(fullPath, "utf-8").toLowerCase();
|
|
3711
3840
|
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
3712
3841
|
if (matchedPattern) {
|
|
3713
3842
|
warnings.push({
|
|
@@ -3730,7 +3859,7 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
3730
3859
|
if (deletedStdout) {
|
|
3731
3860
|
const deletedFiles = deletedStdout.split("\n").filter(Boolean);
|
|
3732
3861
|
for (const file of deletedFiles) {
|
|
3733
|
-
const ext =
|
|
3862
|
+
const ext = path12.extname(file).toLowerCase();
|
|
3734
3863
|
if (SCRIPT_EXTENSIONS.includes(ext)) {
|
|
3735
3864
|
warnings.push({
|
|
3736
3865
|
severity: "high",
|
|
@@ -3790,21 +3919,21 @@ function detectAllAntiPatterns() {
|
|
|
3790
3919
|
}
|
|
3791
3920
|
|
|
3792
3921
|
// src/engine/contextSnapshot.ts
|
|
3793
|
-
import
|
|
3794
|
-
import
|
|
3922
|
+
import fs13 from "fs";
|
|
3923
|
+
import path13 from "path";
|
|
3795
3924
|
import { execSync as execSync5 } from "child_process";
|
|
3796
3925
|
var SNAPSHOT_DIR = "context-snapshots";
|
|
3797
3926
|
function snapshotDir() {
|
|
3798
|
-
return
|
|
3927
|
+
return path13.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
|
|
3799
3928
|
}
|
|
3800
3929
|
function ensureSnapshotDir() {
|
|
3801
3930
|
const dir = snapshotDir();
|
|
3802
|
-
if (!
|
|
3803
|
-
|
|
3931
|
+
if (!fs13.existsSync(dir)) {
|
|
3932
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
3804
3933
|
}
|
|
3805
3934
|
}
|
|
3806
3935
|
function snapshotFilePath(timestamp) {
|
|
3807
|
-
return
|
|
3936
|
+
return path13.join(snapshotDir(), `${timestamp}.json`);
|
|
3808
3937
|
}
|
|
3809
3938
|
function saveSnapshot(goal) {
|
|
3810
3939
|
try {
|
|
@@ -3825,7 +3954,7 @@ function saveSnapshot(goal) {
|
|
|
3825
3954
|
hasConventions: !!summary.hasConventions
|
|
3826
3955
|
};
|
|
3827
3956
|
try {
|
|
3828
|
-
|
|
3957
|
+
fs13.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
|
|
3829
3958
|
} catch {
|
|
3830
3959
|
}
|
|
3831
3960
|
sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
|
|
@@ -3833,12 +3962,12 @@ function saveSnapshot(goal) {
|
|
|
3833
3962
|
}
|
|
3834
3963
|
function listSnapshots() {
|
|
3835
3964
|
const dir = snapshotDir();
|
|
3836
|
-
if (!
|
|
3965
|
+
if (!fs13.existsSync(dir)) return [];
|
|
3837
3966
|
try {
|
|
3838
|
-
const files =
|
|
3967
|
+
const files = fs13.readdirSync(dir).filter((f) => f.endsWith(".json")).sort((a, b) => b.localeCompare(a));
|
|
3839
3968
|
return files.map((f) => {
|
|
3840
3969
|
try {
|
|
3841
|
-
const content =
|
|
3970
|
+
const content = fs13.readFileSync(path13.join(dir, f), "utf-8");
|
|
3842
3971
|
return JSON.parse(content);
|
|
3843
3972
|
} catch {
|
|
3844
3973
|
return null;
|
|
@@ -4046,8 +4175,8 @@ async function handleKumaContext(params) {
|
|
|
4046
4175
|
|
|
4047
4176
|
// src/engine/lspClient.ts
|
|
4048
4177
|
import { spawn as spawn2 } from "child_process";
|
|
4049
|
-
import
|
|
4050
|
-
import
|
|
4178
|
+
import path14 from "path";
|
|
4179
|
+
import fs14 from "fs";
|
|
4051
4180
|
var LSPClient = class {
|
|
4052
4181
|
process = null;
|
|
4053
4182
|
requestId = 0;
|
|
@@ -4085,8 +4214,8 @@ var LSPClient = class {
|
|
|
4085
4214
|
console.error(`[LSP] typescript-language-server not found. LSP features will fallback to regex.`);
|
|
4086
4215
|
return;
|
|
4087
4216
|
}
|
|
4088
|
-
const tsconfigPath =
|
|
4089
|
-
if (!
|
|
4217
|
+
const tsconfigPath = path14.join(root, "tsconfig.json");
|
|
4218
|
+
if (!fs14.existsSync(tsconfigPath)) {
|
|
4090
4219
|
console.error(`[LSP] Warning: tsconfig.json not found at "${root}". Running in implicit project mode.`);
|
|
4091
4220
|
}
|
|
4092
4221
|
this.process = spawn2(lspBinary, ["--stdio", "--log-level=4"], {
|
|
@@ -4148,19 +4277,19 @@ var LSPClient = class {
|
|
|
4148
4277
|
/** Resolve the typescript-language-server binary from local or global install */
|
|
4149
4278
|
resolveLspBinary(projectRoot) {
|
|
4150
4279
|
const candidates = [
|
|
4151
|
-
|
|
4152
|
-
|
|
4280
|
+
path14.join(projectRoot, "node_modules", ".bin", "typescript-language-server"),
|
|
4281
|
+
path14.join(projectRoot, "..", "node_modules", ".bin", "typescript-language-server")
|
|
4153
4282
|
];
|
|
4154
4283
|
for (const candidate of candidates) {
|
|
4155
|
-
if (
|
|
4284
|
+
if (fs14.existsSync(candidate)) {
|
|
4156
4285
|
return candidate;
|
|
4157
4286
|
}
|
|
4158
4287
|
}
|
|
4159
4288
|
try {
|
|
4160
4289
|
const envPath = process.env.PATH ?? "";
|
|
4161
|
-
for (const dir of envPath.split(
|
|
4162
|
-
const binPath =
|
|
4163
|
-
if (
|
|
4290
|
+
for (const dir of envPath.split(path14.delimiter)) {
|
|
4291
|
+
const binPath = path14.join(dir, "typescript-language-server");
|
|
4292
|
+
if (fs14.existsSync(binPath)) {
|
|
4164
4293
|
return binPath;
|
|
4165
4294
|
}
|
|
4166
4295
|
}
|
|
@@ -4181,7 +4310,7 @@ var LSPClient = class {
|
|
|
4181
4310
|
this.openDocuments.add(filePath);
|
|
4182
4311
|
let content;
|
|
4183
4312
|
try {
|
|
4184
|
-
content =
|
|
4313
|
+
content = fs14.readFileSync(filePath, "utf-8");
|
|
4185
4314
|
} catch {
|
|
4186
4315
|
content = "";
|
|
4187
4316
|
}
|
|
@@ -4398,8 +4527,8 @@ var LSPClient = class {
|
|
|
4398
4527
|
var lspClient = new LSPClient();
|
|
4399
4528
|
|
|
4400
4529
|
// src/tools/lspTools.ts
|
|
4401
|
-
import
|
|
4402
|
-
import
|
|
4530
|
+
import fs15 from "fs";
|
|
4531
|
+
import path15 from "path";
|
|
4403
4532
|
async function handleFindReferences(params) {
|
|
4404
4533
|
const { filePath, line, character } = params;
|
|
4405
4534
|
const validation = validateFilePath(filePath);
|
|
@@ -4407,7 +4536,7 @@ async function handleFindReferences(params) {
|
|
|
4407
4536
|
return `Error: ${validation.error.message}`;
|
|
4408
4537
|
}
|
|
4409
4538
|
const resolvedPath = validation.resolvedPath;
|
|
4410
|
-
if (!
|
|
4539
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
4411
4540
|
return `Error: File not found: "${filePath}".`;
|
|
4412
4541
|
}
|
|
4413
4542
|
if (!lspClient.isAvailable()) {
|
|
@@ -4429,7 +4558,7 @@ async function handleFindReferences(params) {
|
|
|
4429
4558
|
const enrichedRefs = references.map((ref) => {
|
|
4430
4559
|
let lineContent = "";
|
|
4431
4560
|
try {
|
|
4432
|
-
const content =
|
|
4561
|
+
const content = fs15.readFileSync(ref.filePath, "utf-8");
|
|
4433
4562
|
const lines2 = content.split("\n");
|
|
4434
4563
|
lineContent = lines2[ref.line]?.trim() ?? "";
|
|
4435
4564
|
} catch {
|
|
@@ -4445,11 +4574,11 @@ async function handleFindReferences(params) {
|
|
|
4445
4574
|
const projectRoot = getProjectRoot();
|
|
4446
4575
|
const lines = [
|
|
4447
4576
|
`\u{1F50D} **Find References** \u2014 ${enrichedRefs.length} references found`,
|
|
4448
|
-
`\u{1F4CD} File: ${
|
|
4577
|
+
`\u{1F4CD} File: ${path15.relative(projectRoot, resolvedPath)}:${line + 1}:${character + 1}`,
|
|
4449
4578
|
""
|
|
4450
4579
|
];
|
|
4451
4580
|
for (const [file, refs] of grouped) {
|
|
4452
|
-
const relPath =
|
|
4581
|
+
const relPath = path15.relative(projectRoot, file);
|
|
4453
4582
|
lines.push(`**\u{1F4C4} ${relPath}:**`);
|
|
4454
4583
|
for (const ref of refs) {
|
|
4455
4584
|
const loc = `L${ref.line + 1}:${ref.character + 1}`;
|
|
@@ -4470,7 +4599,7 @@ async function handleGoToDefinition(params) {
|
|
|
4470
4599
|
return `Error: ${validation.error.message}`;
|
|
4471
4600
|
}
|
|
4472
4601
|
const resolvedPath = validation.resolvedPath;
|
|
4473
|
-
if (!
|
|
4602
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
4474
4603
|
return `Error: File not found: "${filePath}".`;
|
|
4475
4604
|
}
|
|
4476
4605
|
if (!lspClient.isAvailable()) {
|
|
@@ -4490,10 +4619,10 @@ async function handleGoToDefinition(params) {
|
|
|
4490
4619
|
\u26A0\uFE0F Cannot find definition for symbol at this position.`;
|
|
4491
4620
|
}
|
|
4492
4621
|
const projectRoot = getProjectRoot();
|
|
4493
|
-
const relPath =
|
|
4622
|
+
const relPath = path15.relative(projectRoot, definition.filePath);
|
|
4494
4623
|
let lineContent = "";
|
|
4495
4624
|
try {
|
|
4496
|
-
const content =
|
|
4625
|
+
const content = fs15.readFileSync(definition.filePath, "utf-8");
|
|
4497
4626
|
const lines2 = content.split("\n");
|
|
4498
4627
|
lineContent = lines2[definition.line]?.trim() ?? "";
|
|
4499
4628
|
} catch {
|
|
@@ -4525,7 +4654,7 @@ async function handleRenameSymbol(params) {
|
|
|
4525
4654
|
return `Error: ${validation.error.message}`;
|
|
4526
4655
|
}
|
|
4527
4656
|
const resolvedPath = validation.resolvedPath;
|
|
4528
|
-
if (!
|
|
4657
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
4529
4658
|
return `Error: File not found: "${filePath}".`;
|
|
4530
4659
|
}
|
|
4531
4660
|
if (!lspClient.isAvailable()) {
|
|
@@ -4554,7 +4683,7 @@ Make sure:
|
|
|
4554
4683
|
const fileChanges = [];
|
|
4555
4684
|
for (const change of result.changes) {
|
|
4556
4685
|
try {
|
|
4557
|
-
const content =
|
|
4686
|
+
const content = fs15.readFileSync(change.filePath, "utf-8");
|
|
4558
4687
|
const lines2 = content.split("\n");
|
|
4559
4688
|
const sortedEdits = [...change.edits].sort((a, b) => {
|
|
4560
4689
|
if (b.line !== a.line) return b.line - a.line;
|
|
@@ -4568,10 +4697,10 @@ Make sure:
|
|
|
4568
4697
|
lines2[edit.line] = before + edit.newText + after;
|
|
4569
4698
|
}
|
|
4570
4699
|
}
|
|
4571
|
-
|
|
4700
|
+
fs15.writeFileSync(change.filePath, lines2.join("\n"), "utf-8");
|
|
4572
4701
|
totalEdits += change.edits.length;
|
|
4573
4702
|
fileChanges.push({
|
|
4574
|
-
filePath:
|
|
4703
|
+
filePath: path15.relative(projectRoot, change.filePath),
|
|
4575
4704
|
editCount: change.edits.length
|
|
4576
4705
|
});
|
|
4577
4706
|
} catch (err) {
|
|
@@ -4598,13 +4727,13 @@ async function handleGetTypeInfo(params) {
|
|
|
4598
4727
|
return `Error: ${validation.error.message}`;
|
|
4599
4728
|
}
|
|
4600
4729
|
const resolvedPath = validation.resolvedPath;
|
|
4601
|
-
if (!
|
|
4730
|
+
if (!fs15.existsSync(resolvedPath)) {
|
|
4602
4731
|
return `Error: File not found: "${filePath}".`;
|
|
4603
4732
|
}
|
|
4604
4733
|
if (!lspClient.isAvailable()) {
|
|
4605
4734
|
sessionMemory.recordToolCall("lsp_query", { action: "type", filePath, line, character, fallback: "line-context" });
|
|
4606
4735
|
try {
|
|
4607
|
-
const fileContent =
|
|
4736
|
+
const fileContent = fs15.readFileSync(resolvedPath, "utf-8");
|
|
4608
4737
|
const fileLines = fileContent.split("\n");
|
|
4609
4738
|
const targetLine = fileLines[line];
|
|
4610
4739
|
if (!targetLine) {
|
|
@@ -4612,7 +4741,7 @@ async function handleGetTypeInfo(params) {
|
|
|
4612
4741
|
}
|
|
4613
4742
|
const symbolName = extractSymbolAtPosition(resolvedPath, line, character);
|
|
4614
4743
|
const projectRoot = getProjectRoot();
|
|
4615
|
-
const relPath =
|
|
4744
|
+
const relPath = path15.relative(projectRoot, resolvedPath);
|
|
4616
4745
|
const contextStart = Math.max(0, line - 3);
|
|
4617
4746
|
const contextEnd = Math.min(fileLines.length, line + 4);
|
|
4618
4747
|
const contextLines = [];
|
|
@@ -4650,7 +4779,7 @@ async function handleGetTypeInfo(params) {
|
|
|
4650
4779
|
\u26A0\uFE0F No type info for this position.`;
|
|
4651
4780
|
}
|
|
4652
4781
|
const projectRoot = getProjectRoot();
|
|
4653
|
-
const relPath =
|
|
4782
|
+
const relPath = path15.relative(projectRoot, resolvedPath);
|
|
4654
4783
|
const lines = [
|
|
4655
4784
|
`\u{1F4CB} **Type Info** \u2014 \`${relPath}:${line + 1}:${character + 1}\``,
|
|
4656
4785
|
"",
|
|
@@ -4691,7 +4820,7 @@ async function handleLspQuery(params) {
|
|
|
4691
4820
|
}
|
|
4692
4821
|
function extractSymbolAtPosition(filePath, line, character) {
|
|
4693
4822
|
try {
|
|
4694
|
-
const content =
|
|
4823
|
+
const content = fs15.readFileSync(filePath, "utf-8");
|
|
4695
4824
|
const lines = content.split("\n");
|
|
4696
4825
|
const targetLine = lines[line];
|
|
4697
4826
|
if (!targetLine) return null;
|
|
@@ -4722,7 +4851,7 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4722
4851
|
const regex = new RegExp(`\\b${escapedSymbol}\\b`, "g");
|
|
4723
4852
|
for (const file of tsFiles.slice(0, 100)) {
|
|
4724
4853
|
try {
|
|
4725
|
-
const content =
|
|
4854
|
+
const content = fs15.readFileSync(file, "utf-8");
|
|
4726
4855
|
const lines2 = content.split("\n");
|
|
4727
4856
|
for (let i = 0; i < lines2.length; i++) {
|
|
4728
4857
|
if (regex.test(lines2[i])) {
|
|
@@ -4751,8 +4880,28 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4751
4880
|
`\u26A0\uFE0F Regex results may be less accurate than LSP (includes comments/strings).`,
|
|
4752
4881
|
""
|
|
4753
4882
|
];
|
|
4883
|
+
if (grouped.size > 1) {
|
|
4884
|
+
const fileList = [...grouped.keys()].map((f) => path15.relative(projectRoot, f)).join(", ");
|
|
4885
|
+
lines.push(`\u26A0\uFE0F **Ambiguity warning:** Found matches across ${grouped.size} different files (${fileList}).`);
|
|
4886
|
+
lines.push(` Regex fallback cannot distinguish between different scopes with the same symbol name.`);
|
|
4887
|
+
lines.push(` If you intended only one scope, clarify which file or location you mean.`);
|
|
4888
|
+
lines.push(` \u{1F4A1} Install typescript-language-server for scope-aware disambiguation.`);
|
|
4889
|
+
lines.push("");
|
|
4890
|
+
} else if (results.length > 1) {
|
|
4891
|
+
const filePath = [...grouped.keys()][0];
|
|
4892
|
+
const refs = grouped.get(filePath);
|
|
4893
|
+
const matchLines = refs.map((r) => r.line);
|
|
4894
|
+
const minLine = Math.min(...matchLines);
|
|
4895
|
+
const maxLine = Math.max(...matchLines);
|
|
4896
|
+
if (maxLine - minLine > 15) {
|
|
4897
|
+
lines.push(`\u26A0\uFE0F **Ambiguity warning:** Found ${results.length} matches for "${symbolName}" spanning lines ${minLine}-${maxLine} in the same file.`);
|
|
4898
|
+
lines.push(` They may be in different function scopes \u2014 regex cannot distinguish them.`);
|
|
4899
|
+
lines.push(` \u{1F4A1} Verify each match is the intended symbol before editing.`);
|
|
4900
|
+
lines.push("");
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4754
4903
|
for (const [file, refs] of grouped) {
|
|
4755
|
-
const relPath =
|
|
4904
|
+
const relPath = path15.relative(projectRoot, file);
|
|
4756
4905
|
lines.push(`**\u{1F4C4} ${relPath}:**`);
|
|
4757
4906
|
for (const ref of refs) {
|
|
4758
4907
|
lines.push(` \u2514 L${ref.line} \u2014 ${ref.content}`);
|
|
@@ -4786,14 +4935,14 @@ async function fallbackGrepDefinition(symbolName) {
|
|
|
4786
4935
|
];
|
|
4787
4936
|
for (const file of tsFiles) {
|
|
4788
4937
|
try {
|
|
4789
|
-
const content =
|
|
4938
|
+
const content = fs15.readFileSync(file, "utf-8");
|
|
4790
4939
|
const lines = content.split("\n");
|
|
4791
4940
|
for (let i = 0; i < lines.length; i++) {
|
|
4792
4941
|
const trimmed = lines[i].trim();
|
|
4793
4942
|
for (const pattern of declPatterns) {
|
|
4794
4943
|
if (pattern.test(trimmed)) {
|
|
4795
4944
|
const projectRoot = getProjectRoot();
|
|
4796
|
-
const relPath =
|
|
4945
|
+
const relPath = path15.relative(projectRoot, file);
|
|
4797
4946
|
return [
|
|
4798
4947
|
`\u{1F4CD} **Go to Definition** (regex fallback)`,
|
|
4799
4948
|
`\u{1F4C4} File: \`${relPath}\``,
|
|
@@ -4906,11 +5055,13 @@ function registerAllTools(server) {
|
|
|
4906
5055
|
);
|
|
4907
5056
|
server.tool(
|
|
4908
5057
|
"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.",
|
|
5058
|
+
"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
5059
|
{
|
|
4911
5060
|
task: z.enum(["test", "build", "lint", "typecheck", "custom"]).describe("Task to execute: test, build, lint, typecheck, or custom"),
|
|
4912
5061
|
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)")
|
|
5062
|
+
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)"),
|
|
5063
|
+
cwd: z.string().optional().describe("Relative directory from project root (e.g. 'packages/web'). Defaults to project root."),
|
|
5064
|
+
workspace: z.string().optional().describe("Workspace name from project_conventions (e.g. 'frontend', 'api'). Auto-resolves to path.")
|
|
4914
5065
|
},
|
|
4915
5066
|
async (params) => {
|
|
4916
5067
|
try {
|
|
@@ -5143,8 +5294,8 @@ function registerAllTools(server) {
|
|
|
5143
5294
|
}
|
|
5144
5295
|
|
|
5145
5296
|
// src/cli/init.ts
|
|
5146
|
-
import
|
|
5147
|
-
import
|
|
5297
|
+
import fs16 from "fs";
|
|
5298
|
+
import path16 from "path";
|
|
5148
5299
|
var ALL_CONFIG_TYPES = [
|
|
5149
5300
|
"claude",
|
|
5150
5301
|
"cursor",
|
|
@@ -5536,10 +5687,10 @@ var TEMPLATES = {
|
|
|
5536
5687
|
};
|
|
5537
5688
|
var APPEND_SEPARATOR = "\n\n---\n_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_\n\n";
|
|
5538
5689
|
function handleOpencodeSecondary(root, results) {
|
|
5539
|
-
const claudePath =
|
|
5540
|
-
if (!
|
|
5690
|
+
const claudePath = path16.resolve(root, "CLAUDE.md");
|
|
5691
|
+
if (!fs16.existsSync(claudePath)) {
|
|
5541
5692
|
try {
|
|
5542
|
-
|
|
5693
|
+
fs16.writeFileSync(claudePath, [
|
|
5543
5694
|
"# Kuma MCP - OpenCode Instructions",
|
|
5544
5695
|
"",
|
|
5545
5696
|
KUMA_CORE_INSTRUCTIONS
|
|
@@ -5556,21 +5707,21 @@ function handleOpencodeSecondary(root, results) {
|
|
|
5556
5707
|
}
|
|
5557
5708
|
}
|
|
5558
5709
|
function handleCodexSecondary(root, results) {
|
|
5559
|
-
const tomlPath =
|
|
5710
|
+
const tomlPath = path16.resolve(root, ".codex/config.toml");
|
|
5560
5711
|
if (results.some((r) => r.filePath === ".codex/config.toml")) return;
|
|
5561
5712
|
try {
|
|
5562
|
-
const dir =
|
|
5563
|
-
if (!
|
|
5564
|
-
if (
|
|
5565
|
-
const existingContent =
|
|
5713
|
+
const dir = path16.dirname(tomlPath);
|
|
5714
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5715
|
+
if (fs16.existsSync(tomlPath)) {
|
|
5716
|
+
const existingContent = fs16.readFileSync(tomlPath, "utf-8");
|
|
5566
5717
|
if (existingContent.includes("kuma")) {
|
|
5567
5718
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
|
|
5568
5719
|
return;
|
|
5569
5720
|
}
|
|
5570
|
-
|
|
5721
|
+
fs16.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
|
|
5571
5722
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
|
|
5572
5723
|
} else {
|
|
5573
|
-
|
|
5724
|
+
fs16.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
|
|
5574
5725
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
|
|
5575
5726
|
}
|
|
5576
5727
|
} catch (err) {
|
|
@@ -5583,11 +5734,11 @@ function handleCodexSecondary(root, results) {
|
|
|
5583
5734
|
}
|
|
5584
5735
|
}
|
|
5585
5736
|
function handleQwenSecondary(root, results) {
|
|
5586
|
-
const settingsPath =
|
|
5737
|
+
const settingsPath = path16.resolve(root, "settings.json");
|
|
5587
5738
|
if (results.some((r) => r.filePath === "settings.json")) return;
|
|
5588
5739
|
try {
|
|
5589
|
-
if (
|
|
5590
|
-
const existingContent =
|
|
5740
|
+
if (fs16.existsSync(settingsPath)) {
|
|
5741
|
+
const existingContent = fs16.readFileSync(settingsPath, "utf-8");
|
|
5591
5742
|
if (existingContent.includes("kuma")) {
|
|
5592
5743
|
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5593
5744
|
try {
|
|
@@ -5595,7 +5746,7 @@ function handleQwenSecondary(root, results) {
|
|
|
5595
5746
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
5596
5747
|
if (!parsed.mcpServers.kuma) {
|
|
5597
5748
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5598
|
-
|
|
5749
|
+
fs16.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5599
5750
|
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
5600
5751
|
return;
|
|
5601
5752
|
}
|
|
@@ -5610,13 +5761,13 @@ function handleQwenSecondary(root, results) {
|
|
|
5610
5761
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
5611
5762
|
if (!parsed.mcpServers.kuma) {
|
|
5612
5763
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5613
|
-
|
|
5764
|
+
fs16.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5614
5765
|
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
5615
5766
|
}
|
|
5616
5767
|
} catch {
|
|
5617
5768
|
}
|
|
5618
5769
|
} else {
|
|
5619
|
-
|
|
5770
|
+
fs16.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
|
|
5620
5771
|
results.push({ type: "qwen", filePath: "settings.json", action: "created" });
|
|
5621
5772
|
}
|
|
5622
5773
|
} catch (err) {
|
|
@@ -5652,18 +5803,18 @@ function getCombinedAgentsMd(selectedTypes) {
|
|
|
5652
5803
|
return sections.join("\n\n---\n\n");
|
|
5653
5804
|
}
|
|
5654
5805
|
function handleAntigravityMcpConfig(root, results) {
|
|
5655
|
-
const mcpPath =
|
|
5806
|
+
const mcpPath = path16.resolve(root, ".agents/mcp_config.json");
|
|
5656
5807
|
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
5657
5808
|
try {
|
|
5658
|
-
const mcpDir =
|
|
5659
|
-
if (
|
|
5660
|
-
const existingContent =
|
|
5809
|
+
const mcpDir = path16.dirname(mcpPath);
|
|
5810
|
+
if (fs16.existsSync(mcpPath)) {
|
|
5811
|
+
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5661
5812
|
if (existingContent.includes("kuma")) {
|
|
5662
5813
|
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5663
5814
|
const trimmed = existingContent.trimEnd();
|
|
5664
5815
|
if (trimmed.endsWith("}")) {
|
|
5665
5816
|
const updated = trimmed.slice(0, -1).trimEnd() + ',\n "_kuma_note": "Kuma MCP - Generated by kuma init"\n}\n';
|
|
5666
|
-
|
|
5817
|
+
fs16.writeFileSync(mcpPath, updated, "utf-8");
|
|
5667
5818
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5668
5819
|
}
|
|
5669
5820
|
}
|
|
@@ -5672,11 +5823,11 @@ function handleAntigravityMcpConfig(root, results) {
|
|
|
5672
5823
|
const parsed = JSON.parse(existingContent);
|
|
5673
5824
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
5674
5825
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5675
|
-
|
|
5826
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5676
5827
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5677
5828
|
} else {
|
|
5678
|
-
if (!
|
|
5679
|
-
|
|
5829
|
+
if (!fs16.existsSync(mcpDir)) fs16.mkdirSync(mcpDir, { recursive: true });
|
|
5830
|
+
fs16.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
5680
5831
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
|
|
5681
5832
|
}
|
|
5682
5833
|
} catch (err) {
|
|
@@ -5689,18 +5840,18 @@ function handleAntigravityMcpConfig(root, results) {
|
|
|
5689
5840
|
}
|
|
5690
5841
|
}
|
|
5691
5842
|
function handleAiderSecondary(root, results) {
|
|
5692
|
-
const ymlPath =
|
|
5843
|
+
const ymlPath = path16.resolve(root, ".aider.conf.yml");
|
|
5693
5844
|
if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
|
|
5694
5845
|
try {
|
|
5695
5846
|
const conventionsRef = "read: CONVENTIONS.md";
|
|
5696
|
-
if (
|
|
5697
|
-
const existingContent =
|
|
5847
|
+
if (fs16.existsSync(ymlPath)) {
|
|
5848
|
+
const existingContent = fs16.readFileSync(ymlPath, "utf-8");
|
|
5698
5849
|
if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
|
|
5699
5850
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
|
|
5700
5851
|
return;
|
|
5701
5852
|
}
|
|
5702
5853
|
const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
|
|
5703
|
-
|
|
5854
|
+
fs16.writeFileSync(ymlPath, newContent, "utf-8");
|
|
5704
5855
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
|
|
5705
5856
|
} else {
|
|
5706
5857
|
const content = [
|
|
@@ -5710,7 +5861,7 @@ function handleAiderSecondary(root, results) {
|
|
|
5710
5861
|
conventionsRef,
|
|
5711
5862
|
""
|
|
5712
5863
|
].join("\n");
|
|
5713
|
-
|
|
5864
|
+
fs16.writeFileSync(ymlPath, content, "utf-8");
|
|
5714
5865
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
|
|
5715
5866
|
}
|
|
5716
5867
|
} catch (err) {
|
|
@@ -5723,10 +5874,10 @@ function handleAiderSecondary(root, results) {
|
|
|
5723
5874
|
}
|
|
5724
5875
|
}
|
|
5725
5876
|
function handleCopilotSecondary(root, results) {
|
|
5726
|
-
const skillPath =
|
|
5877
|
+
const skillPath = path16.resolve(root, ".github/skills/kuma/SKILL.md");
|
|
5727
5878
|
if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
|
|
5728
5879
|
try {
|
|
5729
|
-
const dir =
|
|
5880
|
+
const dir = path16.dirname(skillPath);
|
|
5730
5881
|
const content = [
|
|
5731
5882
|
"---",
|
|
5732
5883
|
"name: kuma-mcp",
|
|
@@ -5737,17 +5888,17 @@ function handleCopilotSecondary(root, results) {
|
|
|
5737
5888
|
"",
|
|
5738
5889
|
KUMA_CORE_INSTRUCTIONS
|
|
5739
5890
|
].join("\n");
|
|
5740
|
-
if (
|
|
5741
|
-
const existingContent =
|
|
5891
|
+
if (fs16.existsSync(skillPath)) {
|
|
5892
|
+
const existingContent = fs16.readFileSync(skillPath, "utf-8");
|
|
5742
5893
|
if (existingContent.includes("kuma")) {
|
|
5743
5894
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
|
|
5744
5895
|
return;
|
|
5745
5896
|
}
|
|
5746
|
-
|
|
5897
|
+
fs16.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
|
|
5747
5898
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
|
|
5748
5899
|
} else {
|
|
5749
|
-
if (!
|
|
5750
|
-
|
|
5900
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5901
|
+
fs16.writeFileSync(skillPath, content, "utf-8");
|
|
5751
5902
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
|
|
5752
5903
|
}
|
|
5753
5904
|
} catch (err) {
|
|
@@ -5760,21 +5911,21 @@ function handleCopilotSecondary(root, results) {
|
|
|
5760
5911
|
}
|
|
5761
5912
|
}
|
|
5762
5913
|
function handleOpenclawSecondary(root, results) {
|
|
5763
|
-
const mcpPath =
|
|
5914
|
+
const mcpPath = path16.resolve(root, ".agents/mcp_config.json");
|
|
5764
5915
|
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
5765
5916
|
try {
|
|
5766
|
-
const dir =
|
|
5767
|
-
if (
|
|
5768
|
-
const existingContent =
|
|
5917
|
+
const dir = path16.dirname(mcpPath);
|
|
5918
|
+
if (fs16.existsSync(mcpPath)) {
|
|
5919
|
+
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5769
5920
|
if (existingContent.includes("kuma")) return;
|
|
5770
5921
|
const parsed = JSON.parse(existingContent);
|
|
5771
5922
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
5772
5923
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5773
|
-
|
|
5924
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5774
5925
|
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5775
5926
|
} else {
|
|
5776
|
-
if (!
|
|
5777
|
-
|
|
5927
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5928
|
+
fs16.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
5778
5929
|
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
|
|
5779
5930
|
}
|
|
5780
5931
|
} catch (err) {
|
|
@@ -5787,20 +5938,20 @@ function handleOpenclawSecondary(root, results) {
|
|
|
5787
5938
|
}
|
|
5788
5939
|
}
|
|
5789
5940
|
function handleCodewhaleSecondary(root, results) {
|
|
5790
|
-
const mcpPath =
|
|
5941
|
+
const mcpPath = path16.resolve(root, ".codewhale/mcp.json");
|
|
5791
5942
|
if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
|
|
5792
5943
|
try {
|
|
5793
|
-
const dir =
|
|
5794
|
-
if (
|
|
5795
|
-
const existingContent =
|
|
5944
|
+
const dir = path16.dirname(mcpPath);
|
|
5945
|
+
if (fs16.existsSync(mcpPath)) {
|
|
5946
|
+
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5796
5947
|
if (existingContent.includes("kuma")) return;
|
|
5797
5948
|
const parsed = JSON.parse(existingContent);
|
|
5798
5949
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
5799
5950
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5800
|
-
|
|
5951
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5801
5952
|
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
|
|
5802
5953
|
} else {
|
|
5803
|
-
if (!
|
|
5954
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5804
5955
|
const config = {
|
|
5805
5956
|
mcpServers: {
|
|
5806
5957
|
kuma: {
|
|
@@ -5810,7 +5961,7 @@ function handleCodewhaleSecondary(root, results) {
|
|
|
5810
5961
|
}
|
|
5811
5962
|
}
|
|
5812
5963
|
};
|
|
5813
|
-
|
|
5964
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5814
5965
|
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
|
|
5815
5966
|
}
|
|
5816
5967
|
} catch (err) {
|
|
@@ -5831,24 +5982,24 @@ function runInit(types, projectRoot) {
|
|
|
5831
5982
|
let agentsMdHandled = false;
|
|
5832
5983
|
for (const type of selected) {
|
|
5833
5984
|
const relativePath = configFilePath(type);
|
|
5834
|
-
const fullPath =
|
|
5985
|
+
const fullPath = path16.resolve(root, relativePath);
|
|
5835
5986
|
const getTemplate = TEMPLATES[type];
|
|
5836
5987
|
try {
|
|
5837
5988
|
if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
|
|
5838
5989
|
agentsMdHandled = true;
|
|
5839
5990
|
const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
|
|
5840
|
-
if (
|
|
5841
|
-
const existingContent =
|
|
5991
|
+
if (fs16.existsSync(fullPath)) {
|
|
5992
|
+
const existingContent = fs16.readFileSync(fullPath, "utf-8");
|
|
5842
5993
|
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5843
5994
|
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
5844
5995
|
} else {
|
|
5845
|
-
|
|
5996
|
+
fs16.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
|
|
5846
5997
|
results.push({ type, filePath: relativePath, action: "appended" });
|
|
5847
5998
|
}
|
|
5848
5999
|
} else {
|
|
5849
|
-
const dir =
|
|
5850
|
-
if (!
|
|
5851
|
-
|
|
6000
|
+
const dir = path16.dirname(fullPath);
|
|
6001
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
6002
|
+
fs16.writeFileSync(fullPath, combinedContent, "utf-8");
|
|
5852
6003
|
results.push({ type, filePath: relativePath, action: "created" });
|
|
5853
6004
|
}
|
|
5854
6005
|
if (selectedSet.has("codex")) handleCodexSecondary(root, results);
|
|
@@ -5859,8 +6010,8 @@ function runInit(types, projectRoot) {
|
|
|
5859
6010
|
continue;
|
|
5860
6011
|
} else {
|
|
5861
6012
|
const template = getTemplate();
|
|
5862
|
-
if (
|
|
5863
|
-
const existingContent =
|
|
6013
|
+
if (fs16.existsSync(fullPath)) {
|
|
6014
|
+
const existingContent = fs16.readFileSync(fullPath, "utf-8");
|
|
5864
6015
|
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5865
6016
|
if (type === "antigravity") {
|
|
5866
6017
|
handleAntigravityMcpConfig(root, results);
|
|
@@ -5873,14 +6024,14 @@ function runInit(types, projectRoot) {
|
|
|
5873
6024
|
continue;
|
|
5874
6025
|
}
|
|
5875
6026
|
const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
|
|
5876
|
-
|
|
6027
|
+
fs16.writeFileSync(fullPath, newContent, "utf-8");
|
|
5877
6028
|
results.push({ type, filePath: relativePath, action: "appended" });
|
|
5878
6029
|
} else {
|
|
5879
|
-
const dir =
|
|
5880
|
-
if (!
|
|
5881
|
-
|
|
6030
|
+
const dir = path16.dirname(fullPath);
|
|
6031
|
+
if (!fs16.existsSync(dir)) {
|
|
6032
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
5882
6033
|
}
|
|
5883
|
-
|
|
6034
|
+
fs16.writeFileSync(fullPath, template, "utf-8");
|
|
5884
6035
|
results.push({ type, filePath: relativePath, action: "created" });
|
|
5885
6036
|
}
|
|
5886
6037
|
if (type === "antigravity") {
|