@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.
Files changed (2) hide show
  1. package/dist/index.js +884 -733
  2. 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
- var TASK_COMMANDS = {
1672
- test: "npm test",
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
- Fix the code first before running the task again.`;
1708
- }
1709
- const dangerousPattern = DANGEROUS_PATTERNS.find((p) => command.toLowerCase().includes(p.toLowerCase()));
1710
- if (dangerousPattern) {
1711
- return `\u{1F6AB} BLOCKED: Command contains a dangerous pattern: "${dangerousPattern}".
1712
- This command is not permitted.`;
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
- sessionMemory.recordToolCall("execute_safe_test", { task, command });
1717
- const result = await spawnShell(command, {
1718
- cwd: projectRoot,
1719
- timeoutSeconds: timeout
1720
- });
1721
- const output = formatExecResult(result, command, task);
1722
- if (result.exitCode !== 0) {
1723
- sessionMemory.addFailedFile(task, result.stderr || result.stdout);
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
- return output;
1726
- } catch (err) {
1727
- const errorMsg = err instanceof Error ? err.message : String(err);
1728
- if (errorMsg.toLowerCase().includes("timeout")) {
1729
- return formatTimeoutResult(command, timeout);
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
- return `Error running "${command}": ${errorMsg}`;
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 formatExecResult(result, command, task) {
1735
- const status = result.exitCode === 0 ? "\u2705 PASS" : "\u274C FAIL";
1736
- const lines = [
1737
- `\u{1F4BB} ${status} \u2014 Task: ${task}`,
1738
- `$ ${command}`,
1739
- `Exit code: ${result.exitCode}`,
1740
- `Duration: ${result.timedOut ? "TIMEOUT" : "completed"}`,
1741
- ""
1742
- ];
1743
- if (result.stdout.trim()) {
1744
- lines.push("\u{1F4E4} STDOUT:", "```", result.stdout, "```", "");
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 (result.stderr.trim()) {
1747
- lines.push("\u{1F4E4} STDERR:", "```", result.stderr, "```", "");
1752
+ if (Array.isArray(pnpmWorkspace?.packages)) {
1753
+ for (const p of pnpmWorkspace.packages) if (typeof p === "string") patterns.add(p);
1748
1754
  }
1749
- if (result.exitCode !== 0) {
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 fs7 from "fs";
1780
- import path7 from "path";
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 = path7.extname(filePath).toLowerCase();
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 (!fs7.existsSync(resolvedPath)) {
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 = fs7.readFileSync(resolvedPath, "utf-8");
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 = path7.extname(filePath).toLowerCase();
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 seen = /* @__PURE__ */ new Set();
2657
- return results.filter((w) => {
2658
- if (seen.has(w.path)) return false;
2659
- seen.add(w.path);
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
- function detectTestRunner(root) {
2687
- const pkg = readJsonSafe(path8.join(root, "package.json"));
2688
- if (!pkg) return "unknown";
2689
- const pkgDeps2 = pkg.dependencies ?? {};
2690
- const pkgDevDeps2 = pkg.devDependencies ?? {};
2691
- const deps = { ...pkgDeps2, ...pkgDevDeps2 };
2692
- if (deps.vitest) return "Vitest";
2693
- if (deps.jest) return "Jest";
2694
- if (deps.mocha) return "Mocha";
2695
- if (deps.ava) return "AVA";
2696
- if (deps.cypress) return "Cypress";
2697
- if (deps.playwright) return "Playwright";
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 detectStyling(root) {
2708
- const pkg = readJsonSafe(path8.join(root, "package.json"));
2709
- if (!pkg) return "unknown";
2710
- const pkgDeps3 = pkg.dependencies ?? {};
2711
- const pkgDevDeps3 = pkg.devDependencies ?? {};
2712
- const deps = { ...pkgDeps3, ...pkgDevDeps3 };
2713
- if (deps.tailwindcss) return "Tailwind CSS";
2714
- if (deps["styled-components"]) return "Styled Components";
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 "unknown";
2912
+ return results;
2727
2913
  }
2728
- function detectImportAlias(root) {
2729
- const tsconfig = readJsonSafe(path8.join(root, "tsconfig.json"));
2730
- const tsconfigPaths = tsconfig?.compilerOptions;
2731
- if (tsconfigPaths?.paths) {
2732
- const paths = tsconfigPaths.paths;
2733
- const alias = Object.keys(paths)[0];
2734
- if (alias) return alias.replace("/*", "");
2735
- }
2736
- const jsconfig = readJsonSafe(path8.join(root, "jsconfig.json"));
2737
- const jsconfigPaths = jsconfig?.compilerOptions;
2738
- if (jsconfigPaths?.paths) {
2739
- const paths = jsconfigPaths.paths;
2740
- const alias = Object.keys(paths)[0];
2741
- if (alias) return alias.replace("/*", "");
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 void 0;
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 detectPackageManager() {
2759
- const root = getProjectRoot();
2760
- if (fs8.existsSync(path8.join(root, "pnpm-lock.yaml"))) return "pnpm";
2761
- if (fs8.existsSync(path8.join(root, "yarn.lock"))) return "yarn";
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 detectModuleSystem(root) {
2767
- const pkg = readJsonSafe(path8.join(root, "package.json"));
2768
- if (pkg?.type === "module") return "esm";
2769
- const srcDir = path8.join(root, "src");
2770
- if (fs8.existsSync(srcDir)) {
2771
- const tsFiles = findFiles(srcDir, [".ts", ".tsx", ".js", ".jsx", ".mjs"]);
2772
- for (const file of tsFiles.slice(0, 20)) {
2773
- try {
2774
- const content = fs8.readFileSync(file, "utf-8");
2775
- if (content.includes("import ") || content.includes("export ")) return "esm";
2776
- } catch {
2777
- continue;
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
- return "cjs";
2782
- }
2783
- function detectLanguage(root) {
2784
- if (fs8.existsSync(path8.join(root, "tsconfig.json"))) return "typescript";
2785
- const srcDir = path8.join(root, "src");
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
- return "javascript";
2791
- }
2792
- function detectFeatures(root) {
2793
- const features = [];
2794
- const pkg = readJsonSafe(path8.join(root, "package.json"));
2795
- if (!pkg) return features;
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
- } catch {
2966
+ lines.push("");
2822
2967
  }
2823
- return null;
2824
- }
2825
- function hasCSSModules(root) {
2826
- const srcDir = path8.join(root, "src");
2827
- if (!fs8.existsSync(srcDir)) return false;
2828
- const files = findFiles(srcDir, [".module.css", ".module.scss", ".module.less"]);
2829
- return files.length > 0;
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 results;
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 fs9 from "fs";
3039
- import path9 from "path";
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 = path9.basename(root);
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 = fs9.readdirSync(currentDir, { withFileTypes: true });
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 = path9.relative(root, currentDir) || ".";
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 = path9.join(currentDir, entry.name);
3113
- const relativePath = path9.relative(root, fullPath);
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 = fs9.readdirSync(fullPath, { withFileTypes: true });
3253
+ subEntries = fs10.readdirSync(fullPath, { withFileTypes: true });
3125
3254
  } catch {
3126
3255
  }
3127
3256
  const hasVisibleContent = subEntries.some(
3128
- (e) => !shouldIgnore(e.name, path9.relative(root, fullPath), root)
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 = fs9.statSync(fullPath);
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 fs10 from "fs";
3171
- import path10 from "path";
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) => fs10.existsSync(path10.join(root, 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 (fs10.existsSync(path10.join(root, "tsconfig.json")) && depNames.has("typescript")) {
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) => fs10.existsSync(path10.join(root, 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 (fs10.existsSync(path10.join(root, "ruff.toml")) || fs10.existsSync(path10.join(root, ".ruff.toml"))) {
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 = path10.join(root, "package.json");
3253
- if (fs10.existsSync(pkgPath)) {
3254
- return JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
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 (fs10.existsSync(path10.join(root, "pnpm-lock.yaml"))) return "pnpm ";
3300
- if (fs10.existsSync(path10.join(root, "yarn.lock"))) return "yarn ";
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 path10.isAbsolute(trimmed) ? path10.relative(projectRoot, trimmed) : trimmed.replace(/^\.\//, "");
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 fs11 from "fs";
3597
- import path11 from "path";
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 = path11.extname(file).toLowerCase();
3753
+ const ext = path12.extname(file).toLowerCase();
3625
3754
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
3626
- const relativePath = path11.relative(projectRoot, file);
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 = fs11.readFileSync(file, "utf-8").toLowerCase();
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: ${path11.basename(file)}`,
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 = fs11.readdirSync(projectRoot, { withFileTypes: true });
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 = fs11.statSync(path11.join(projectRoot, entry.name));
3806
+ const stat = fs12.statSync(path12.join(projectRoot, entry.name));
3678
3807
  if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
3679
- recent.push(path11.join(projectRoot, entry.name));
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 = path11.extname(file).toLowerCase();
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 = path11.join(projectRoot, file);
3709
- if (fs11.existsSync(fullPath)) {
3710
- const content = fs11.readFileSync(fullPath, "utf-8").toLowerCase();
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 = path11.extname(file).toLowerCase();
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 fs12 from "fs";
3794
- import path12 from "path";
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 path12.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
3927
+ return path13.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
3799
3928
  }
3800
3929
  function ensureSnapshotDir() {
3801
3930
  const dir = snapshotDir();
3802
- if (!fs12.existsSync(dir)) {
3803
- fs12.mkdirSync(dir, { recursive: true });
3931
+ if (!fs13.existsSync(dir)) {
3932
+ fs13.mkdirSync(dir, { recursive: true });
3804
3933
  }
3805
3934
  }
3806
3935
  function snapshotFilePath(timestamp) {
3807
- return path12.join(snapshotDir(), `${timestamp}.json`);
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
- fs12.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
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 (!fs12.existsSync(dir)) return [];
3965
+ if (!fs13.existsSync(dir)) return [];
3837
3966
  try {
3838
- const files = fs12.readdirSync(dir).filter((f) => f.endsWith(".json")).sort((a, b) => b.localeCompare(a));
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 = fs12.readFileSync(path12.join(dir, f), "utf-8");
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 path13 from "path";
4050
- import fs13 from "fs";
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 = path13.join(root, "tsconfig.json");
4089
- if (!fs13.existsSync(tsconfigPath)) {
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
- path13.join(projectRoot, "node_modules", ".bin", "typescript-language-server"),
4152
- path13.join(projectRoot, "..", "node_modules", ".bin", "typescript-language-server")
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 (fs13.existsSync(candidate)) {
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(path13.delimiter)) {
4162
- const binPath = path13.join(dir, "typescript-language-server");
4163
- if (fs13.existsSync(binPath)) {
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 = fs13.readFileSync(filePath, "utf-8");
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 fs14 from "fs";
4402
- import path14 from "path";
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 (!fs14.existsSync(resolvedPath)) {
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 = fs14.readFileSync(ref.filePath, "utf-8");
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: ${path14.relative(projectRoot, resolvedPath)}:${line + 1}:${character + 1}`,
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 = path14.relative(projectRoot, file);
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 (!fs14.existsSync(resolvedPath)) {
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 = path14.relative(projectRoot, definition.filePath);
4622
+ const relPath = path15.relative(projectRoot, definition.filePath);
4494
4623
  let lineContent = "";
4495
4624
  try {
4496
- const content = fs14.readFileSync(definition.filePath, "utf-8");
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 (!fs14.existsSync(resolvedPath)) {
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 = fs14.readFileSync(change.filePath, "utf-8");
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
- fs14.writeFileSync(change.filePath, lines2.join("\n"), "utf-8");
4700
+ fs15.writeFileSync(change.filePath, lines2.join("\n"), "utf-8");
4572
4701
  totalEdits += change.edits.length;
4573
4702
  fileChanges.push({
4574
- filePath: path14.relative(projectRoot, change.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 (!fs14.existsSync(resolvedPath)) {
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 = fs14.readFileSync(resolvedPath, "utf-8");
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 = path14.relative(projectRoot, resolvedPath);
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 = path14.relative(projectRoot, resolvedPath);
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 = fs14.readFileSync(filePath, "utf-8");
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 = fs14.readFileSync(file, "utf-8");
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 = path14.relative(projectRoot, file);
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 = fs14.readFileSync(file, "utf-8");
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 = path14.relative(projectRoot, file);
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 fs15 from "fs";
5147
- import path15 from "path";
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 = path15.resolve(root, "CLAUDE.md");
5540
- if (!fs15.existsSync(claudePath)) {
5690
+ const claudePath = path16.resolve(root, "CLAUDE.md");
5691
+ if (!fs16.existsSync(claudePath)) {
5541
5692
  try {
5542
- fs15.writeFileSync(claudePath, [
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 = path15.resolve(root, ".codex/config.toml");
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 = path15.dirname(tomlPath);
5563
- if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
5564
- if (fs15.existsSync(tomlPath)) {
5565
- const existingContent = fs15.readFileSync(tomlPath, "utf-8");
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
- fs15.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
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
- fs15.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
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 = path15.resolve(root, "settings.json");
5737
+ const settingsPath = path16.resolve(root, "settings.json");
5587
5738
  if (results.some((r) => r.filePath === "settings.json")) return;
5588
5739
  try {
5589
- if (fs15.existsSync(settingsPath)) {
5590
- const existingContent = fs15.readFileSync(settingsPath, "utf-8");
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
- fs15.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
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
- fs15.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
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
- fs15.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
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 = path15.resolve(root, ".agents/mcp_config.json");
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 = path15.dirname(mcpPath);
5659
- if (fs15.existsSync(mcpPath)) {
5660
- const existingContent = fs15.readFileSync(mcpPath, "utf-8");
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
- fs15.writeFileSync(mcpPath, updated, "utf-8");
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
- fs15.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
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 (!fs15.existsSync(mcpDir)) fs15.mkdirSync(mcpDir, { recursive: true });
5679
- fs15.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
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 = path15.resolve(root, ".aider.conf.yml");
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 (fs15.existsSync(ymlPath)) {
5697
- const existingContent = fs15.readFileSync(ymlPath, "utf-8");
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
- fs15.writeFileSync(ymlPath, newContent, "utf-8");
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
- fs15.writeFileSync(ymlPath, content, "utf-8");
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 = path15.resolve(root, ".github/skills/kuma/SKILL.md");
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 = path15.dirname(skillPath);
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 (fs15.existsSync(skillPath)) {
5741
- const existingContent = fs15.readFileSync(skillPath, "utf-8");
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
- fs15.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
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 (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
5750
- fs15.writeFileSync(skillPath, content, "utf-8");
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 = path15.resolve(root, ".agents/mcp_config.json");
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 = path15.dirname(mcpPath);
5767
- if (fs15.existsSync(mcpPath)) {
5768
- const existingContent = fs15.readFileSync(mcpPath, "utf-8");
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
- fs15.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
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 (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
5777
- fs15.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
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 = path15.resolve(root, ".codewhale/mcp.json");
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 = path15.dirname(mcpPath);
5794
- if (fs15.existsSync(mcpPath)) {
5795
- const existingContent = fs15.readFileSync(mcpPath, "utf-8");
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
- fs15.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
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 (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
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
- fs15.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
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 = path15.resolve(root, relativePath);
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 (fs15.existsSync(fullPath)) {
5841
- const existingContent = fs15.readFileSync(fullPath, "utf-8");
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
- fs15.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
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 = path15.dirname(fullPath);
5850
- if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
5851
- fs15.writeFileSync(fullPath, combinedContent, "utf-8");
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 (fs15.existsSync(fullPath)) {
5863
- const existingContent = fs15.readFileSync(fullPath, "utf-8");
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
- fs15.writeFileSync(fullPath, newContent, "utf-8");
6027
+ fs16.writeFileSync(fullPath, newContent, "utf-8");
5877
6028
  results.push({ type, filePath: relativePath, action: "appended" });
5878
6029
  } else {
5879
- const dir = path15.dirname(fullPath);
5880
- if (!fs15.existsSync(dir)) {
5881
- fs15.mkdirSync(dir, { recursive: true });
6030
+ const dir = path16.dirname(fullPath);
6031
+ if (!fs16.existsSync(dir)) {
6032
+ fs16.mkdirSync(dir, { recursive: true });
5882
6033
  }
5883
- fs15.writeFileSync(fullPath, template, "utf-8");
6034
+ fs16.writeFileSync(fullPath, template, "utf-8");
5884
6035
  results.push({ type, filePath: relativePath, action: "created" });
5885
6036
  }
5886
6037
  if (type === "antigravity") {