@plumpslabs/kuma 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +826 -731
  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,156 +1613,565 @@ function formatBatchResult(results, totalRequested) {
1594
1613
  return lines.join("\n");
1595
1614
  }
1596
1615
 
1597
- // src/utils/processRunner.ts
1598
- import { spawn } from "child_process";
1599
- var DEFAULT_MAX_STDOUT = 5e3;
1600
- var DEFAULT_MAX_STDERR = 2e3;
1601
- var DEFAULT_TIMEOUT = 60;
1602
- function spawnProcess(command, options) {
1603
- const {
1604
- cwd,
1605
- timeoutSeconds = DEFAULT_TIMEOUT,
1606
- useShell = false,
1607
- maxStdout = DEFAULT_MAX_STDOUT,
1608
- maxStderr = DEFAULT_MAX_STDERR
1609
- } = options;
1610
- return new Promise((resolve) => {
1611
- const parts = command.split(" ");
1612
- const cmd = parts[0];
1613
- const args = parts.slice(1);
1614
- const proc = spawn(cmd, args, {
1615
- cwd,
1616
- shell: useShell,
1617
- stdio: ["pipe", "pipe", "pipe"]
1618
- });
1619
- let stdout = "";
1620
- let stderr = "";
1621
- let timedOut = false;
1622
- const timeoutId = setTimeout(() => {
1623
- timedOut = true;
1624
- proc.kill("SIGTERM");
1625
- if (process.platform === "win32") {
1626
- try {
1627
- spawn("taskkill", ["/pid", String(proc.pid), "/f", "/t"], {
1628
- stdio: "ignore"
1629
- });
1630
- } catch {
1631
- }
1632
- }
1633
- }, timeoutSeconds * 1e3);
1634
- proc.stdout?.on("data", (data) => {
1635
- stdout += data.toString();
1636
- });
1637
- proc.stderr?.on("data", (data) => {
1638
- stderr += data.toString();
1639
- });
1640
- proc.on("close", (code) => {
1641
- clearTimeout(timeoutId);
1642
- resolve({
1643
- stdout: truncateOutput(stdout, maxStdout),
1644
- stderr: truncateOutput(stderr, maxStderr),
1645
- exitCode: code ?? -1,
1646
- timedOut
1647
- });
1648
- });
1649
- proc.on("error", () => {
1650
- clearTimeout(timeoutId);
1651
- resolve({
1652
- stdout,
1653
- stderr: `Failed to spawn process: ${cmd}`,
1654
- exitCode: -1,
1655
- timedOut: false
1656
- });
1657
- });
1658
- });
1659
- }
1660
- function spawnShell(command, options) {
1661
- return spawnProcess(command, { ...options, useShell: true });
1662
- }
1663
- function truncateOutput(output, maxChars) {
1664
- if (output.length <= maxChars) return output;
1665
- return output.slice(0, maxChars) + `
1666
-
1667
- [...truncated, ${output.length - maxChars} more characters]`;
1668
- }
1669
-
1670
1616
  // src/tools/safeTerminalExec.ts
