@plumpslabs/kuma 2.1.5 → 2.1.8

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 +25 -0
  2. package/dist/index.js +149 -107
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -166,6 +166,31 @@ Most tools make AI smarter. **Kuma makes AI not break things.**
166
166
 
167
167
  ---
168
168
 
169
+ ---
170
+
171
+ ## 🍵 Pair with Matcha
172
+
173
+ **Kuma keeps AI agents safe. Matcha keeps AI agents deliberate.**
174
+
175
+ [Matcha](https://github.com/plumpslabs/matcha) is an engineering philosophy
176
+ ruleset that enforces deliberate thinking before, during, and after coding:
177
+
178
+ - **5W1H Gate** — Why are we doing this? Is there a simpler path?
179
+ - **Reuse Before Write** — Never write what already exists
180
+ - **Clean Finish** — No temp, no debug, no unused code
181
+
182
+ Where Kuma provides **runtime safety** (rollback, circuit breaker, sandbox),
183
+ Matcha provides **session discipline** (planning gate, cleanup scan, intensity levels).
184
+
185
+ ```bash
186
+ # Try them together
187
+ npx @plumpslabs/matcha init # Install matcha philosophy
188
+ npx @plumpslabs/kuma init --all # Install kuma safety tools
189
+ ```
190
+
191
+ Both tools are designed to complement each other — Kuma handles the
192
+ "can't break things" layer while Matcha handles the "think before you act" layer.
193
+
169
194
  ## Contributing
170
195
 
171
196
  See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
package/dist/index.js CHANGED
@@ -3662,7 +3662,10 @@ async function handleStaticAnalysis(params) {
3662
3662
  function detectAvailableTools(root) {
3663
3663
  const tools = [];
3664
3664
  const pkg = readPackageJson(root);
3665
- const allDeps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
3665
+ const allDeps = {
3666
+ ...pkg?.dependencies ?? {},
3667
+ ...pkg?.devDependencies ?? {}
3668
+ };
3666
3669
  const depNames = new Set(Object.keys(allDeps));
3667
3670
  const eslintConfigs = [
3668
3671
  ".eslintrc",
@@ -3974,80 +3977,140 @@ function formatToolNotAvailable(requested, available) {
3974
3977
  ].join("\n");
3975
3978
  }
3976
3979
 
3977
- // src/tools/kumaReflect.ts
3980
+ // src/utils/kumaShared.ts
3978
3981
  import { execSync as execSync3 } from "child_process";
3979
- async function handleReflect(params) {
3982
+ function getSessionStats(inputGoal) {
3980
3983
  const summary = sessionMemory.getSummary();
3981
- const goal = params.goal || summary.currentGoal || "";
3984
+ const goal = inputGoal || summary.currentGoal || "";
3982
3985
  const modifiedFiles = sessionMemory.getModifiedFiles();
3983
3986
  const toolCalls = sessionMemory.getToolCallHistory(50);
3984
3987
  const failedFiles = sessionMemory.getFailedFiles();
3985
3988
  const loop = sessionMemory.detectLoop();
3986
- const unresolved = [];
3987
- for (const f of failedFiles) {
3988
- for (const ff of f.failures) {
3989
- if (!ff.resolved) {
3990
- unresolved.push({ task: f.task, error: ff.error.substring(0, 200) });
3991
- }
3992
- }
3993
- }
3994
- const hasRunTests = toolCalls.some(
3995
- (c) => c.toolName === "execute_safe_test"
3996
- );
3997
- let gitStat = "";
3989
+ return {
3990
+ goal,
3991
+ modifiedFiles,
3992
+ toolCalls,
3993
+ toolCallCount: toolCalls.length,
3994
+ failedFiles,
3995
+ hasLoop: loop.isLooping,
3996
+ loopMessage: loop.message,
3997
+ hasRunTests: toolCalls.some((c) => c.toolName === "execute_safe_test")
3998
+ };
3999
+ }
4000
+ function getGitDiffStat(timeout = 3e3) {
3998
4001
  try {
3999
4002
  const root = getProjectRoot();
4000
- gitStat = execSync3("git diff --stat", {
4003
+ return execSync3("git diff --stat", {
4001
4004
  cwd: root,
4002
4005
  encoding: "utf-8",
4003
- timeout: 5e3
4006
+ timeout
4004
4007
  }).trim();
4005
4008
  } catch {
4009
+ return "";
4006
4010
  }
4007
- const drifts = [];
4008
- if (modifiedFiles.length > 0 && !hasRunTests) {
4009
- drifts.push(`${modifiedFiles.length} file(s) edited but no test run`);
4010
- }
4011
- if (loop.isLooping) {
4012
- drifts.push(loop.message);
4013
- }
4014
- if (unresolved.length > 0) {
4015
- drifts.push(`${unresolved.length} unresolved failure(s)`);
4011
+ }
4012
+ function getUnresolvedCount(failedFiles) {
4013
+ let count = 0;
4014
+ for (const f of failedFiles) {
4015
+ for (const ff of f.failures) {
4016
+ if (!ff.resolved) count++;
4017
+ }
4016
4018
  }
4017
- if (gitStat) {
4018
- drifts.push(`Git diff: ${gitStat}`);
4019
+ return count;
4020
+ }
4021
+ function getUnresolvedDetails(failedFiles) {
4022
+ const result = [];
4023
+ for (const f of failedFiles) {
4024
+ for (const ff of f.failures) {
4025
+ if (!ff.resolved) {
4026
+ result.push({ task: f.task, error: ff.error.substring(0, 200) });
4027
+ }
4028
+ }
4019
4029
  }
4020
- const ladderViolations = [];
4030
+ return result;
4031
+ }
4032
+ function checkLadderViolations(toolCalls, modifiedFiles, hasRunTests) {
4033
+ const violations = [];
4021
4034
  const editCalls = toolCalls.filter(
4022
4035
  (c) => c.toolName === "precise_diff_editor" || c.toolName === "batch_file_writer"
4023
4036
  ).length;
4024
4037
  if (editCalls > 5) {
4025
- ladderViolations.push(
4026
- `${editCalls} file ops in a row \u2014 consider if all are needed`
4027
- );
4038
+ violations.push(`${editCalls} file ops in a row \u2014 consider if all are needed`);
4028
4039
  }
4029
4040
  if (modifiedFiles.length > 5 && !hasRunTests) {
4030
- ladderViolations.push(
4031
- `${modifiedFiles.length} files modified without verification`
4032
- );
4041
+ violations.push(`${modifiedFiles.length} files modified without verification`);
4042
+ }
4043
+ return violations;
4044
+ }
4045
+ function buildDriftMessages(modifiedFiles, hasRunTests, unresolvedCount, gitStat, loopMessage) {
4046
+ const drifts = [];
4047
+ if (modifiedFiles > 0 && !hasRunTests) {
4048
+ drifts.push(`${modifiedFiles} file(s) edited but no test run`);
4049
+ }
4050
+ if (loopMessage) {
4051
+ drifts.push(loopMessage);
4052
+ }
4053
+ if (unresolvedCount > 0) {
4054
+ drifts.push(`${unresolvedCount} unresolved failure(s)`);
4055
+ }
4056
+ if (gitStat) {
4057
+ drifts.push(`Git diff: ${gitStat}`);
4058
+ }
4059
+ return drifts;
4060
+ }
4061
+ function getPrioritySuggestion(goal, warnings, hasLoop, unresolvedCount, modifiedFiles, hasRunTests, editCalls) {
4062
+ if (warnings.some((w) => w.severity === "high" && w.pattern === "script-patching")) {
4063
+ return "Remove patch scripts and use precise_diff_editor for all file modifications";
4064
+ }
4065
+ if (hasLoop) {
4066
+ return "Switch approach \u2014 current tool is not making progress";
4067
+ }
4068
+ if (warnings.some((w) => w.pattern === "no-test-after-edit") || modifiedFiles > 0 && !hasRunTests) {
4069
+ return "Run tests to verify your changes before continuing";
4070
+ }
4071
+ if (unresolvedCount > 0) {
4072
+ return "Fix unresolved failures before continuing";
4073
+ }
4074
+ if (warnings.some((w) => w.pattern === "bash-grep")) {
4075
+ return "Use smart_grep for code search instead of bash grep";
4076
+ }
4077
+ if (warnings.some((w) => w.pattern === "excessive-edits") || editCalls > 10) {
4078
+ return "Consider if refactoring can be simplified \u2014 fewer files = fewer bugs";
4033
4079
  }
4034
- const onTrack = !loop.isLooping && unresolved.length === 0 && (modifiedFiles.length === 0 || hasRunTests);
4035
- let suggestion;
4036
4080
  if (!goal) {
4037
- suggestion = "No goal set \u2014 use goal parameter or setGoal";
4038
- } else if (loop.isLooping) {
4039
- suggestion = "Switch approach \u2014 current tool is not making progress";
4040
- } else if (unresolved.length > 0) {
4041
- suggestion = "Fix unresolved failures before continuing";
4042
- } else if (modifiedFiles.length > 0 && !hasRunTests) {
4043
- suggestion = "Run tests to verify changes";
4044
- } else if (modifiedFiles.length === 0 && !toolCalls.some((c) => ["smart_file_picker", "smart_grep"].includes(c.toolName))) {
4045
- suggestion = "Start by exploring what exists before writing code";
4046
- } else if (editCalls > 10) {
4047
- suggestion = "Consider if refactoring can be simplified \u2014 fewer files = fewer bugs";
4048
- } else {
4049
- suggestion = "On track";
4081
+ return "No goal set \u2014 use goal parameter or setGoal to track intent";
4050
4082
  }
4083
+ if (modifiedFiles === 0) {
4084
+ return "Start by exploring what exists before writing code";
4085
+ }
4086
+ return "On track \u2014 continue with current approach";
4087
+ }
4088
+
4089
+ // src/tools/kumaReflect.ts
4090
+ async function handleReflect(params) {
4091
+ const stats = getSessionStats(params.goal);
4092
+ const unresolved = getUnresolvedDetails(stats.failedFiles);
4093
+ const gitStat = getGitDiffStat(5e3);
4094
+ const ladderViolations = checkLadderViolations(stats.toolCalls, stats.modifiedFiles, stats.hasRunTests);
4095
+ const drifts = buildDriftMessages(
4096
+ stats.modifiedFiles.length,
4097
+ stats.hasRunTests,
4098
+ unresolved.length,
4099
+ gitStat,
4100
+ stats.loopMessage
4101
+ );
4102
+ const onTrack = !stats.hasLoop && unresolved.length === 0 && (stats.modifiedFiles.length === 0 || stats.hasRunTests);
4103
+ const suggestion = getPrioritySuggestion(
4104
+ stats.goal,
4105
+ [],
4106
+ stats.hasLoop,
4107
+ unresolved.length,
4108
+ stats.modifiedFiles.length,
4109
+ stats.hasRunTests,
4110
+ stats.toolCalls.filter(
4111
+ (c) => c.toolName === "precise_diff_editor" || c.toolName === "batch_file_writer"
4112
+ ).length
4113
+ );
4051
4114
  return JSON.stringify(
4052
4115
  {
4053
4116
  onTrack,
@@ -4055,12 +4118,12 @@ async function handleReflect(params) {
4055
4118
  ...ladderViolations.length > 0 ? { ladderViolations } : {},
4056
4119
  suggestion,
4057
4120
  stats: {
4058
- goal,
4059
- modifiedFiles: modifiedFiles.length,
4060
- toolCalls: toolCalls.length,
4121
+ goal: stats.goal,
4122
+ modifiedFiles: stats.modifiedFiles.length,
4123
+ toolCalls: stats.toolCallCount,
4061
4124
  unresolvedFailures: unresolved.length,
4062
- hasLoop: loop.isLooping,
4063
- hasRunTests
4125
+ hasLoop: stats.hasLoop,
4126
+ hasRunTests: stats.hasRunTests
4064
4127
  }
4065
4128
  },
4066
4129
  null,
@@ -4068,9 +4131,6 @@ async function handleReflect(params) {
4068
4131
  );
4069
4132
  }
4070
4133
 
4071
- // src/tools/kumaGuard.ts
4072
- import { execSync as execSync6 } from "child_process";
4073
-
4074
4134
  // src/guards/antiPatternDetector.ts
4075
4135
  import fs11 from "fs";
4076
4136
  import path11 from "path";
@@ -4299,7 +4359,7 @@ function saveSnapshot(goal) {
4299
4359
  modifiedFiles: summary.modifiedFiles || [],
4300
4360
  unresolvedFailures: summary.unresolvedFailures || [],
4301
4361
  toolCallCount: summary.toolCallCount || 0,
4302
- gitDiffStat: getGitDiffStat(),
4362
+ gitDiffStat: getGitDiffStat2(),
4303
4363
  completedSteps: summary.completedSteps || [],
4304
4364
  hasConventions: !!summary.hasConventions
4305
4365
  };
@@ -4357,7 +4417,7 @@ function formatSnapshot(snapshot) {
4357
4417
  lines.push('\u{1F4A1} Use kuma_guard({ check: "context" }) to create a new snapshot.');
4358
4418
  return lines.join("\n");
4359
4419
  }
4360
- function getGitDiffStat() {
4420
+ function getGitDiffStat2() {
4361
4421
  try {
4362
4422
  const root = getProjectRoot();
4363
4423
  const stdout = execSync5("git diff --stat", {
@@ -4374,9 +4434,8 @@ function getGitDiffStat() {
4374
4434
  // src/tools/kumaGuard.ts
4375
4435
  async function handleKumaGuard(params) {
4376
4436
  const { check = "all", goal: inputGoal } = params;
4377
- const summary = sessionMemory.getSummary();
4378
- const goal = inputGoal || summary.currentGoal || "";
4379
- sessionMemory.recordToolCall("kuma_guard", { check, goal });
4437
+ sessionMemory.recordToolCall("kuma_guard", { check, goal: inputGoal });
4438
+ const stats = getSessionStats(inputGoal);
4380
4439
  const warnings = [];
4381
4440
  if (check === "all" || check === "anti-pattern") {
4382
4441
  warnings.push(...detectAllAntiPatterns());
@@ -4391,44 +4450,26 @@ async function handleKumaGuard(params) {
4391
4450
  });
4392
4451
  }
4393
4452
  const drifts = [];
4394
- const toolCalls = sessionMemory.getToolCallHistory(50);
4395
- const hasRunTests = toolCalls.some((c) => c.toolName === "execute_safe_test");
4396
4453
  if (check === "all" || check === "drift") {
4397
- const modifiedFiles = sessionMemory.getModifiedFiles();
4398
- const failedFiles = sessionMemory.getFailedFiles();
4399
- let unresolvedCount = 0;
4400
- for (const f of failedFiles) {
4401
- for (const ff of f.failures) {
4402
- if (!ff.resolved) unresolvedCount++;
4403
- }
4404
- }
4405
- if (modifiedFiles.length > 0 && !hasRunTests) {
4406
- drifts.push(`${modifiedFiles.length} file(s) edited but no test run`);
4454
+ const unresolvedCount = getUnresolvedCount(stats.failedFiles);
4455
+ const gitStat = getGitDiffStat();
4456
+ const editCalls = stats.toolCalls.filter(
4457
+ (c) => c.toolName === "precise_diff_editor" || c.toolName === "batch_file_writer"
4458
+ ).length;
4459
+ drifts.push(...buildDriftMessages(
4460
+ stats.modifiedFiles.length,
4461
+ stats.hasRunTests,
4462
+ unresolvedCount,
4463
+ gitStat
4464
+ ));
4465
+ if (stats.modifiedFiles.length > 0 && !stats.hasRunTests) {
4407
4466
  warnings.push({
4408
4467
  severity: "medium",
4409
4468
  pattern: "no-test-after-edit",
4410
- message: `${modifiedFiles.length} file(s) modified without running tests`,
4469
+ message: `${stats.modifiedFiles.length} file(s) modified without running tests`,
4411
4470
  suggestion: 'Run execute_safe_test({ task: "typecheck" }) to verify changes'
4412
4471
  });
4413
4472
  }
4414
- if (unresolvedCount > 0) {
4415
- drifts.push(`${unresolvedCount} unresolved failure(s)`);
4416
- }
4417
- try {
4418
- const root = getProjectRoot();
4419
- const gitStat = execSync6("git diff --stat", {
4420
- cwd: root,
4421
- encoding: "utf-8",
4422
- timeout: 3e3
4423
- }).trim();
4424
- if (gitStat) {
4425
- drifts.push(`Git diff: ${gitStat}`);
4426
- }
4427
- } catch {
4428
- }
4429
- const editCalls = toolCalls.filter(
4430
- (c) => c.toolName === "precise_diff_editor" || c.toolName === "batch_file_writer"
4431
- ).length;
4432
4473
  if (editCalls > 5) {
4433
4474
  warnings.push({
4434
4475
  severity: "low",
@@ -4439,7 +4480,7 @@ async function handleKumaGuard(params) {
4439
4480
  }
4440
4481
  }
4441
4482
  if (check === "context") {
4442
- const snapshot = saveSnapshot(goal);
4483
+ const snapshot = saveSnapshot(stats.goal);
4443
4484
  if (!snapshot) {
4444
4485
  return "\u26A0\uFE0F Could not create context snapshot. The .kuma directory might not be accessible.";
4445
4486
  }
@@ -4459,7 +4500,7 @@ async function handleKumaGuard(params) {
4459
4500
  suggestion = "Use smart_grep for code search instead of bash grep";
4460
4501
  } else if (warnings.some((w) => w.pattern === "excessive-edits")) {
4461
4502
  suggestion = "Pause and review: are all these edits necessary?";
4462
- } else if (!goal) {
4503
+ } else if (!stats.goal) {
4463
4504
  suggestion = "No goal set \u2014 use goal parameter or setGoal to track intent";
4464
4505
  } else {
4465
4506
  suggestion = "On track \u2014 continue with current approach";
@@ -4471,12 +4512,12 @@ async function handleKumaGuard(params) {
4471
4512
  drifts,
4472
4513
  suggestion,
4473
4514
  stats: {
4474
- goal,
4475
- modifiedFiles: summary.modifiedFiles.length,
4476
- toolCalls: summary.toolCallCount ?? 0,
4477
- unresolvedFailures: summary.unresolvedFailures ? summary.unresolvedFailures.length : 0,
4515
+ goal: stats.goal,
4516
+ modifiedFiles: stats.modifiedFiles.length,
4517
+ toolCalls: stats.toolCallCount,
4518
+ unresolvedFailures: getUnresolvedCount(stats.failedFiles),
4478
4519
  hasLoop: loop.isLooping,
4479
- hasRunTests
4520
+ hasRunTests: stats.hasRunTests
4480
4521
  }
4481
4522
  };
4482
4523
  return JSON.stringify(report, null, 2);
@@ -5826,15 +5867,16 @@ async function main() {
5826
5867
  process.cwd(),
5827
5868
  ".agents/skills/matcha/SKILL.md"
5828
5869
  );
5829
- const matchaCursor = path16.resolve(
5870
+ const matchaRootSkills = path16.resolve(
5830
5871
  process.cwd(),
5831
- ".cursor/rules/matcha.mdc"
5872
+ "skills/matcha/SKILL.md"
5832
5873
  );
5833
- const matchaWindsurf = path16.resolve(
5874
+ const matchaAgentsMd = path16.resolve(process.cwd(), "AGENTS.md");
5875
+ const matchaWindsurfRules = path16.resolve(
5834
5876
  process.cwd(),
5835
- ".windsurf/rules/matcha.md"
5877
+ ".windsurfrules"
5836
5878
  );
5837
- if (fs16.existsSync(matchaSkills) || fs16.existsSync(matchaAgents) || fs16.existsSync(matchaCursor) || fs16.existsSync(matchaWindsurf)) {
5879
+ if (fs16.existsSync(matchaSkills) || fs16.existsSync(matchaAgents) || fs16.existsSync(matchaRootSkills) || fs16.existsSync(matchaAgentsMd) || fs16.existsSync(matchaWindsurfRules)) {
5838
5880
  console.error(
5839
5881
  "\n\u{1F375} Hey, I see matcha is installed \u2014 they pair well together!"
5840
5882
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumpslabs/kuma",
3
- "version": "2.1.5",
3
+ "version": "2.1.8",
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",
@@ -16,7 +16,7 @@
16
16
  "dev": "tsup src/index.ts --format esm --watch --out-dir dist",
17
17
  "start": "node dist/index.js",
18
18
  "typecheck": "tsc --noEmit",
19
- "lint": "eslint src/",
19
+ "lint": "echo 'No linting configured'",
20
20
  "prepublishOnly": "npm run build",
21
21
  "test": "node --experimental-vm-modules node_modules/.bin/jest"
22
22
  },
@@ -33,6 +33,7 @@
33
33
  "@types/node": "^22.0.0",
34
34
  "jest": "^29.7.0",
35
35
  "ts-jest": "^29.2.0",
36
+ "ts-node": "^10.9.2",
36
37
  "tsup": "^8.3.0",
37
38
  "typescript": "^5.9.3"
38
39
  },