@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.
- package/dist/index.js +163 -15
- 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
|
-
"
|
|
2095
|
-
|
|
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
|
|
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
|
|
2863
|
-
const line = lines[
|
|
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(
|
|
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
|
-
|
|
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.
|
|
5941
|
-
|
|
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
|
|
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.
|
|
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
|
+
}
|