1671
- 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()) {
1752
+ if (Array.isArray(pnpmWorkspace?.packages)) {
1753
+ for (const p of pnpmWorkspace.packages) if (typeof p === "string") patterns.add(p);
1754
+ }
1755
+ patterns.add("apps/*");
1756
+ patterns.add("packages/*");
1757
+ patterns.add("services/*");
1758
+ for (const pattern of patterns) {
1759
+ const match = pattern.match(/^([^*]+)\/\*$/);
1760
+ if (!match) continue;
1761
+ const dir = path7.join(root, match[1]);
1762
+ if (!fs7.existsSync(dir)) continue;
1763
+ let entries = [];
1764
+ try {
1765
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
1766
+ } catch {
1767
+ continue;
1768
+ }
1769
+ for (const entry of entries) {
1770
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
1771
+ const pkgPath = path7.join(dir, entry.name, "package.json");
1772
+ const subPkg = readJsonSafe(pkgPath);
1773
+ if (!subPkg) continue;
1774
+ const workspacePath = path7.join(dir, entry.name);
1775
+ results.push({
1776
+ path: path7.relative(root, workspacePath),
1777
+ name: subPkg.name ?? entry.name,
1778
+ framework: detectFramework(workspacePath),
1779
+ packageManager: detectPackageManagerForDir(workspacePath)
1780
+ });
1781
+ }
1782
+ }
1783
+ const seen = /* @__PURE__ */ new Set();
1784
+ return results.filter((w) => {
1785
+ if (seen.has(w.path)) return false;
1786
+ seen.add(w.path);
1787
+ return true;
1788
+ });
1789
+ }
1790
+ function readYamlLite(filePath) {
1791
+ try {
1792
+ if (!fs7.existsSync(filePath)) return null;
1793
+ const text = fs7.readFileSync(filePath, "utf-8");
1794
+ const packages = [];
1795
+ let inPackages = false;
1796
+ for (const rawLine of text.split("\n")) {
1797
+ const line = rawLine.replace(/#.*$/, "").trimEnd();
1798
+ if (/^packages:\s*$/.test(line)) {
1799
+ inPackages = true;
1800
+ continue;
1801
+ }
1802
+ if (inPackages) {
1803
+ const m = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/);
1804
+ if (m) packages.push(m[1]);
1805
+ else if (/^\S/.test(line)) inPackages = false;
1806
+ }
1807
+ }
1808
+ return { packages };
1809
+ } catch {
1810
+ return null;
1811
+ }
1812
+ }
1813
+ function detectTestRunner(root) {
1814
+ const pkg = readJsonSafe(path7.join(root, "package.json"));
1815
+ if (!pkg) return "unknown";
1816
+ const pkgDeps2 = pkg.dependencies ?? {};
1817
+ const pkgDevDeps2 = pkg.devDependencies ?? {};
1818
+ const deps = { ...pkgDeps2, ...pkgDevDeps2 };
1819
+ if (deps.vitest) return "Vitest";
1820
+ if (deps.jest) return "Jest";
1821
+ if (deps.mocha) return "Mocha";
1822
+ if (deps.ava) return "AVA";
1823
+ if (deps.cypress) return "Cypress";
1824
+ if (deps.playwright) return "Playwright";
1825
+ if (deps["@testing-library/react"]) return "React Testing Library";
1826
+ const scripts = pkg.scripts ?? {};
1827
+ if (scripts.test) {
1828
+ if (scripts.test.includes("vitest")) return "Vitest";
1829
+ if (scripts.test.includes("jest")) return "Jest";
1830
+ if (scripts.test.includes("mocha")) return "Mocha";
1831
+ }
1832
+ return "unknown";
1833
+ }
1834
+ function detectStyling(root) {
1835
+ const pkg = readJsonSafe(path7.join(root, "package.json"));
1836
+ if (!pkg) return "unknown";
1837
+ const pkgDeps3 = pkg.dependencies ?? {};
1838
+ const pkgDevDeps3 = pkg.devDependencies ?? {};
1839
+ const deps = { ...pkgDeps3, ...pkgDevDeps3 };
1840
+ if (deps.tailwindcss) return "Tailwind CSS";
1841
+ if (deps["styled-components"]) return "Styled Components";
1842
+ if (deps["@emotion/react"]) return "Emotion";
1843
+ if (deps.linaria) return "Linaria";
1844
+ if (deps.sass || deps["node-sass"]) return "SCSS";
1845
+ if (deps.less) return "Less";
1846
+ if (deps["@vanilla-extract/css"]) return "Vanilla Extract";
1847
+ if (deps.cssmodules || hasCSSModules(root)) return "CSS Modules";
1848
+ const srcDir = path7.join(root, "src");
1849
+ if (fs7.existsSync(srcDir)) {
1850
+ const cssFiles = findFiles(srcDir, [".css", ".scss", ".less"]);
1851
+ if (cssFiles.length > 0) return "Plain CSS/SCSS";
1852
+ }
1853
+ return "unknown";
1854
+ }
1855
+ function detectImportAlias(root) {
1856
+ const tsconfig = readJsonSafe(path7.join(root, "tsconfig.json"));
1857
+ const tsconfigPaths = tsconfig?.compilerOptions;
1858
+ if (tsconfigPaths?.paths) {
1859
+ const paths = tsconfigPaths.paths;
1860
+ const alias = Object.keys(paths)[0];
1861
+ if (alias) return alias.replace("/*", "");
1862
+ }
1863
+ const jsconfig = readJsonSafe(path7.join(root, "jsconfig.json"));
1864
+ const jsconfigPaths = jsconfig?.compilerOptions;
1865
+ if (jsconfigPaths?.paths) {
1866
+ const paths = jsconfigPaths.paths;
1867
+ const alias = Object.keys(paths)[0];
1868
+ if (alias) return alias.replace("/*", "");
1869
+ }
1870
+ return void 0;
1871
+ }
1872
+ function detectLintRules(root) {
1873
+ const rules = [];
1874
+ if (fs7.existsSync(path7.join(root, ".eslintrc"))) rules.push("ESLint (.eslintrc)");
1875
+ if (fs7.existsSync(path7.join(root, ".eslintrc.js"))) rules.push("ESLint (.eslintrc.js)");
1876
+ if (fs7.existsSync(path7.join(root, ".eslintrc.json"))) rules.push("ESLint (.eslintrc.json)");
1877
+ if (fs7.existsSync(path7.join(root, "eslint.config.js"))) rules.push("ESLint (flat config)");
1878
+ if (fs7.existsSync(path7.join(root, ".prettierrc"))) rules.push("Prettier");
1879
+ if (fs7.existsSync(path7.join(root, ".prettierrc.json"))) rules.push("Prettier");
1880
+ if (fs7.existsSync(path7.join(root, ".prettierrc.js"))) rules.push("Prettier");
1881
+ if (fs7.existsSync(path7.join(root, ".stylelintrc"))) rules.push("StyleLint");
1882
+ if (fs7.existsSync(path7.join(root, ".stylelintrc.json"))) rules.push("StyleLint");
1883
+ return rules;
1884
+ }
1885
+ function detectPackageManager() {
1886
+ const root = getProjectRoot();
1887
+ return detectPackageManagerForDir(root);
1888
+ }
1889
+ function detectPackageManagerForDir(dir) {
1890
+ const root = getProjectRoot();
1891
+ let currentDir = path7.resolve(dir);
1892
+ while (currentDir.startsWith(root)) {
1893
+ if (fs7.existsSync(path7.join(currentDir, "pnpm-lock.yaml"))) return "pnpm";
1894
+ if (fs7.existsSync(path7.join(currentDir, "yarn.lock"))) return "yarn";
1895
+ if (fs7.existsSync(path7.join(currentDir, "package-lock.json"))) return "npm";
1896
+ if (fs7.existsSync(path7.join(currentDir, "bun.lockb"))) return "bun";
1897
+ if (currentDir === root) break;
1898
+ currentDir = path7.dirname(currentDir);
1899
+ }
1900
+ return "npm";
1901
+ }
1902
+ function detectModuleSystem(root) {
1903
+ const pkg = readJsonSafe(path7.join(root, "package.json"));
1904
+ if (pkg?.type === "module") return "esm";
1905
+ const srcDir = path7.join(root, "src");
1906
+ if (fs7.existsSync(srcDir)) {
1907
+ const tsFiles = findFiles(srcDir, [".ts", ".tsx", ".js", ".jsx", ".mjs"]);
1908
+ for (const file of tsFiles.slice(0, 20)) {
1909
+ try {
1910
+ const content = fs7.readFileSync(file, "utf-8");
1911
+ if (content.includes("import ") || content.includes("export ")) return "esm";
1912
+ } catch {
1913
+ continue;
1914
+ }
1915
+ }
1916
+ }
1917
+ return "cjs";
1918
+ }
1919
+ function detectLanguage(root) {
1920
+ if (fs7.existsSync(path7.join(root, "tsconfig.json"))) return "typescript";
1921
+ const srcDir = path7.join(root, "src");
1922
+ if (fs7.existsSync(srcDir)) {
1923
+ const tsFiles = findFiles(srcDir, [".ts", ".tsx"]);
1924
+ if (tsFiles.length > 0) return "typescript";
1925
+ }
1926
+ return "javascript";
1927
+ }
1928
+ function detectFeatures(root) {
1929
+ const features = [];
1930
+ const pkg = readJsonSafe(path7.join(root, "package.json"));
1931
+ if (!pkg) return features;
1932
+ const pkgDeps4 = pkg.dependencies ?? {};
1933
+ const pkgDevDeps4 = pkg.devDependencies ?? {};
1934
+ const deps = { ...pkgDeps4, ...pkgDevDeps4 };
1935
+ if (deps["@prisma/client"] || deps.prisma) features.push("Prisma ORM");
1936
+ if (deps.typeorm) features.push("TypeORM");
1937
+ if (deps["drizzle-orm"]) features.push("Drizzle ORM");
1938
+ if (deps.graphql || deps["@graphql-tools"]) features.push("GraphQL");
1939
+ if (deps.trpc || deps["@trpc/client"]) features.push("tRPC");
1940
+ if (deps["socket.io"] || deps.ws) features.push("WebSockets");
1941
+ if (deps["@reduxjs/toolkit"] || deps.redux) features.push("Redux");
1942
+ if (deps.zustand) features.push("Zustand");
1943
+ if (deps["react-router"] || deps["react-router-dom"]) features.push("React Router");
1944
+ if (deps["next-auth"] || deps["next-auth/react"]) features.push("NextAuth");
1945
+ if (deps["@tanstack/react-query"]) features.push("React Query");
1946
+ if (deps.jest || deps.vitest) features.push("Unit Testing");
1947
+ if (deps.cypress || deps.playwright) features.push("E2E Testing");
1948
+ if (deps.storybook || deps["@storybook/react"]) features.push("Storybook");
1949
+ if (deps.i18next || deps["react-i18next"]) features.push("i18n");
1950
+ return features;
1951
+ }
1952
+ function readJsonSafe(filePath) {
1953
+ try {
1954
+ if (fs7.existsSync(filePath)) {
1955
+ return JSON.parse(fs7.readFileSync(filePath, "utf-8"));
1956
+ }
1957
+ } catch {
1958
+ }
1959
+ return null;
1960
+ }
1961
+ function hasCSSModules(root) {
1962
+ const srcDir = path7.join(root, "src");
1963
+ if (!fs7.existsSync(srcDir)) return false;
1964
+ const files = findFiles(srcDir, [".module.css", ".module.scss", ".module.less"]);
1965
+ return files.length > 0;
1966
+ }
1967
+ function findFiles(dir, extensions) {
1968
+ const results = [];
1969
+ try {
1970
+ const entries = fs7.readdirSync(dir, { withFileTypes: true });
1971
+ for (const entry of entries) {
1972
+ const fullPath = path7.join(dir, entry.name);
1973
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
1974
+ results.push(...findFiles(fullPath, extensions));
1975
+ } else if (entry.isFile()) {
1976
+ if (extensions.some((ext) => entry.name.endsWith(ext))) {
1977
+ results.push(fullPath);
1978
+ }
1979
+ }
1980
+ }
1981
+ } catch {
1982
+ }
1983
+ return results;
1984
+ }
1985
+
1986
+ // src/utils/processRunner.ts
1987
+ import { spawn } from "child_process";
1988
+ var DEFAULT_MAX_STDOUT = 5e3;
1989
+ var DEFAULT_MAX_STDERR = 2e3;
1990
+ var DEFAULT_TIMEOUT = 60;
1991
+ function spawnProcess(command, options) {
1992
+ const {
1993
+ cwd,
1994
+ timeoutSeconds = DEFAULT_TIMEOUT,
1995
+ useShell = false,
1996
+ maxStdout = DEFAULT_MAX_STDOUT,
1997
+ maxStderr = DEFAULT_MAX_STDERR
1998
+ } = options;
1999
+ return new Promise((resolve) => {
2000
+ const parts = command.split(" ");
2001
+ const cmd = parts[0];
2002
+ const args = parts.slice(1);
2003
+ const proc = spawn(cmd, args, {
2004
+ cwd,
2005
+ shell: useShell,
2006
+ stdio: ["pipe", "pipe", "pipe"]
2007
+ });
2008
+ let stdout = "";
2009
+ let stderr = "";
2010
+ let timedOut = false;
2011
+ const timeoutId = setTimeout(() => {
2012
+ timedOut = true;
2013
+ proc.kill("SIGTERM");
2014
+ if (process.platform === "win32") {
2015
+ try {
2016
+ spawn("taskkill", ["/pid", String(proc.pid), "/f", "/t"], {
2017
+ stdio: "ignore"
2018
+ });
2019
+ } catch {
2020
+ }
2021
+ }
2022
+ }, timeoutSeconds * 1e3);
2023
+ proc.stdout?.on("data", (data) => {
2024
+ stdout += data.toString();
2025
+ });
2026
+ proc.stderr?.on("data", (data) => {
2027
+ stderr += data.toString();
2028
+ });
2029
+ proc.on("close", (code) => {
2030
+ clearTimeout(timeoutId);
2031
+ resolve({
2032
+ stdout: truncateOutput(stdout, maxStdout),
2033
+ stderr: truncateOutput(stderr, maxStderr),
2034
+ exitCode: code ?? -1,
2035
+ timedOut
2036
+ });
2037
+ });
2038
+ proc.on("error", () => {
2039
+ clearTimeout(timeoutId);
2040
+ resolve({
2041
+ stdout,
2042
+ stderr: `Failed to spawn process: ${cmd}`,
2043
+ exitCode: -1,
2044
+ timedOut: false
2045
+ });
2046
+ });
2047
+ });
2048
+ }
2049
+ function spawnShell(command, options) {
2050
+ return spawnProcess(command, { ...options, useShell: true });
2051
+ }
2052
+ function truncateOutput(output, maxChars) {
2053
+ if (output.length <= maxChars) return output;
2054
+ return output.slice(0, maxChars) + `
2055
+
2056
+ [...truncated, ${output.length - maxChars} more characters]`;
2057
+ }
2058
+
2059
+ // src/tools/safeTerminalExec.ts
2060
+ function buildTaskCommand(task, cwd) {
2061
+ const pm = detectPackageManagerForDir(cwd);
2062
+ const prefix = pm === "pnpm" ? "pnpm" : pm === "yarn" ? "yarn" : pm === "bun" ? "bun" : "npm";
2063
+ const npx = pm === "pnpm" ? "pnpm" : pm === "bun" ? "bunx" : "npx";
2064
+ switch (task) {
2065
+ case "test":
2066
+ return `${prefix} test`;
2067
+ case "build":
2068
+ return `${prefix} run build`;
2069
+ case "lint":
2070
+ return `${prefix} run lint`;
2071
+ case "typecheck":
2072
+ return `${npx} tsc --noEmit`;
2073
+ default:
2074
+ return `${prefix} test`;
2075
+ }
2076
+ }
2077
+ var DANGEROUS_PATTERNS = [
2078
+ "rm -rf",
2079
+ "rm -fr",
2080
+ "del /f",
2081
+ "rd /s",
2082
+ "rmdir /s",
2083
+ "git push",
2084
+ "git commit",
2085
+ "npm publish",
2086
+ "npx publish",
2087
+ "yarn publish",
2088
+ "pnpm publish",
2089
+ "> /dev/sda",
2090
+ "format",
2091
+ "mkfs",
2092
+ "dd if=",
2093
+ ":(){ :|:& };:",
2094
+ "curl ",
2095
+ "wget "
2096
+ ];
2097
+ async function handleSafeTerminalExec(params) {
2098
+ const { task, customCommand, timeout = 60, cwd: inputCwd, workspace } = params;
2099
+ if (task === "custom" && !customCommand) {
2100
+ return "Error: Task 'custom' requires the 'customCommand' parameter.";
2101
+ }
2102
+ const projectRoot = getProjectRoot();
2103
+ let workingDir = projectRoot;
2104
+ let resolvedFrom = "root";
2105
+ if (workspace) {
2106
+ const conventions = sessionMemory.getConventions();
2107
+ const workspaces = conventions?.workspaces;
2108
+ const matched = workspaces?.find((w) => w.name === workspace || w.path === workspace);
2109
+ if (matched) {
2110
+ workingDir = path8.resolve(projectRoot, matched.path);
2111
+ resolvedFrom = `workspace "${matched.name}" \u2192 ${matched.path}`;
2112
+ } else {
2113
+ return `\u26A0\uFE0F Workspace "${workspace}" not found. Run project_conventions first to detect workspaces, or use 'cwd' parameter with a direct path.
2114
+
2115
+ Available workspaces: ${workspaces?.map((w) => `"${w.name}" (${w.path})`).join(", ") || "none detected"}`;
2116
+ }
2117
+ } else if (inputCwd) {
2118
+ const resolved = path8.resolve(projectRoot, inputCwd);
2119
+ const normalizedResolved = path8.normalize(resolved).toLowerCase();
2120
+ const normalizedRoot = path8.normalize(projectRoot).toLowerCase();
2121
+ if (!normalizedResolved.startsWith(normalizedRoot)) {
2122
+ return `\u{1F6AB} BLOCKED: Path "${inputCwd}" resolves outside project root "${projectRoot}".`;
2123
+ }
2124
+ if (!fs8.existsSync(resolved)) {
2125
+ return `\u26A0\uFE0F Directory "${inputCwd}" does not exist at "${resolved}".`;
2126
+ }
2127
+ workingDir = resolved;
2128
+ resolvedFrom = `cwd: ${inputCwd}`;
2129
+ }
2130
+ const command = task === "custom" ? customCommand : buildTaskCommand(task, workingDir);
2131
+ const cbResult = circuitBreaker.check("safe_terminal_exec", { task, command });
2132
+ if (!cbResult.allowed) {
2133
+ return `\u26A0\uFE0F Circuit breaker: ${cbResult.reason}
2134
+
2135
+ Fix the code first before running the task again.`;
2136
+ }
2137
+ const dangerousPattern = DANGEROUS_PATTERNS.find((p) => command.toLowerCase().includes(p.toLowerCase()));
2138
+ if (dangerousPattern) {
2139
+ return `\u{1F6AB} BLOCKED: Command contains a dangerous pattern: "${dangerousPattern}".
2140
+ This command is not permitted.`;
2141
+ }
2142
+ try {
2143
+ sessionMemory.recordToolCall("execute_safe_test", { task, command, cwd: workingDir });
2144
+ const result = await spawnShell(command, {
2145
+ cwd: workingDir,
2146
+ timeoutSeconds: timeout
2147
+ });
2148
+ const output = formatExecResult(result, command, task, resolvedFrom);
2149
+ if (result.exitCode !== 0) {
2150
+ sessionMemory.addFailedFile(task, result.stderr || result.stdout);
2151
+ }
2152
+ return output;
2153
+ } catch (err) {
2154
+ const errorMsg = err instanceof Error ? err.message : String(err);
2155
+ if (errorMsg.toLowerCase().includes("timeout")) {
2156
+ return formatTimeoutResult(command, timeout);
2157
+ }
2158
+ return `Error running "${command}": ${errorMsg}`;
2159
+ }
2160
+ }
2161
+ function formatExecResult(result, command, task, resolvedFrom) {
2162
+ const status = result.exitCode === 0 ? "\u2705 PASS" : "\u274C FAIL";
2163
+ const lines = [
2164
+ `\u{1F4BB} ${status} \u2014 Task: ${task}`,
2165
+ `$ ${command}`,
2166
+ ...resolvedFrom ? [`\u{1F4CD} ${resolvedFrom}`] : [],
2167
+ `Exit code: ${result.exitCode}`,
2168
+ `Duration: ${result.timedOut ? "TIMEOUT" : "completed"}`,
2169
+ ""
2170
+ ];
2171
+ if (result.stdout.trim()) {
2172
+ lines.push("\u{1F4E4} STDOUT:", "```", result.stdout, "```", "");
2173
+ }
2174
+ if (result.stderr.trim()) {
1747
2175
  lines.push("\u{1F4E4} STDERR:", "```", result.stderr, "```", "");
1748
2176
  }
1749
2177
  if (result.exitCode !== 0) {
@@ -1776,8 +2204,8 @@ function formatTimeoutResult(command, timeout) {
1776
2204
  }
1777
2205
 
1778
2206
  // src/agents/codeReviewer.ts
1779
- import fs7 from "fs";
1780
- import path7 from "path";
2207
+ import fs9 from "fs";
2208
+ import path9 from "path";
1781
2209
  import { execSync } from "child_process";
1782
2210
  var CODE_EXTENSIONS = [
1783
2211
  ".ts",
@@ -1809,7 +2237,7 @@ function getGitChangedFiles() {
1809
2237
  if (filePath.startsWith('"') && filePath.endsWith('"')) {
1810
2238
  filePath = filePath.substring(1, filePath.length - 1);
1811
2239
  }
1812
- const ext = path7.extname(filePath).toLowerCase();
2240
+ const ext = path9.extname(filePath).toLowerCase();
1813
2241
  if (CODE_EXTENSIONS.includes(ext)) files.push(filePath);
1814
2242
  }
1815
2243
  return files.slice(0, 10);
@@ -1846,7 +2274,7 @@ Pass an explicit 'files' array to review specific files.`;
1846
2274
  continue;
1847
2275
  }
1848
2276
  const resolvedPath = validation.resolvedPath;
1849
- if (!fs7.existsSync(resolvedPath)) {
2277
+ if (!fs9.existsSync(resolvedPath)) {
1850
2278
  allIssues.push({
1851
2279
  file: filePath,
1852
2280
  line: 0,
@@ -1858,7 +2286,7 @@ Pass an explicit 'files' array to review specific files.`;
1858
2286
  continue;
1859
2287
  }
1860
2288
  try {
1861
- const content = fs7.readFileSync(resolvedPath, "utf-8");
2289
+ const content = fs9.readFileSync(resolvedPath, "utf-8");
1862
2290
  filesReviewed++;
1863
2291
  checkGeneral(filePath, content, allIssues);
1864
2292
  switch (focus) {
@@ -1952,7 +2380,7 @@ function isTestFile(filePath) {
1952
2380
  return /\.(test|spec)\.[a-z]+$/i.test(filePath) || /__tests__/.test(filePath);
1953
2381
  }
1954
2382
  function isTsLike(filePath) {
1955
- const ext = path7.extname(filePath).toLowerCase();
2383
+ const ext = path9.extname(filePath).toLowerCase();
1956
2384
  return ext === ".ts" || ext === ".tsx";
1957
2385
  }
1958
2386
  function checkGeneral(filePath, content, issues) {
@@ -2391,460 +2819,105 @@ function checkOverEngineering(filePath, content, issues) {
2391
2819
  line: lineNum,
2392
2820
  severity: "info",
2393
2821
  rule: sp.rule,
2394
- message: sp.msg,
2395
- suggestion: sp.suggestion
2396
- });
2397
- }
2398
- }
2399
- const exportDecls = [];
2400
- for (let i = 0; i < lines.length; i++) {
2401
- const ed = lines[i].match(/^\s*export\s+(const|let|var|function|class)\s+(\w+)/);
2402
- if (ed) exportDecls.push({ name: ed[2], line: i + 1 });
2403
- }
2404
- for (const decl of exportDecls) {
2405
- const refCount = (text.match(new RegExp(`\\b${decl.name}\\b`, "g")) || []).length;
2406
- const isReactComponent = /^[A-Z]/.test(decl.name) && /return\s+<|React\./.test(text);
2407
- if (refCount <= 1 && decl.name !== "default" && !isReactComponent) {
2408
- issues.push({
2409
- file: filePath,
2410
- line: decl.line,
2411
- severity: "info",
2412
- rule: "over-engineering/unused-export",
2413
- message: `"${decl.name}" is exported but referenced only in its declaration`,
2414
- suggestion: "Check if it is imported elsewhere; if not, remove or inline"
2415
- });
2416
- }
2417
- }
2418
- }
2419
- function findBlockBodies(lines, startPattern) {
2420
- const results = [];
2421
- for (let i = 0; i < lines.length; i++) {
2422
- const m = lines[i].match(startPattern);
2423
- if (!m) continue;
2424
- const name = m[0].match(/class\s+(\w+)/)?.[1] ?? "Unknown";
2425
- const body = collectBlockLines(lines, i);
2426
- results.push({ body: body.join("\n"), nameLine: i + 1, name });
2427
- }
2428
- return results;
2429
- }
2430
- function collectBlockLines(lines, startIdx) {
2431
- let depth = 0;
2432
- let started = false;
2433
- const block = [];
2434
- for (let j = startIdx; j < lines.length; j++) {
2435
- const line = lines[j];
2436
- block.push(line);
2437
- for (const ch of line) {
2438
- if (ch === "{") {
2439
- depth++;
2440
- started = true;
2441
- } else if (ch === "}") depth--;
2442
- }
2443
- if (started && depth <= 0) break;
2444
- }
2445
- return block;
2446
- }
2447
- function extractBodyAfter(lines, match) {
2448
- const startIdx = lines.findIndex((l) => l.includes(match[0].trim().split(/\s+/).pop() ?? ""));
2449
- if (startIdx < 0) return "";
2450
- return collectBlockLines(lines, startIdx).join("\n");
2451
- }
2452
- function formatReviewOutput(issues, filesReviewed, totalFiles, focus, customCriteria, autoDetected) {
2453
- const errors = issues.filter((i) => i.severity === "error");
2454
- const warnings = issues.filter((i) => i.severity === "warning");
2455
- const infos = issues.filter((i) => i.severity === "info");
2456
- const lines = [
2457
- `\u{1F50D} **Code Review \u2014 Focus: ${focus}**`,
2458
- ...focus === "over-engineering" ? ["\u{1F4A1} **The Ladder:** 1. Does this code need to exist? 2. Does stdlib cover it? 3. Is there a one-liner? 4. Only then, write it."] : [],
2459
- ...autoDetected ? [`\u{1F4C2} Auto-detected ${totalFiles} changed file(s) from git`] : [],
2460
- ...customCriteria ? [`\u{1F4CB} **Custom Criteria:** ${customCriteria}`] : [],
2461
- `\u{1F4C1} ${filesReviewed}/${totalFiles} files reviewed`,
2462
- `\u{1F534} ${errors.length} errors | \u{1F7E1} ${warnings.length} warnings | \u{1F535} ${infos.length} info`,
2463
- ""
2464
- ];
2465
- if (issues.length === 0) {
2466
- lines.push("\u2705 No issues found.");
2467
- return lines.join("\n");
2468
- }
2469
- const grouped = /* @__PURE__ */ new Map();
2470
- for (const issue of issues) {
2471
- const existing = grouped.get(issue.file) ?? [];
2472
- existing.push(issue);
2473
- grouped.set(issue.file, existing);
2474
- }
2475
- for (const [file, fileIssues] of grouped) {
2476
- lines.push(`**\u{1F4C4} ${file}:**`);
2477
- for (const issue of fileIssues) {
2478
- const icon = issue.severity === "error" ? "\u{1F534}" : issue.severity === "warning" ? "\u{1F7E1}" : "\u{1F535}";
2479
- lines.push(` ${icon} [L${issue.line}] [${issue.rule}] ${issue.message}`);
2480
- if (issue.suggestion) lines.push(` \u{1F4A1} ${issue.suggestion}`);
2481
- }
2482
- lines.push("");
2483
- }
2484
- if (errors.length > 0) {
2485
- lines.push("\u26A0\uFE0F Fix errors first, then warnings.");
2486
- lines.push("\u{1F4A1} Use precise_diff_editor to apply fixes.");
2487
- } else if (warnings.length > 0) {
2488
- lines.push("\u{1F4A1} Address warnings before merging.");
2489
- } else {
2490
- lines.push("\u2705 Only informational issues \u2014 ready for testing.");
2491
- }
2492
- return lines.join("\n");
2493
- }
2494
-
2495
- // src/utils/conventionsDetector.ts
2496
- import fs8 from "fs";
2497
- import path8 from "path";
2498
- var cachedConventions = null;
2499
- async function detectConventions(forceRescan = false) {
2500
- if (cachedConventions && !forceRescan) {
2501
- return cachedConventions;
2502
- }
2503
- const projectRoot = getProjectRoot();
2504
- const workspaces = detectWorkspaces(projectRoot);
2505
- const conventions = {
2506
- framework: detectFramework(projectRoot),
2507
- projectType: detectProjectType(projectRoot),
2508
- testRunner: detectTestRunner(projectRoot),
2509
- styling: detectStyling(projectRoot),
2510
- importAlias: detectImportAlias(projectRoot),
2511
- lintRules: detectLintRules(projectRoot),
2512
- packageManager: detectPackageManager(),
2513
- moduleSystem: detectModuleSystem(projectRoot),
2514
- language: detectLanguage(projectRoot),
2515
- features: detectFeatures(projectRoot),
2516
- isMonorepo: workspaces.length > 0,
2517
- workspaces
2518
- };
2519
- cachedConventions = conventions;
2520
- return conventions;
2521
- }
2522
- function detectFramework(root) {
2523
- const pkg = readJsonSafe(path8.join(root, "package.json"));
2524
- if (pkg) {
2525
- const pkgDeps = pkg.dependencies ?? {};
2526
- const pkgDevDeps = pkg.devDependencies ?? {};
2527
- const deps = { ...pkgDeps, ...pkgDevDeps };
2528
- if (deps.next) return "Next.js";
2529
- if (deps["@remix-run/react"]) return "Remix";
2530
- if (deps.gatsby) return "Gatsby";
2531
- if (deps.nuxt || deps["@nuxt/core"]) return "Nuxt.js";
2532
- if (deps.vue) return "Vue";
2533
- if (deps.react) return "React";
2534
- if (deps.svelte) return "Svelte";
2535
- if (deps.astro) return "Astro";
2536
- if (deps.solid || deps["solid-js"]) return "SolidJS";
2537
- if (deps.qwik || deps["@builder.io/qwik"]) return "Qwik";
2538
- if (deps.vite) return "Vite";
2539
- if (deps["@nestjs/core"]) return "NestJS";
2540
- if (deps.fastify) return "Fastify";
2541
- if (deps.express) return "Express";
2542
- if (deps.koa) return "Koa";
2543
- if (deps.hono) return "Hono";
2544
- if (deps["@hapi/hapi"]) return "Hapi";
2545
- if (deps["@modelcontextprotocol/sdk"]) return "MCP Server";
2546
- if (deps["@anthropic-ai/claude-agent-sdk"]) return "Claude Agent SDK";
2547
- if (deps.commander) return "Commander CLI";
2548
- if (deps.yargs) return "Yargs CLI";
2549
- if (deps["@oclif/core"] || deps.oclif) return "Oclif CLI";
2550
- if (deps.ink) return "Ink (React CLI)";
2551
- if (pkg.bin) return "CLI Tool";
2552
- if (pkg.main || pkg.exports) return "Library";
2553
- }
2554
- const subFrameworks = scanSubProjectFrameworks(root);
2555
- if (subFrameworks.length > 0) {
2556
- const freq = /* @__PURE__ */ new Map();
2557
- for (const fw of subFrameworks) {
2558
- freq.set(fw, (freq.get(fw) ?? 0) + 1);
2559
- }
2560
- const [topFw] = [...freq.entries()].sort((a, b) => b[1] - a[1])[0] ?? [];
2561
- if (topFw) return `${topFw} (monorepo)`;
2562
- }
2563
- return "unknown";
2564
- }
2565
- function scanSubProjectFrameworks(root) {
2566
- const frameworks = [];
2567
- try {
2568
- const entries = fs8.readdirSync(root, { withFileTypes: true });
2569
- for (const entry of entries) {
2570
- if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
2571
- const subDir = path8.join(root, entry.name);
2572
- const subPkg = readJsonSafe(path8.join(subDir, "package.json"));
2573
- if (!subPkg) continue;
2574
- const subDeps = { ...subPkg.dependencies ?? {}, ...subPkg.devDependencies ?? {} };
2575
- if (subDeps.next) frameworks.push("Next.js");
2576
- else if (subDeps.react) frameworks.push("React");
2577
- else if (subDeps.vue) frameworks.push("Vue");
2578
- else if (subDeps.svelte) frameworks.push("Svelte");
2579
- else if (subDeps.astro) frameworks.push("Astro");
2580
- else if (subDeps["@nestjs/core"]) frameworks.push("NestJS");
2581
- else if (subDeps.express) frameworks.push("Express");
2582
- else if (subDeps.hono) frameworks.push("Hono");
2583
- else if (subDeps.vite) frameworks.push("Vite");
2584
- else if (subDeps["@modelcontextprotocol/sdk"]) frameworks.push("MCP Server");
2585
- }
2586
- } catch {
2587
- }
2588
- return frameworks;
2589
- }
2590
- function detectProjectType(root) {
2591
- const pkg = readJsonSafe(path8.join(root, "package.json"));
2592
- if (pkg) {
2593
- const pkgDeps = pkg.dependencies ?? {};
2594
- const pkgDevDeps = pkg.devDependencies ?? {};
2595
- const deps = { ...pkgDeps, ...pkgDevDeps };
2596
- if (deps["@modelcontextprotocol/sdk"]) return "mcp-server";
2597
- if (deps.next || deps.react || deps.vue || deps.svelte || deps.astro || deps.gatsby || deps.nuxt || deps["@remix-run/react"] || deps.solid) {
2598
- return "web-app";
2599
- }
2600
- if (deps.express || deps.fastify || deps.koa || deps.hono || deps["@nestjs/core"] || deps["@hapi/hapi"]) {
2601
- return "backend";
2602
- }
2603
- if (deps.commander || deps.yargs || deps["@oclif/core"] || deps.oclif || deps.ink) {
2604
- return "cli";
2605
- }
2606
- if (pkg.bin) return "cli";
2607
- if (pkg.main || pkg.exports) return "library";
2608
- }
2609
- const subFrameworks = scanSubProjectFrameworks(root);
2610
- if (subFrameworks.length > 0) return "web-app";
2611
- return "unknown";
2612
- }
2613
- function detectWorkspaces(root) {
2614
- const results = [];
2615
- const pkg = readJsonSafe(path8.join(root, "package.json"));
2616
- const pnpmWorkspace = readYamlLite(path8.join(root, "pnpm-workspace.yaml"));
2617
- const patterns = /* @__PURE__ */ new Set();
2618
- const pkgWorkspaces = pkg?.workspaces;
2619
- if (Array.isArray(pkgWorkspaces)) {
2620
- for (const p of pkgWorkspaces) if (typeof p === "string") patterns.add(p);
2621
- } else if (pkgWorkspaces && typeof pkgWorkspaces === "object") {
2622
- const packages = pkgWorkspaces.packages;
2623
- if (Array.isArray(packages)) {
2624
- for (const p of packages) if (typeof p === "string") patterns.add(p);
2625
- }
2626
- }
2627
- if (Array.isArray(pnpmWorkspace?.packages)) {
2628
- for (const p of pnpmWorkspace.packages) if (typeof p === "string") patterns.add(p);
2629
- }
2630
- patterns.add("apps/*");
2631
- patterns.add("packages/*");
2632
- patterns.add("services/*");
2633
- for (const pattern of patterns) {
2634
- const match = pattern.match(/^([^*]+)\/\*$/);
2635
- if (!match) continue;
2636
- const dir = path8.join(root, match[1]);
2637
- if (!fs8.existsSync(dir)) continue;
2638
- let entries = [];
2639
- try {
2640
- entries = fs8.readdirSync(dir, { withFileTypes: true });
2641
- } catch {
2642
- continue;
2643
- }
2644
- for (const entry of entries) {
2645
- if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
2646
- const pkgPath = path8.join(dir, entry.name, "package.json");
2647
- const subPkg = readJsonSafe(pkgPath);
2648
- if (!subPkg) continue;
2649
- results.push({
2650
- path: path8.relative(root, path8.join(dir, entry.name)),
2651
- name: subPkg.name ?? entry.name,
2652
- framework: detectFramework(path8.join(dir, entry.name))
2822
+ message: sp.msg,
2823
+ suggestion: sp.suggestion
2653
2824
  });
2654
2825
  }
2655
2826
  }
2656
- const 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;
2827
+ const exportDecls = [];
2828
+ for (let i = 0; i < lines.length; i++) {
2829
+ const ed = lines[i].match(/^\s*export\s+(const|let|var|function|class)\s+(\w+)/);
2830
+ if (ed) exportDecls.push({ name: ed[2], line: i + 1 });
2684
2831
  }
2685
- }
2686
- 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";
2832
+ for (const decl of exportDecls) {
2833
+ const refCount = (text.match(new RegExp(`\\b${decl.name}\\b`, "g")) || []).length;
2834
+ const isReactComponent = /^[A-Z]/.test(decl.name) && /return\s+<|React\./.test(text);
2835
+ if (refCount <= 1 && decl.name !== "default" && !isReactComponent) {
2836
+ issues.push({
2837
+ file: filePath,
2838
+ line: decl.line,
2839
+ severity: "info",
2840
+ rule: "over-engineering/unused-export",
2841
+ message: `"${decl.name}" is exported but referenced only in its declaration`,
2842
+ suggestion: "Check if it is imported elsewhere; if not, remove or inline"
2843
+ });
2844
+ }
2704
2845
  }
2705
- return "unknown";
2706
2846
  }
