@plumpslabs/kuma 2.1.1 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +163 -15
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -2057,6 +2057,24 @@ function truncateOutput(output, maxChars) {
2057
2057
  }
2058
2058
 
2059
2059
  // src/tools/safeTerminalExec.ts
2060
+ function stripShellObfuscation(cmd) {
2061
+ let result = cmd;
2062
+ let prev;
2063
+ do {
2064
+ prev = result;
2065
+ result = result.replace(/\$\([^()]*\)/g, "");
2066
+ } while (result !== prev);
2067
+ do {
2068
+ prev = result;
2069
+ result = result.replace(/\$\(\([^()]*\)\)/g, "");
2070
+ } while (result !== prev);
2071
+ result = result.replace(/\$\{[^}]*\}/g, " ");
2072
+ result = result.replace(/\$'[^']*'/g, "");
2073
+ result = result.replace(/\$"[^"]*"/g, "");
2074
+ result = result.replace(/\$[a-zA-Z_][a-zA-Z0-9_]*/g, " ");
2075
+ result = result.replace(/\s+/g, " ").trim();
2076
+ return result;
2077
+ }
2060
2078
  function buildTaskCommand(task, cwd) {
2061
2079
  const pm = detectPackageManagerForDir(cwd);
2062
2080
  const prefix = pm === "pnpm" ? "pnpm" : pm === "yarn" ? "yarn" : pm === "bun" ? "bun" : "npm";
@@ -2075,24 +2093,59 @@ function buildTaskCommand(task, cwd) {
2075
2093
  }
2076
2094
  }
2077
2095
  var DANGEROUS_PATTERNS = [
2096
+ // RM variants
2078
2097
  "rm -rf",
2079
2098
  "rm -fr",
2099
+ "rm --recursive",
2100
+ "rm --force",
2080
2101
  "del /f",
2081
2102
  "rd /s",
2082
2103
  "rmdir /s",
2104
+ // Git dangerous
2083
2105
  "git push",
2084
2106
  "git commit",
2107
+ "git reset --hard",
2108
+ "git clean -fd",
2109
+ "git checkout -- .",
2110
+ // Publish to registries
2085
2111
  "npm publish",
2086
2112
  "npx publish",
2087
2113
  "yarn publish",
2088
2114
  "pnpm publish",
2115
+ // System destruction
2089
2116
  "> /dev/sda",
2090
- "format",
2091
2117
  "mkfs",
2092
2118
  "dd if=",
2119
+ "dd of=",
2120
+ "fdisk",
2121
+ "parted",
2122
+ "mkswap",
2123
+ // Fork bomb
2093
2124
  ":(){ :|:& };:",
2094
- "curl ",
2095
- "wget "
2125
+ ":(){",
2126
+ // Pipe to shell (remote code execution vector)
2127
+ "| bash",
2128
+ "| sh",
2129
+ // Dangerous eval/exec
2130
+ "eval ",
2131
+ "exec ",
2132
+ "source /dev/stdin",
2133
+ ". /dev/stdin",
2134
+ // Find destruction
2135
+ "find . -delete",
2136
+ "find / -delete",
2137
+ "find . -exec rm",
2138
+ "find / -exec rm",
2139
+ // Chmod abuse
2140
+ "chmod -R 777",
2141
+ "chmod 777 /",
2142
+ "chmod 000",
2143
+ // Alias hijacking
2144
+ "alias rm=",
2145
+ "alias cd=",
2146
+ "alias sudo=",
2147
+ // Overwrite protection
2148
+ "shred"
2096
2149
  ];
