@plumpslabs/kuma 2.0.5 → 2.1.1

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