2707
- function 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";
2847
+ function findBlockBodies(lines, startPattern) {
2848
+ const results = [];
2849
+ for (let i = 0; i < lines.length; i++) {
2850
+ const m = lines[i].match(startPattern);
2851
+ if (!m) continue;
2852
+ const name = m[0].match(/class\s+(\w+)/)?.[1] ?? "Unknown";
2853
+ const body = collectBlockLines(lines, i);
2854
+ results.push({ body: body.join("\n"), nameLine: i + 1, name });
2725
2855
  }
2726
- return "unknown";
2856
+ return results;
2727
2857
  }
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("/*", "");
2858
+ function collectBlockLines(lines, startIdx) {
2859
+ let depth = 0;
2860
+ let started = false;
2861
+ const block = [];
2862
+ for (let j = startIdx; j < lines.length; j++) {
2863
+ const line = lines[j];
2864
+ block.push(line);
2865
+ for (const ch of line) {
2866
+ if (ch === "{") {
2867
+ depth++;
2868
+ started = true;
2869
+ } else if (ch === "}") depth--;
2870
+ }
2871
+ if (started && depth <= 0) break;
2742
2872
  }
2743
- return 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;
2873
+ return block;
2757
2874
  }
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";
2875
+ function extractBodyAfter(lines, match) {
2876
+ const startIdx = lines.findIndex((l) => l.includes(match[0].trim().split(/\s+/).pop() ?? ""));
2877
+ if (startIdx < 0) return "";
2878
+ return collectBlockLines(lines, startIdx).join("\n");
2765
2879
  }