2097
2150
  async function handleSafeTerminalExec(params) {
2098
2151
  const { task, customCommand, timeout = 60, cwd: inputCwd, workspace } = params;
@@ -2134,7 +2187,10 @@ Available workspaces: ${workspaces?.map((w) => `"${w.name}" (${w.path})`).join("
2134
2187
 
2135
2188
  Fix the code first before running the task again.`;
2136
2189
  }
2137
- const dangerousPattern = DANGEROUS_PATTERNS.find((p) => command.toLowerCase().includes(p.toLowerCase()));
2190
+ const deobfuscated = stripShellObfuscation(command);
2191
+ const dangerousPattern = DANGEROUS_PATTERNS.find(
2192
+ (p) => deobfuscated.toLowerCase().includes(p.toLowerCase())
2193
+ );
2138
2194
  if (dangerousPattern) {
2139
2195
  return `\u{1F6AB} BLOCKED: Command contains a dangerous pattern: "${dangerousPattern}".
2140
2196
  This command is not permitted.`;
@@ -2247,7 +2303,7 @@ function getGitChangedFiles() {
2247
2303
  }
2248
2304
  }
2249
2305
  async function handleCodeReviewer(params) {
2250
- const { files: inputFiles, focus = "correctness", customCriteria, format = "text" } = params;
2306
+ const { files: inputFiles, focus = "correctness", customCriteria, format = "text", convention } = params;
2251
2307
  let files = inputFiles ?? [];
2252
2308
  let isAutoDetected = false;
2253
2309
  if (files.length === 0) {
@@ -2306,6 +2362,9 @@ Pass an explicit 'files' array to review specific files.`;
2306
2362
  checkOverEngineering(filePath, content, allIssues);
2307
2363
  break;
2308
2364
  }
2365
+ if (convention === "matcha") {
2366
+ checkMatchaConventions(filePath, content, allIssues);
2367
+ }
2309
2368
  } catch (err) {
2310
2369
  allIssues.push({
2311
2370
  file: filePath,
@@ -2333,6 +2392,7 @@ Pass an explicit 'files' array to review specific files.`;
2333
2392
  filesReviewed,
2334
2393
  filesRequested: files.length,
2335
2394
  focus,
2395
+ convention,
2336
2396
  autoDetected: isAutoDetected,
2337
2397
  ...customCriteria ? { customCriteria } : {}
2338
2398
  };
@@ -2859,8 +2919,8 @@ function collectBlockLines(lines, startIdx) {
2859
2919
  let depth = 0;
2860
2920
  let started = false;
2861
2921
  const block = [];
2862
- for (let j = startIdx; j < lines.length; j++) {
2863
- const line = lines[j];
2922
+ for (let i = startIdx; i < lines.length; i++) {
2923
+ const line = lines[i];
2864
2924
  block.push(line);
2865
2925
  for (const ch of line) {
2866
2926
  if (ch === "{") {
@@ -2877,6 +2937,73 @@ function extractBodyAfter(lines, match) {
2877
2937
  if (startIdx < 0) return "";
2878
2938
  return collectBlockLines(lines, startIdx).join("\n");
2879
2939
  }
2940
+ function checkMatchaConventions(filePath, content, issues) {
2941
+ const lines = content.split("\n");
2942
+ const text = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
2943
+ for (let i = 0; i < lines.length; i++) {
2944
+ const line = stripCommentsAndStrings(lines[i]);
2945
+ const lineNum = i + 1;
2946
+ if (/\b(?:const|let|var)\s+\w+\s*=\s*(?:[2-9]|\d{2,})\b/.test(line)) {
2947
+ if (!/eslint-disable/.test(lines[i])) {
2948
+ issues.push({
2949
+ file: filePath,
2950
+ line: lineNum,
2951
+ severity: "info",
2952
+ rule: "matcha/no-hardcoded-values",
2953
+ message: "Possible hardcoded numeric value (magic number)",
2954
+ suggestion: "Extract to a named constant"
2955
+ });
2956
+ }
2957
+ }
2958
+ const envMatch = line.match(/process\.env\.([A-Z0-9_]+)/);
2959
+ if (envMatch) {
2960
+ const envName = envMatch[1];
2961
+ if (!envName.includes("_")) {
2962
+ issues.push({
2963
+ file: filePath,
2964
+ line: lineNum,
2965
+ severity: "warning",
2966
+ rule: "matcha/env-prefix",
2967
+ message: `Environment variable "${envName}" doesn't seem to have a prefix`,
2968
+ suggestion: "Use an APPNAME_ prefix for environment variables"
2969
+ });
2970
+ }
2971
+ }
2972
+ }
2973
+ const interfaceNames = [];
2974
+ for (let i = 0; i < lines.length; i++) {
2975
+ const m = lines[i].match(/^\s*(export\s+)?interface\s+(\w+)/);
2976
+ if (m) interfaceNames.push(m[2]);
2977
+ }
2978
+ for (const iface of interfaceNames) {
2979
+ const implCount = (text.match(new RegExp(`implements\\s+.*\\b${iface}\\b`, "g")) || []).length;
2980
+ if (implCount <= 1) {
2981
+ const lineNum = lines.findIndex((l) => l.includes(`interface ${iface}`)) + 1;
2982
+ issues.push({
2983
+ file: filePath,
2984
+ line: lineNum,
2985
+ severity: "info",
2986
+ rule: "matcha/no-premature-abstraction",
2987
+ message: `Interface "${iface}" has only 1 implementation \u2014 premature abstraction?`,
2988
+ suggestion: "Wait for a 2nd use case before abstracting"
2989
+ });
2990
+ }
2991
+ }
2992
+ const funcBodies = findBlockBodies(lines, /^\s*(export\s+)?(async\s+)?function\s+\w+/);
2993
+ for (const { body, nameLine, name } of funcBodies) {
2994
+ const nonEmptyLines = body.split("\n").filter((l) => l.trim()).length;
2995
+ if (nonEmptyLines > 40) {
2996
+ issues.push({
2997
+ file: filePath,
2998
+ line: nameLine,
2999
+ severity: "warning",
3000
+ rule: "matcha/single-responsibility",
3001
+ message: `Function "${name}" is ${nonEmptyLines} lines long \u2014 might be doing >1 thing`,
3002
+ suggestion: "Extract into smaller, focused functions"
3003
+ });
3004
+ }
3005
+ }
3006
+ }
2880
3007
  function formatReviewOutput(issues, filesReviewed, totalFiles, focus, customCriteria, autoDetected) {
2881
3008
  const errors = issues.filter((i) => i.severity === "error");
2882
3009
  const warnings = issues.filter((i) => i.severity === "warning");
@@ -5917,9 +6044,9 @@ function handleCodewhaleSecondary(root, results) {
5917
6044
  });
5918
6045
  }
5919
6046
  }
5920
- function runInit(types, projectRoot) {
5921
- const root = projectRoot ?? getProjectRoot();
5922
- const selected = types.length > 0 ? types : ALL_CONFIG_TYPES;
6047
+ function runInit(options) {
6048
+ const root = options.projectRoot ?? getProjectRoot();
6049
+ const selected = options.types.length > 0 ? options.types : ALL_CONFIG_TYPES;
5923
6050
  const results = [];
5924
6051
  const selectedSet = new Set(selected);
5925
6052
  const agentsMdSelected = AGENTS_MD_TYPES.filter((t) => selectedSet.has(t));
@@ -5933,12 +6060,16 @@ function runInit(types, projectRoot) {
5933
6060
  agentsMdHandled = true;
5934
6061
  const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
5935
6062
  if (fs16.existsSync(fullPath)) {
5936
- const existingContent = fs16.readFileSync(fullPath, "utf-8");
5937
- if (existingContent.includes("_Generated by Kuma MCP_")) {
6063
+ if (options.skipExisting) {
5938
6064
  results.push({ type, filePath: relativePath, action: "skipped" });
5939
6065
  } else {
5940
- fs16.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
5941
- results.push({ type, filePath: relativePath, action: "appended" });
6066
+ const existingContent = fs16.readFileSync(fullPath, "utf-8");
6067
+ if (existingContent.includes("_Generated by Kuma MCP_")) {
6068
+ results.push({ type, filePath: relativePath, action: "skipped" });
6069
+ } else {
6070
+ fs16.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
6071
+ results.push({ type, filePath: relativePath, action: "appended" });
6072
+ }
5942
6073
  }
5943
6074
  } else {
5944
6075
  const dir = path16.dirname(fullPath);
@@ -5955,6 +6086,10 @@ function runInit(types, projectRoot) {
5955
6086
  } else {
5956
6087
  const template = getTemplate();
5957
6088
  if (fs16.existsSync(fullPath)) {
6089
+ if (options.skipExisting) {
6090
+ results.push({ type, filePath: relativePath, action: "skipped" });
6091
+ continue;
6092
+ }
5958
6093
  const existingContent = fs16.readFileSync(fullPath, "utf-8");
5959
6094
  if (existingContent.includes("_Generated by Kuma MCP_")) {
5960
6095
  if (type === "antigravity") {
@@ -6054,6 +6189,8 @@ Usage:
6054
6189
  npx @plumpslabs/kuma Start MCP server (default)
6055
6190
  npx @plumpslabs/kuma init Generate AI agent config files
6056
6191
  npx @plumpslabs/kuma init --all Generate ALL config files
6192
+ npx @plumpslabs/kuma init --merge Append to existing files (default)
6193
+ npx @plumpslabs/kuma init --skip-existing Skip generation if file exists
6057
6194
  npx @plumpslabs/kuma init --claude --cursor Generate specific files
6058
6195
  npx @plumpslabs/kuma init --help Show this help
6059
6196
 
@@ -6127,9 +6264,20 @@ async function main() {
6127
6264
  }
6128
6265
  }
6129
6266
  }
6130
- const results = runInit(selectedTypes, process.cwd());
6267
+ const skipExisting = requestedFlags.includes("--skip-existing");
6268
+ const merge = requestedFlags.includes("--merge");
6269
+ const results = runInit({ types: selectedTypes, projectRoot: process.cwd(), skipExisting });
6131
6270
  const output = formatInitResults(results);
6132
6271
  console.log(output);
6272
+ const fs17 = await import("fs");
6273
+ const path17 = await import("path");
6274
+ const matchaSkills = path17.resolve(process.cwd(), "skills/matcha/SKILL.md");
6275
+ const matchaAgents = path17.resolve(process.cwd(), ".agents/skills/matcha/SKILL.md");
6276
+ const matchaCursor = path17.resolve(process.cwd(), ".cursor/rules/matcha.mdc");
6277
+ const matchaWindsurf = path17.resolve(process.cwd(), ".windsurf/rules/matcha.md");
6278
+ if (fs17.existsSync(matchaSkills) || fs17.existsSync(matchaAgents) || fs17.existsSync(matchaCursor) || fs17.existsSync(matchaWindsurf)) {
6279
+ console.error("\n\u{1F375} Hey, I see matcha is installed \u2014 they pair well together!");
6280
+ }
6133
6281
  process.exit(0);
6134
6282
  }
6135
6283
  sessionMemory.init({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumpslabs/kuma",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "Zero-setup safety toolkit for AI coding agents. MCP server with code review, git analysis, file editing, session memory, and static analysis — works with Claude Code, Cursor, Gemini CLI.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -56,7 +56,7 @@
56
56
  "git-diff",
57
57
  "cli"
58
58
  ],
59
- "repository": {
59
+ "repository": {
60
60
  "type": "git",
61
61
  "url": "git+https://github.com/plumpslabs/kuma.git"
62
62
  },
@@ -65,4 +65,4 @@
65
65
  },
66
66
  "homepage": "https://github.com/plumpslabs/kuma#readme",
67
67
  "license": "MIT"
68
- }
68
+ }