2766
- function 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
- }
2880
+ function formatReviewOutput(issues, filesReviewed, totalFiles, focus, customCriteria, autoDetected) {
2881
+ const errors = issues.filter((i) => i.severity === "error");
2882
+ const warnings = issues.filter((i) => i.severity === "warning");
2883
+ const infos = issues.filter((i) => i.severity === "info");
2884
+ const lines = [
2885
+ `\u{1F50D} **Code Review \u2014 Focus: ${focus}**`,
2886
+ ...focus === "over-engineering" ? ["\u{1F4A1} **The Ladder:** 1. Does this code need to exist? 2. Does stdlib cover it? 3. Is there a one-liner? 4. Only then, write it."] : [],
2887
+ ...autoDetected ? [`\u{1F4C2} Auto-detected ${totalFiles} changed file(s) from git`] : [],
2888
+ ...customCriteria ? [`\u{1F4CB} **Custom Criteria:** ${customCriteria}`] : [],
2889
+ `\u{1F4C1} ${filesReviewed}/${totalFiles} files reviewed`,
2890
+ `\u{1F534} ${errors.length} errors | \u{1F7E1} ${warnings.length} warnings | \u{1F535} ${infos.length} info`,
2891
+ ""
2892
+ ];
2893
+ if (issues.length === 0) {
2894
+ lines.push("\u2705 No issues found.");
2895
+ return lines.join("\n");
2780
2896
  }
2781
- 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";
2897
+ const grouped = /* @__PURE__ */ new Map();
2898
+ for (const issue of issues) {
2899
+ const existing = grouped.get(issue.file) ?? [];
2900
+ existing.push(issue);
2901
+ grouped.set(issue.file, existing);
2789
2902
  }
2790
- 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"));
2903
+ for (const [file, fileIssues] of grouped) {
2904
+ lines.push(`**\u{1F4C4} ${file}:**`);
2905
+ for (const issue of fileIssues) {
2906
+ const icon = issue.severity === "error" ? "\u{1F534}" : issue.severity === "warning" ? "\u{1F7E1}" : "\u{1F535}";
2907
+ lines.push(` ${icon} [L${issue.line}] [${issue.rule}] ${issue.message}`);
2908
+ if (issue.suggestion) lines.push(` \u{1F4A1} ${issue.suggestion}`);
2820
2909
  }
2821
- } catch {
2910
+ lines.push("");
2822
2911
  }
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 {
2912
+ if (errors.length > 0) {
2913
+ lines.push("\u26A0\uFE0F Fix errors first, then warnings.");
2914
+ lines.push("\u{1F4A1} Use precise_diff_editor to apply fixes.");
2915
+ } else if (warnings.length > 0) {
2916
+ lines.push("\u{1F4A1} Address warnings before merging.");
2917
+ } else {
2918
+ lines.push("\u2705 Only informational issues \u2014 ready for testing.");
2846
2919
  }
2847
- return results;
2920
+ return lines.join("\n");
2848
2921
  }
2849
2922
 
2850
2923
  // src/agents/projectConventions.ts
@@ -3035,8 +3108,8 @@ async function handleGitDiff(params) {
3035
3108
  }
3036
3109
 
3037
3110
  // src/tools/projectStructure.ts
3038
- import fs9 from "fs";
3039
- import path9 from "path";
3111
+ import fs10 from "fs";
3112
+ import path10 from "path";
3040
3113
  var DEFAULT_IGNORE = [
3041
3114
  "node_modules",
3042
3115
  ".git",
@@ -3073,7 +3146,7 @@ async function handleProjectStructure(params) {
3073
3146
  sessionMemory.recordToolCall("project_structure", { depth: clampedDepth, folderOnly });
3074
3147
  try {
3075
3148
  const tree = buildTree(root, root, clampedDepth, 0, folderOnly, includePattern, excludePattern);
3076
- const projectName = path9.basename(root);
3149
+ const projectName = path10.basename(root);
3077
3150
  const lines = [
3078
3151
  "[Project Structure] - " + projectName,
3079
3152
  "Depth: " + clampedDepth + " | " + (folderOnly ? "Folders only" : "Files and folders"),
@@ -3096,7 +3169,7 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
3096
3169
  const lines = [];
3097
3170
  let entries = [];
3098
3171
  try {
3099
- entries = fs9.readdirSync(currentDir, { withFileTypes: true });
3172
+ entries = fs10.readdirSync(currentDir, { withFileTypes: true });
3100
3173
  } catch {
3101
3174
  return lines;
3102
3175
  }
@@ -3105,12 +3178,12 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
3105
3178
  if (!a.isDirectory() && b.isDirectory()) return 1;
3106
3179
  return a.name.localeCompare(b.name);
3107
3180
  });
3108
- const relativeDir = path9.relative(root, currentDir) || ".";
3181
+ const relativeDir = path10.relative(root, currentDir) || ".";
3109
3182
  const prefix = getPrefix(currentDepth);
3110
3183
  for (const entry of entries) {
3111
3184
  if (shouldIgnore(entry.name, relativeDir, root)) continue;
3112
- const fullPath = path9.join(currentDir, entry.name);
3113
- const relativePath = path9.relative(root, fullPath);
3185
+ const fullPath = path10.join(currentDir, entry.name);
3186
+ const relativePath = path10.relative(root, fullPath);
3114
3187
  if (includePattern && !entry.name.includes(includePattern) && !relativePath.includes(includePattern)) {
3115
3188
  if (!entry.isDirectory()) continue;
3116
3189
  }
@@ -3121,11 +3194,11 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
3121
3194
  lines.push(prefix + "[D] " + entry.name + "/");
3122
3195
  let subEntries = [];
3123
3196
  try {
3124
- subEntries = fs9.readdirSync(fullPath, { withFileTypes: true });
3197
+ subEntries = fs10.readdirSync(fullPath, { withFileTypes: true });
3125
3198
  } catch {
3126
3199
  }
3127
3200
  const hasVisibleContent = subEntries.some(
3128
- (e) => !shouldIgnore(e.name, path9.relative(root, fullPath), root)
3201
+ (e) => !shouldIgnore(e.name, path10.relative(root, fullPath), root)
3129
3202
  );
3130
3203
  if (hasVisibleContent) {
3131
3204
  const subLines = buildTree(root, fullPath, maxDepth, currentDepth + 1, folderOnly, includePattern, excludePattern);
@@ -3156,7 +3229,7 @@ function getPrefix(depth) {
3156
3229
  }
3157
3230
  function getFileSize(fullPath) {
3158
3231
  try {
3159
- const stat = fs9.statSync(fullPath);
3232
+ const stat = fs10.statSync(fullPath);
3160
3233
  if (stat.size < 1024) return stat.size + "B";
3161
3234
  if (stat.size < 1024 * 1024) return (stat.size / 1024).toFixed(0) + "KB";
3162
3235
  const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
@@ -3167,8 +3240,8 @@ function getFileSize(fullPath) {
3167
3240
  }
3168
3241
 
3169
3242
  // src/tools/staticAnalysis.ts
3170
- import fs10 from "fs";
3171
- import path10 from "path";
3243
+ import fs11 from "fs";
3244
+ import path11 from "path";
3172
3245
  async function handleStaticAnalysis(params) {
3173
3246
  const { tool = "all", files, autoFix = false, timeout = 60 } = params;
3174
3247
  const root = getProjectRoot();
@@ -3223,11 +3296,11 @@ function detectAvailableTools(root) {
3223
3296
  "eslint.config.js",
3224
3297
  "eslint.config.mjs"
3225
3298
  ];
3226
- const hasEslintConfig = eslintConfigs.some((cfg) => fs10.existsSync(path10.join(root, cfg)));
3299
+ const hasEslintConfig = eslintConfigs.some((cfg) => fs11.existsSync(path11.join(root, cfg)));
3227
3300
  if (hasEslintConfig && depNames.has("eslint")) {
3228
3301
  tools.push("eslint");
3229
3302
  }
3230
- if (fs10.existsSync(path10.join(root, "tsconfig.json")) && depNames.has("typescript")) {
3303
+ if (fs11.existsSync(path11.join(root, "tsconfig.json")) && depNames.has("typescript")) {
3231
3304
  tools.push("tsc");
3232
3305
  }
3233
3306
  const prettierConfigs = [
@@ -3238,20 +3311,20 @@ function detectAvailableTools(root) {
3238
3311
  ".prettierrc.toml",
3239
3312
  "prettier.config.js"
3240
3313
  ];
3241
- const hasPrettierConfig = prettierConfigs.some((cfg) => fs10.existsSync(path10.join(root, cfg)));
3314
+ const hasPrettierConfig = prettierConfigs.some((cfg) => fs11.existsSync(path11.join(root, cfg)));
3242
3315
  if (hasPrettierConfig && depNames.has("prettier")) {
3243
3316
  tools.push("prettier");
3244
3317
  }
3245
- if (fs10.existsSync(path10.join(root, "ruff.toml")) || fs10.existsSync(path10.join(root, ".ruff.toml"))) {
3318
+ if (fs11.existsSync(path11.join(root, "ruff.toml")) || fs11.existsSync(path11.join(root, ".ruff.toml"))) {
3246
3319
  tools.push("ruff");
3247
3320
  }
3248
3321
  return tools;
3249
3322
  }
3250
3323
  function readPackageJson(root) {
3251
3324
  try {
3252
- const pkgPath = path10.join(root, "package.json");
3253
- if (fs10.existsSync(pkgPath)) {
3254
- return JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
3325
+ const pkgPath = path11.join(root, "package.json");
3326
+ if (fs11.existsSync(pkgPath)) {
3327
+ return JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
3255
3328
  }
3256
3329
  } catch {
3257
3330
  }
@@ -3296,8 +3369,8 @@ function buildToolCommand(tool, files, autoFix) {
3296
3369
  }
3297
3370
  function detectPackageManagerPrefix() {
3298
3371
  const root = getProjectRoot();
3299
- if (fs10.existsSync(path10.join(root, "pnpm-lock.yaml"))) return "pnpm ";
3300
- if (fs10.existsSync(path10.join(root, "yarn.lock"))) return "yarn ";
3372
+ if (fs11.existsSync(path11.join(root, "pnpm-lock.yaml"))) return "pnpm ";
3373
+ if (fs11.existsSync(path11.join(root, "yarn.lock"))) return "yarn ";
3301
3374
  return "npx --no-install ";
3302
3375
  }
3303
3376
  function parseToolOutput(tool, stdout, stderr, projectRoot) {
@@ -3316,7 +3389,7 @@ function parseToolOutput(tool, stdout, stderr, projectRoot) {
3316
3389
  }
3317
3390
  function resolveToolPath(filePath, projectRoot) {
3318
3391
  const trimmed = filePath.trim();
3319
- return path10.isAbsolute(trimmed) ? path10.relative(projectRoot, trimmed) : trimmed.replace(/^\.\//, "");
3392
+ return path11.isAbsolute(trimmed) ? path11.relative(projectRoot, trimmed) : trimmed.replace(/^\.\//, "");
3320
3393
  }
3321
3394
  function parseEslintOutput(output, projectRoot) {
3322
3395
  const issues = [];
@@ -3593,8 +3666,8 @@ async function handleReflect(params) {
3593
3666
  import { execSync as execSync6 } from "child_process";
3594
3667
 
3595
3668
  // src/guards/antiPatternDetector.ts
3596
- import fs11 from "fs";
3597
- import path11 from "path";
3669
+ import fs12 from "fs";
3670
+ import path12 from "path";
3598
3671
  import { execSync as execSync4 } from "child_process";
3599
3672
  var SCRIPT_PATCH_PATTERNS = [
3600
3673
  "writeFileSync",
@@ -3621,20 +3694,20 @@ function findPatchScripts(projectRoot) {
3621
3694
  const warnings = [];
3622
3695
  const recentFiles = scanRecentFiles(projectRoot);
3623
3696
  for (const file of recentFiles) {
3624
- const ext = path11.extname(file).toLowerCase();
3697
+ const ext = path12.extname(file).toLowerCase();
3625
3698
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
3626
- const relativePath = path11.relative(projectRoot, file);
3699
+ const relativePath = path12.relative(projectRoot, file);
3627
3700
  if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".")) {
3628
3701
  continue;
3629
3702
  }
3630
3703
  try {
3631
- const content = fs11.readFileSync(file, "utf-8").toLowerCase();
3704
+ const content = fs12.readFileSync(file, "utf-8").toLowerCase();
3632
3705
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
3633
3706
  if (matchedPattern) {
3634
3707
  warnings.push({
3635
3708
  severity: "high",
3636
3709
  pattern: "script-patching",
3637
- message: `Created script file that modifies other files: ${path11.basename(file)}`,
3710
+ message: `Created script file that modifies other files: ${path12.basename(file)}`,
3638
3711
  suggestion: "Use **precise_diff_editor** instead \u2014 it has fuzzy matching, auto-backup, and rollback support",
3639
3712
  evidence: `File: ${relativePath} contains '${matchedPattern}'`,
3640
3713
  filePath: relativePath
@@ -3668,15 +3741,15 @@ function detectBashGrepUsage() {
3668
3741
  function scanRecentFiles(projectRoot) {
3669
3742
  const recent = [];
3670
3743
  try {
3671
- const entries = fs11.readdirSync(projectRoot, { withFileTypes: true });
3744
+ const entries = fs12.readdirSync(projectRoot, { withFileTypes: true });
3672
3745
  const now = Date.now();
3673
3746
  const recentThreshold = 30 * 60 * 1e3;
3674
3747
  for (const entry of entries) {
3675
3748
  if (!entry.isFile()) continue;
3676
3749
  try {
3677
- const stat = fs11.statSync(path11.join(projectRoot, entry.name));
3750
+ const stat = fs12.statSync(path12.join(projectRoot, entry.name));
3678
3751
  if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
3679
- recent.push(path11.join(projectRoot, entry.name));
3752
+ recent.push(path12.join(projectRoot, entry.name));
3680
3753
  }
3681
3754
  } catch {
3682
3755
  continue;
@@ -3700,14 +3773,14 @@ function detectGitPatchScripts(projectRoot) {
3700
3773
  const prefix = line.substring(0, 2);
3701
3774
  if (prefix !== "??" && prefix !== "A ") continue;
3702
3775
  const file = line.substring(3).trim();
3703
- const ext = path11.extname(file).toLowerCase();
3776
+ const ext = path12.extname(file).toLowerCase();
3704
3777
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
3705
3778
  const isRootLevel = !file.includes("/");
3706
3779
  const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
3707
3780
  if (!isRootLevel && !isScriptsDir) continue;
3708
- const fullPath = path11.join(projectRoot, file);
3709
- if (fs11.existsSync(fullPath)) {
3710
- const content = fs11.readFileSync(fullPath, "utf-8").toLowerCase();
3781
+ const fullPath = path12.join(projectRoot, file);
3782
+ if (fs12.existsSync(fullPath)) {
3783
+ const content = fs12.readFileSync(fullPath, "utf-8").toLowerCase();
3711
3784
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
3712
3785
  if (matchedPattern) {
3713
3786
  warnings.push({
@@ -3730,7 +3803,7 @@ function detectGitPatchScripts(projectRoot) {
3730
3803
  if (deletedStdout) {
3731
3804
  const deletedFiles = deletedStdout.split("\n").filter(Boolean);
3732
3805
  for (const file of deletedFiles) {
3733
- const ext = path11.extname(file).toLowerCase();
3806
+ const ext = path12.extname(file).toLowerCase();
3734
3807
  if (SCRIPT_EXTENSIONS.includes(ext)) {
3735
3808
  warnings.push({
3736
3809
  severity: "high",
@@ -3790,21 +3863,21 @@ function detectAllAntiPatterns() {
3790
3863
  }
3791
3864
 
3792
3865
  // src/engine/contextSnapshot.ts
3793
- import fs12 from "fs";
3794
- import path12 from "path";
3866
+ import fs13 from "fs";
3867
+ import path13 from "path";
3795
3868
  import { execSync as execSync5 } from "child_process";
3796
3869
  var SNAPSHOT_DIR = "context-snapshots";
3797
3870
  function snapshotDir() {
3798
- return path12.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
3871
+ return path13.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
3799
3872
  }
3800
3873
  function ensureSnapshotDir() {
3801
3874
  const dir = snapshotDir();
3802
- if (!fs12.existsSync(dir)) {
3803
- fs12.mkdirSync(dir, { recursive: true });
3875
+ if (!fs13.existsSync(dir)) {
3876
+ fs13.mkdirSync(dir, { recursive: true });
3804
3877
  }
3805
3878
  }
3806
3879
  function snapshotFilePath(timestamp) {
3807
- return path12.join(snapshotDir(), `${timestamp}.json`);
3880
+ return path13.join(snapshotDir(), `${timestamp}.json`);
3808
3881
  }
3809
3882
  function saveSnapshot(goal) {
3810
3883
  try {
@@ -3825,7 +3898,7 @@ function saveSnapshot(goal) {
3825
3898
  hasConventions: !!summary.hasConventions
3826
3899
  };
3827
3900
  try {
3828
- fs12.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
3901
+ fs13.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
3829
3902
  } catch {
3830
3903
  }
3831
3904
  sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
@@ -3833,12 +3906,12 @@ function saveSnapshot(goal) {
3833
3906
  }
3834
3907
  function listSnapshots() {
3835
3908
  const dir = snapshotDir();
3836
- if (!fs12.existsSync(dir)) return [];
3909
+ if (!fs13.existsSync(dir)) return [];
3837
3910
  try {
3838
- const files = fs12.readdirSync(dir).filter((f) => f.endsWith(".json")).sort((a, b) => b.localeCompare(a));
3911
+ const files = fs13.readdirSync(dir).filter((f) => f.endsWith(".json")).sort((a, b) => b.localeCompare(a));
3839
3912
  return files.map((f) => {
3840
3913
  try {
3841
- const content = fs12.readFileSync(path12.join(dir, f), "utf-8");
3914
+ const content = fs13.readFileSync(path13.join(dir, f), "utf-8");
3842
3915
  return JSON.parse(content);
3843
3916
  } catch {
3844
3917
  return null;
@@ -4046,8 +4119,8 @@ async function handleKumaContext(params) {
4046
4119
 
4047
4120
  // src/engine/lspClient.ts
4048
4121
  import { spawn as spawn2 } from "child_process";
4049
- import path13 from "path";
4050
- import fs13 from "fs";
4122
+ import path14 from "path";
4123
+ import fs14 from "fs";
4051
4124
  var LSPClient = class {
4052
4125
  process = null;
4053
4126
  requestId = 0;
@@ -4085,8 +4158,8 @@ var LSPClient = class {
4085
4158
  console.error(`[LSP] typescript-language-server not found. LSP features will fallback to regex.`);
4086
4159
  return;
4087
4160
  }
4088
- const tsconfigPath = path13.join(root, "tsconfig.json");
4089
- if (!fs13.existsSync(tsconfigPath)) {
4161
+ const tsconfigPath = path14.join(root, "tsconfig.json");
4162
+ if (!fs14.existsSync(tsconfigPath)) {
4090
4163
  console.error(`[LSP] Warning: tsconfig.json not found at "${root}". Running in implicit project mode.`);
4091
4164
  }
4092
4165
  this.process = spawn2(lspBinary, ["--stdio", "--log-level=4"], {
@@ -4148,19 +4221,19 @@ var LSPClient = class {
4148
4221
  /** Resolve the typescript-language-server binary from local or global install */
4149
4222
  resolveLspBinary(projectRoot) {
4150
4223
  const candidates = [
4151
- path13.join(projectRoot, "node_modules", ".bin", "typescript-language-server"),
4152
- path13.join(projectRoot, "..", "node_modules", ".bin", "typescript-language-server")
4224
+ path14.join(projectRoot, "node_modules", ".bin", "typescript-language-server"),
4225
+ path14.join(projectRoot, "..", "node_modules", ".bin", "typescript-language-server")
4153
4226
  ];
4154
4227
  for (const candidate of candidates) {
4155
- if (fs13.existsSync(candidate)) {
4228
+ if (fs14.existsSync(candidate)) {
4156
4229
  return candidate;
4157
4230
  }
4158
4231
  }
4159
4232
  try {
4160
4233
  const envPath = process.env.PATH ?? "";
4161
- for (const dir of envPath.split(path13.delimiter)) {
4162
- const binPath = path13.join(dir, "typescript-language-server");
4163
- if (fs13.existsSync(binPath)) {
4234
+ for (const dir of envPath.split(path14.delimiter)) {
4235
+ const binPath = path14.join(dir, "typescript-language-server");
4236
+ if (fs14.existsSync(binPath)) {
4164
4237
  return binPath;
4165
4238
  }
4166
4239
  }
@@ -4181,7 +4254,7 @@ var LSPClient = class {
4181
4254
  this.openDocuments.add(filePath);
4182
4255
  let content;
4183
4256
  try {
4184
- content = fs13.readFileSync(filePath, "utf-8");
4257
+ content = fs14.readFileSync(filePath, "utf-8");
4185
4258
  } catch {
4186
4259
  content = "";
4187
4260
  }
@@ -4398,8 +4471,8 @@ var LSPClient = class {
4398
4471
  var lspClient = new LSPClient();
4399
4472
 
4400
4473
  // src/tools/lspTools.ts
4401
- import fs14 from "fs";
4402
- import path14 from "path";
4474
+ import fs15 from "fs";
4475
+ import path15 from "path";
4403
4476
  async function handleFindReferences(params) {
4404
4477
  const { filePath, line, character } = params;
4405
4478
  const validation = validateFilePath(filePath);
@@ -4407,7 +4480,7 @@ async function handleFindReferences(params) {
4407
4480
  return `Error: ${validation.error.message}`;
4408
4481
  }
4409
4482
  const resolvedPath = validation.resolvedPath;
4410
- if (!fs14.existsSync(resolvedPath)) {
4483
+ if (!fs15.existsSync(resolvedPath)) {
4411
4484
  return `Error: File not found: "${filePath}".`;
4412
4485
  }
4413
4486
  if (!lspClient.isAvailable()) {
@@ -4429,7 +4502,7 @@ async function handleFindReferences(params) {
4429
4502
  const enrichedRefs = references.map((ref) => {
4430
4503
  let lineContent = "";
4431
4504
  try {
4432
- const content = fs14.readFileSync(ref.filePath, "utf-8");
4505
+ const content = fs15.readFileSync(ref.filePath, "utf-8");
4433
4506
  const lines2 = content.split("\n");
4434
4507
  lineContent = lines2[ref.line]?.trim() ?? "";
4435
4508
  } catch {
@@ -4445,11 +4518,11 @@ async function handleFindReferences(params) {
4445
4518
  const projectRoot = getProjectRoot();
4446
4519
  const lines = [
4447
4520
  `\u{1F50D} **Find References** \u2014 ${enrichedRefs.length} references found`,
4448
- `\u{1F4CD} File: ${path14.relative(projectRoot, resolvedPath)}:${line + 1}:${character + 1}`,
4521
+ `\u{1F4CD} File: ${path15.relative(projectRoot, resolvedPath)}:${line + 1}:${character + 1}`,
4449
4522
  ""
4450
4523
  ];
4451
4524
  for (const [file, refs] of grouped) {
4452
- const relPath = path14.relative(projectRoot, file);
4525
+ const relPath = path15.relative(projectRoot, file);
4453
4526
  lines.push(`**\u{1F4C4} ${relPath}:**`);
4454
4527
  for (const ref of refs) {
4455
4528
  const loc = `L${ref.line + 1}:${ref.character + 1}`;
@@ -4470,7 +4543,7 @@ async function handleGoToDefinition(params) {
4470
4543
  return `Error: ${validation.error.message}`;
4471
4544
  }
4472
4545
  const resolvedPath = validation.resolvedPath;
4473
- if (!fs14.existsSync(resolvedPath)) {
4546
+ if (!fs15.existsSync(resolvedPath)) {
4474
4547
  return `Error: File not found: "${filePath}".`;
4475
4548
  }
4476
4549
  if (!lspClient.isAvailable()) {
@@ -4490,10 +4563,10 @@ async function handleGoToDefinition(params) {
4490
4563
  \u26A0\uFE0F Cannot find definition for symbol at this position.`;
4491
4564
  }
4492
4565
  const projectRoot = getProjectRoot();
4493
- const relPath = path14.relative(projectRoot, definition.filePath);
4566
+ const relPath = path15.relative(projectRoot, definition.filePath);
4494
4567
  let lineContent = "";
4495
4568
  try {
4496
- const content = fs14.readFileSync(definition.filePath, "utf-8");
4569
+ const content = fs15.readFileSync(definition.filePath, "utf-8");
4497
4570
  const lines2 = content.split("\n");
4498
4571
  lineContent = lines2[definition.line]?.trim() ?? "";
4499
4572
  } catch {
@@ -4525,7 +4598,7 @@ async function handleRenameSymbol(params) {
4525
4598
  return `Error: ${validation.error.message}`;
4526
4599
  }
4527
4600
  const resolvedPath = validation.resolvedPath;
4528
- if (!fs14.existsSync(resolvedPath)) {
4601
+ if (!fs15.existsSync(resolvedPath)) {
4529
4602
  return `Error: File not found: "${filePath}".`;
4530
4603
  }
4531
4604
  if (!lspClient.isAvailable()) {
@@ -4554,7 +4627,7 @@ Make sure:
4554
4627
  const fileChanges = [];
4555
4628
  for (const change of result.changes) {
4556
4629
  try {
4557
- const content = fs14.readFileSync(change.filePath, "utf-8");
4630
+ const content = fs15.readFileSync(change.filePath, "utf-8");
4558
4631
  const lines2 = content.split("\n");
4559
4632
  const sortedEdits = [...change.edits].sort((a, b) => {
4560
4633
  if (b.line !== a.line) return b.line - a.line;
@@ -4568,10 +4641,10 @@ Make sure:
4568
4641
  lines2[edit.line] = before + edit.newText + after;
4569
4642
  }
4570
4643
  }
4571
- fs14.writeFileSync(change.filePath, lines2.join("\n"), "utf-8");
4644
+ fs15.writeFileSync(change.filePath, lines2.join("\n"), "utf-8");
4572
4645
  totalEdits += change.edits.length;
4573
4646
  fileChanges.push({
4574
- filePath: path14.relative(projectRoot, change.filePath),
4647
+ filePath: path15.relative(projectRoot, change.filePath),
4575
4648
  editCount: change.edits.length
4576
4649
  });
4577
4650
  } catch (err) {
@@ -4598,13 +4671,13 @@ async function handleGetTypeInfo(params) {
4598
4671
  return `Error: ${validation.error.message}`;
4599
4672
  }
4600
4673
  const resolvedPath = validation.resolvedPath;
4601
- if (!fs14.existsSync(resolvedPath)) {
4674
+ if (!fs15.existsSync(resolvedPath)) {
4602
4675
  return `Error: File not found: "${filePath}".`;
4603
4676
  }
4604
4677
  if (!lspClient.isAvailable()) {
4605
4678
  sessionMemory.recordToolCall("lsp_query", { action: "type", filePath, line, character, fallback: "line-context" });
4606
4679
  try {
4607
- const fileContent = fs14.readFileSync(resolvedPath, "utf-8");
4680
+ const fileContent = fs15.readFileSync(resolvedPath, "utf-8");
4608
4681
  const fileLines = fileContent.split("\n");
4609
4682
  const targetLine = fileLines[line];
4610
4683
  if (!targetLine) {
@@ -4612,7 +4685,7 @@ async function handleGetTypeInfo(params) {
4612
4685
  }
4613
4686
  const symbolName = extractSymbolAtPosition(resolvedPath, line, character);
4614
4687
  const projectRoot = getProjectRoot();
4615
- const relPath = path14.relative(projectRoot, resolvedPath);
4688
+ const relPath = path15.relative(projectRoot, resolvedPath);
4616
4689
  const contextStart = Math.max(0, line - 3);
4617
4690
  const contextEnd = Math.min(fileLines.length, line + 4);
4618
4691
  const contextLines = [];
@@ -4650,7 +4723,7 @@ async function handleGetTypeInfo(params) {
4650
4723
  \u26A0\uFE0F No type info for this position.`;
4651
4724
  }
4652
4725
  const projectRoot = getProjectRoot();
4653
- const relPath = path14.relative(projectRoot, resolvedPath);
4726
+ const relPath = path15.relative(projectRoot, resolvedPath);
4654
4727
  const lines = [
4655
4728
  `\u{1F4CB} **Type Info** \u2014 \`${relPath}:${line + 1}:${character + 1}\``,
4656
4729
  "",
@@ -4691,7 +4764,7 @@ async function handleLspQuery(params) {
4691
4764
  }
4692
4765
  function extractSymbolAtPosition(filePath, line, character) {
4693
4766
  try {
4694
- const content = fs14.readFileSync(filePath, "utf-8");
4767
+ const content = fs15.readFileSync(filePath, "utf-8");
4695
4768
  const lines = content.split("\n");
4696
4769
  const targetLine = lines[line];
4697
4770
  if (!targetLine) return null;
@@ -4722,7 +4795,7 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
4722
4795
  const regex = new RegExp(`\\b${escapedSymbol}\\b`, "g");
4723
4796
  for (const file of tsFiles.slice(0, 100)) {
4724
4797
  try {
4725
- const content = fs14.readFileSync(file, "utf-8");
4798
+ const content = fs15.readFileSync(file, "utf-8");
4726
4799
  const lines2 = content.split("\n");
4727
4800
  for (let i = 0; i < lines2.length; i++) {
4728
4801
  if (regex.test(lines2[i])) {
@@ -4751,8 +4824,28 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
4751
4824
  `\u26A0\uFE0F Regex results may be less accurate than LSP (includes comments/strings).`,
4752
4825
  ""
4753
4826
  ];
4827
+ if (grouped.size > 1) {
4828
+ const fileList = [...grouped.keys()].map((f) => path15.relative(projectRoot, f)).join(", ");
4829
+ lines.push(`\u26A0\uFE0F **Ambiguity warning:** Found matches across ${grouped.size} different files (${fileList}).`);
4830
+ lines.push(` Regex fallback cannot distinguish between different scopes with the same symbol name.`);
4831
+ lines.push(` If you intended only one scope, clarify which file or location you mean.`);
4832
+ lines.push(` \u{1F4A1} Install typescript-language-server for scope-aware disambiguation.`);
4833
+ lines.push("");
4834
+ } else if (results.length > 1) {
4835
+ const filePath = [...grouped.keys()][0];
4836
+ const refs = grouped.get(filePath);
4837
+ const matchLines = refs.map((r) => r.line);
4838
+ const minLine = Math.min(...matchLines);
4839
+ const maxLine = Math.max(...matchLines);
4840
+ if (maxLine - minLine > 15) {
4841
+ lines.push(`\u26A0\uFE0F **Ambiguity warning:** Found ${results.length} matches for "${symbolName}" spanning lines ${minLine}-${maxLine} in the same file.`);
4842
+ lines.push(` They may be in different function scopes \u2014 regex cannot distinguish them.`);
4843
+ lines.push(` \u{1F4A1} Verify each match is the intended symbol before editing.`);
4844
+ lines.push("");
4845
+ }
4846
+ }
4754
4847
  for (const [file, refs] of grouped) {
4755
- const relPath = path14.relative(projectRoot, file);
4848
+ const relPath = path15.relative(projectRoot, file);
4756
4849
  lines.push(`**\u{1F4C4} ${relPath}:**`);
4757
4850
  for (const ref of refs) {
4758
4851
  lines.push(` \u2514 L${ref.line} \u2014 ${ref.content}`);
@@ -4786,14 +4879,14 @@ async function fallbackGrepDefinition(symbolName) {
4786
4879
  ];
4787
4880
  for (const file of tsFiles) {
4788
4881
  try {
4789
- const content = fs14.readFileSync(file, "utf-8");
4882
+ const content = fs15.readFileSync(file, "utf-8");
4790
4883
  const lines = content.split("\n");
4791
4884
  for (let i = 0; i < lines.length; i++) {
4792
4885
  const trimmed = lines[i].trim();
4793
4886
  for (const pattern of declPatterns) {
4794
4887
  if (pattern.test(trimmed)) {
4795
4888
  const projectRoot = getProjectRoot();
4796
- const relPath = path14.relative(projectRoot, file);
4889
+ const relPath = path15.relative(projectRoot, file);
4797
4890
  return [
4798
4891
  `\u{1F4CD} **Go to Definition** (regex fallback)`,
4799
4892
  `\u{1F4C4} File: \`${relPath}\``,
@@ -4906,11 +4999,13 @@ function registerAllTools(server) {
4906
4999
  );
4907
5000
  server.tool(
4908
5001
  "execute_safe_test",
4909
- "Run tests, lint, or typecheck with timeout protection and circuit breaker. Use after every edit to verify you didn't break anything.",
5002
+ "Run tests, lint, or typecheck with timeout protection and circuit breaker. Supports monorepo workspaces via 'workspace' or relative 'cwd'. Use after every edit to verify you didn't break anything.",
4910
5003
  {
4911
5004
  task: z.enum(["test", "build", "lint", "typecheck", "custom"]).describe("Task to execute: test, build, lint, typecheck, or custom"),
4912
5005
  customCommand: z.string({ invalid_type_error: '"customCommand" must be a string command like "npm run my-script"' }).optional().describe("Custom command (required only if task='custom')"),
4913
- timeout: z.number({ invalid_type_error: '"timeout" must be a number in seconds (5-180)' }).min(5).max(180).optional().default(60).describe("Timeout in seconds (default: 60s, max: 180s)")
5006
+ timeout: z.number({ invalid_type_error: '"timeout" must be a number in seconds (5-180)' }).min(5).max(180).optional().default(60).describe("Timeout in seconds (default: 60s, max: 180s)"),
5007
+ cwd: z.string().optional().describe("Relative directory from project root (e.g. 'packages/web'). Defaults to project root."),
5008
+ workspace: z.string().optional().describe("Workspace name from project_conventions (e.g. 'frontend', 'api'). Auto-resolves to path.")
4914
5009
  },
4915
5010
  async (params) => {
4916
5011
  try {
@@ -5143,8 +5238,8 @@ function registerAllTools(server) {
5143
5238
  }
5144
5239
 
5145
5240
  // src/cli/init.ts
5146
- import fs15 from "fs";
5147
- import path15 from "path";
5241
+ import fs16 from "fs";
5242
+ import path16 from "path";
5148
5243
  var ALL_CONFIG_TYPES = [
5149
5244
  "claude",
5150
5245
  "cursor",
@@ -5536,10 +5631,10 @@ var TEMPLATES = {
5536
5631
  };
5537
5632
  var APPEND_SEPARATOR = "\n\n---\n_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_\n\n";
5538
5633
  function handleOpencodeSecondary(root, results) {
5539
- const claudePath = path15.resolve(root, "CLAUDE.md");
5540
- if (!fs15.existsSync(claudePath)) {
5634
+ const claudePath = path16.resolve(root, "CLAUDE.md");
5635
+ if (!fs16.existsSync(claudePath)) {
5541
5636
  try {
5542
- fs15.writeFileSync(claudePath, [
5637
+ fs16.writeFileSync(claudePath, [
5543
5638
  "# Kuma MCP - OpenCode Instructions",
5544
5639
  "",
5545
5640
  KUMA_CORE_INSTRUCTIONS
@@ -5556,21 +5651,21 @@ function handleOpencodeSecondary(root, results) {
5556
5651
  }
5557
5652
  }
5558
5653
  function handleCodexSecondary(root, results) {
5559
- const tomlPath = path15.resolve(root, ".codex/config.toml");
5654
+ const tomlPath = path16.resolve(root, ".codex/config.toml");
5560
5655
  if (results.some((r) => r.filePath === ".codex/config.toml")) return;
5561
5656
  try {
5562
- const dir = 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");
5657
+ const dir = path16.dirname(tomlPath);
5658
+ if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
5659
+ if (fs16.existsSync(tomlPath)) {
5660
+ const existingContent = fs16.readFileSync(tomlPath, "utf-8");
5566
5661
  if (existingContent.includes("kuma")) {
5567
5662
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
5568
5663
  return;
5569
5664
  }
5570
- fs15.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
5665
+ fs16.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
5571
5666
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
5572
5667
  } else {
5573
- fs15.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
5668
+ fs16.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
5574
5669
  results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
5575
5670
  }
5576
5671
  } catch (err) {
@@ -5583,11 +5678,11 @@ function handleCodexSecondary(root, results) {
5583
5678
  }
5584
5679
  }
5585
5680
  function handleQwenSecondary(root, results) {
5586
- const settingsPath = path15.resolve(root, "settings.json");
5681
+ const settingsPath = path16.resolve(root, "settings.json");
5587
5682
  if (results.some((r) => r.filePath === "settings.json")) return;
5588
5683
  try {
5589
- if (fs15.existsSync(settingsPath)) {
5590
- const existingContent = fs15.readFileSync(settingsPath, "utf-8");
5684
+ if (fs16.existsSync(settingsPath)) {
5685
+ const existingContent = fs16.readFileSync(settingsPath, "utf-8");
5591
5686
  if (existingContent.includes("kuma")) {
5592
5687
  if (!existingContent.includes("_Generated by Kuma MCP_")) {
5593
5688
  try {
@@ -5595,7 +5690,7 @@ function handleQwenSecondary(root, results) {
5595
5690
  parsed.mcpServers = parsed.mcpServers || {};
5596
5691
  if (!parsed.mcpServers.kuma) {
5597
5692
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
5598
- fs15.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5693
+ fs16.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5599
5694
  results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
5600
5695
  return;
5601
5696
  }
@@ -5610,13 +5705,13 @@ function handleQwenSecondary(root, results) {
5610
5705
  parsed.mcpServers = parsed.mcpServers || {};
5611
5706
  if (!parsed.mcpServers.kuma) {
5612
5707
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
5613
- fs15.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5708
+ fs16.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5614
5709
  results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
5615
5710
  }
5616
5711
  } catch {
5617
5712
  }
5618
5713
  } else {
5619
- fs15.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
5714
+ fs16.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
5620
5715
  results.push({ type: "qwen", filePath: "settings.json", action: "created" });
5621
5716
  }
5622
5717
  } catch (err) {
@@ -5652,18 +5747,18 @@ function getCombinedAgentsMd(selectedTypes) {
5652
5747
  return sections.join("\n\n---\n\n");
5653
5748
  }
5654
5749
  function handleAntigravityMcpConfig(root, results) {
5655
- const mcpPath = path15.resolve(root, ".agents/mcp_config.json");
5750
+ const mcpPath = path16.resolve(root, ".agents/mcp_config.json");
5656
5751
  if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
5657
5752
  try {
5658
- const mcpDir = path15.dirname(mcpPath);
5659
- if (fs15.existsSync(mcpPath)) {
5660
- const existingContent = fs15.readFileSync(mcpPath, "utf-8");
5753
+ const mcpDir = path16.dirname(mcpPath);
5754
+ if (fs16.existsSync(mcpPath)) {
5755
+ const existingContent = fs16.readFileSync(mcpPath, "utf-8");
5661
5756
  if (existingContent.includes("kuma")) {
5662
5757
  if (!existingContent.includes("_Generated by Kuma MCP_")) {
5663
5758
  const trimmed = existingContent.trimEnd();
5664
5759
  if (trimmed.endsWith("}")) {
5665
5760
  const updated = trimmed.slice(0, -1).trimEnd() + ',\n "_kuma_note": "Kuma MCP - Generated by kuma init"\n}\n';
5666
- fs15.writeFileSync(mcpPath, updated, "utf-8");
5761
+ fs16.writeFileSync(mcpPath, updated, "utf-8");
5667
5762
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
5668
5763
  }
5669
5764
  }
@@ -5672,11 +5767,11 @@ function handleAntigravityMcpConfig(root, results) {
5672
5767
  const parsed = JSON.parse(existingContent);
5673
5768
  parsed.mcpServers = parsed.mcpServers || {};
5674
5769
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
5675
- fs15.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5770
+ fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5676
5771
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
5677
5772
  } else {
5678
- if (!fs15.existsSync(mcpDir)) fs15.mkdirSync(mcpDir, { recursive: true });
5679
- fs15.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
5773
+ if (!fs16.existsSync(mcpDir)) fs16.mkdirSync(mcpDir, { recursive: true });
5774
+ fs16.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
5680
5775
  results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
5681
5776
  }
5682
5777
  } catch (err) {
@@ -5689,18 +5784,18 @@ function handleAntigravityMcpConfig(root, results) {
5689
5784
  }
5690
5785
  }
5691
5786
  function handleAiderSecondary(root, results) {
5692
- const ymlPath = path15.resolve(root, ".aider.conf.yml");
5787
+ const ymlPath = path16.resolve(root, ".aider.conf.yml");
5693
5788
  if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
5694
5789
  try {
5695
5790
  const conventionsRef = "read: CONVENTIONS.md";
5696
- if (fs15.existsSync(ymlPath)) {
5697
- const existingContent = fs15.readFileSync(ymlPath, "utf-8");
5791
+ if (fs16.existsSync(ymlPath)) {
5792
+ const existingContent = fs16.readFileSync(ymlPath, "utf-8");
5698
5793
  if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
5699
5794
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
5700
5795
  return;
5701
5796
  }
5702
5797
  const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
5703
- fs15.writeFileSync(ymlPath, newContent, "utf-8");
5798
+ fs16.writeFileSync(ymlPath, newContent, "utf-8");
5704
5799
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
5705
5800
  } else {
5706
5801
  const content = [
@@ -5710,7 +5805,7 @@ function handleAiderSecondary(root, results) {
5710
5805
  conventionsRef,
5711
5806
  ""
5712
5807
  ].join("\n");
5713
- fs15.writeFileSync(ymlPath, content, "utf-8");
5808
+ fs16.writeFileSync(ymlPath, content, "utf-8");
5714
5809
  results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
5715
5810
  }
5716
5811
  } catch (err) {
@@ -5723,10 +5818,10 @@ function handleAiderSecondary(root, results) {
5723
5818
  }
5724
5819
  }
5725
5820
  function handleCopilotSecondary(root, results) {
5726
- const skillPath = path15.resolve(root, ".github/skills/kuma/SKILL.md");
5821
+ const skillPath = path16.resolve(root, ".github/skills/kuma/SKILL.md");
5727
5822
  if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
5728
5823
  try {
5729
- const dir = path15.dirname(skillPath);
5824
+ const dir = path16.dirname(skillPath);
5730
5825
  const content = [
5731
5826
  "---",
5732
5827
  "name: kuma-mcp",
@@ -5737,17 +5832,17 @@ function handleCopilotSecondary(root, results) {
5737
5832
  "",
5738
5833
  KUMA_CORE_INSTRUCTIONS
5739
5834
  ].join("\n");
5740
- if (fs15.existsSync(skillPath)) {
5741
- const existingContent = fs15.readFileSync(skillPath, "utf-8");
5835
+ if (fs16.existsSync(skillPath)) {
5836
+ const existingContent = fs16.readFileSync(skillPath, "utf-8");
5742
5837
  if (existingContent.includes("kuma")) {
5743
5838
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
5744
5839
  return;
5745
5840
  }
5746
- fs15.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
5841
+ fs16.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
5747
5842
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
5748
5843
  } else {
5749
- if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
5750
- fs15.writeFileSync(skillPath, content, "utf-8");
5844
+ if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
5845
+ fs16.writeFileSync(skillPath, content, "utf-8");
5751
5846
  results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
5752
5847
  }
5753
5848
  } catch (err) {
@@ -5760,21 +5855,21 @@ function handleCopilotSecondary(root, results) {
5760
5855
  }
5761
5856
  }
5762
5857
  function handleOpenclawSecondary(root, results) {
5763
- const mcpPath = path15.resolve(root, ".agents/mcp_config.json");
5858
+ const mcpPath = path16.resolve(root, ".agents/mcp_config.json");
5764
5859
  if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
5765
5860
  try {
5766
- const dir = path15.dirname(mcpPath);
5767
- if (fs15.existsSync(mcpPath)) {
5768
- const existingContent = fs15.readFileSync(mcpPath, "utf-8");
5861
+ const dir = path16.dirname(mcpPath);
5862
+ if (fs16.existsSync(mcpPath)) {
5863
+ const existingContent = fs16.readFileSync(mcpPath, "utf-8");
5769
5864
  if (existingContent.includes("kuma")) return;
5770
5865
  const parsed = JSON.parse(existingContent);
5771
5866
  parsed.mcpServers = parsed.mcpServers || {};
5772
5867
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
5773
- fs15.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5868
+ fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5774
5869
  results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
5775
5870
  } else {
5776
- if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
5777
- fs15.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
5871
+ if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
5872
+ fs16.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
5778
5873
  results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
5779
5874
  }
5780
5875
  } catch (err) {
@@ -5787,20 +5882,20 @@ function handleOpenclawSecondary(root, results) {
5787
5882
  }
5788
5883
  }
5789
5884
  function handleCodewhaleSecondary(root, results) {
5790
- const mcpPath = path15.resolve(root, ".codewhale/mcp.json");
5885
+ const mcpPath = path16.resolve(root, ".codewhale/mcp.json");
5791
5886
  if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
5792
5887
  try {
5793
- const dir = path15.dirname(mcpPath);
5794
- if (fs15.existsSync(mcpPath)) {
5795
- const existingContent = fs15.readFileSync(mcpPath, "utf-8");
5888
+ const dir = path16.dirname(mcpPath);
5889
+ if (fs16.existsSync(mcpPath)) {
5890
+ const existingContent = fs16.readFileSync(mcpPath, "utf-8");
5796
5891
  if (existingContent.includes("kuma")) return;
5797
5892
  const parsed = JSON.parse(existingContent);
5798
5893
  parsed.mcpServers = parsed.mcpServers || {};
5799
5894
  parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
5800
- fs15.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5895
+ fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5801
5896
  results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
5802
5897
  } else {
5803
- if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
5898
+ if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
5804
5899
  const config = {
5805
5900
  mcpServers: {
5806
5901
  kuma: {
@@ -5810,7 +5905,7 @@ function handleCodewhaleSecondary(root, results) {
5810
5905
  }
5811
5906
  }
5812
5907
  };
5813
- fs15.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
5908
+ fs16.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
5814
5909
  results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
5815
5910
  }
5816
5911
  } catch (err) {
@@ -5831,24 +5926,24 @@ function runInit(types, projectRoot) {
5831
5926
  let agentsMdHandled = false;
5832
5927
  for (const type of selected) {
5833
5928
  const relativePath = configFilePath(type);
5834
- const fullPath = path15.resolve(root, relativePath);
5929
+ const fullPath = path16.resolve(root, relativePath);
5835
5930
  const getTemplate = TEMPLATES[type];
5836
5931
  try {
5837
5932
  if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
5838
5933
  agentsMdHandled = true;
5839
5934
  const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
5840
- if (fs15.existsSync(fullPath)) {
5841
- const existingContent = fs15.readFileSync(fullPath, "utf-8");
5935
+ if (fs16.existsSync(fullPath)) {
5936
+ const existingContent = fs16.readFileSync(fullPath, "utf-8");
5842
5937
  if (existingContent.includes("_Generated by Kuma MCP_")) {
5843
5938
  results.push({ type, filePath: relativePath, action: "skipped" });
5844
5939
  } else {
5845
- fs15.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
5940
+ fs16.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
5846
5941
  results.push({ type, filePath: relativePath, action: "appended" });
5847
5942
  }
5848
5943
  } else {
5849
- const dir = path15.dirname(fullPath);
5850
- if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
5851
- fs15.writeFileSync(fullPath, combinedContent, "utf-8");
5944
+ const dir = path16.dirname(fullPath);
5945
+ if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
5946
+ fs16.writeFileSync(fullPath, combinedContent, "utf-8");
5852
5947
  results.push({ type, filePath: relativePath, action: "created" });
5853
5948
  }
5854
5949
  if (selectedSet.has("codex")) handleCodexSecondary(root, results);
@@ -5859,8 +5954,8 @@ function runInit(types, projectRoot) {
5859
5954
  continue;
5860
5955
  } else {
5861
5956
  const template = getTemplate();
5862
- if (fs15.existsSync(fullPath)) {
5863
- const existingContent = fs15.readFileSync(fullPath, "utf-8");
5957
+ if (fs16.existsSync(fullPath)) {
5958
+ const existingContent = fs16.readFileSync(fullPath, "utf-8");
5864
5959
  if (existingContent.includes("_Generated by Kuma MCP_")) {
5865
5960
  if (type === "antigravity") {
5866
5961
  handleAntigravityMcpConfig(root, results);
@@ -5873,14 +5968,14 @@ function runInit(types, projectRoot) {
5873
5968
  continue;
5874
5969
  }
5875
5970
  const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
5876
- fs15.writeFileSync(fullPath, newContent, "utf-8");
5971
+ fs16.writeFileSync(fullPath, newContent, "utf-8");
5877
5972
  results.push({ type, filePath: relativePath, action: "appended" });
5878
5973
  } else {
5879
- const dir = path15.dirname(fullPath);
5880
- if (!fs15.existsSync(dir)) {
5881
- fs15.mkdirSync(dir, { recursive: true });
5974
+ const dir = path16.dirname(fullPath);
5975
+ if (!fs16.existsSync(dir)) {
5976
+ fs16.mkdirSync(dir, { recursive: true });
5882
5977
  }
5883
- fs15.writeFileSync(fullPath, template, "utf-8");
5978
+ fs16.writeFileSync(fullPath, template, "utf-8");
5884
5979
  results.push({ type, filePath: relativePath, action: "created" });
5885
5980
  }
5886
5981
  if (type === "antigravity") {