claude-crap 0.4.2 → 0.4.4

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/CHANGELOG.md CHANGED
@@ -5,6 +5,104 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.4] - 2026-04-13
9
+
10
+ ### Fixed
11
+
12
+ - **MCP server workspace root resolution (follow-up to 0.4.3)** —
13
+ v0.4.3 attempted to fix the workspace-cache bug by setting
14
+ `"CLAUDE_PROJECT_DIR": "${CLAUDE_PROJECT_DIR}"` in `plugin/.mcp.json`
15
+ env, on the assumption that Claude Code would expand the variable.
16
+ It does not — only `${CLAUDE_PLUGIN_ROOT}` is expanded inside
17
+ `.mcp.json`. The literal string `${CLAUDE_PROJECT_DIR}` was passed
18
+ through to `process.env.CLAUDE_PROJECT_DIR`, the MCP server happily
19
+ used it as a path, and `auto_scan` returned 0 files because the
20
+ "workspace" was a literal template string. This release fixes that:
21
+ - **Reverted** the broken `.mcp.json` env entry.
22
+ - **Defensive `${...}` literal detection.** Any env var whose value
23
+ matches `^${VAR_NAME}$` is treated as unset by `sanitizeEnvPath()`,
24
+ so an unexpanded template can never leak through as a "valid" path.
25
+ - **Parent-cwd fallback.** `loadConfig()` now delegates to
26
+ `discoverWorkspaceRoot()`, which walks a 4-strategy chain:
27
+ `CLAUDE_PROJECT_DIR` → `CLAUDE_CRAP_PLUGIN_ROOT` → parent process
28
+ cwd (via `lsof` on macOS, `/proc/<pid>/cwd` on Linux) →
29
+ `process.cwd()`. Claude Code's cwd IS the user's workspace, so the
30
+ parent-cwd probe is the most reliable fallback when env-var
31
+ inheritance is unavailable.
32
+
33
+ ### Added
34
+
35
+ - **`discoverWorkspaceRoot(options)`** export from `src/config.ts` with
36
+ an injectable `readParentCwd` for testability.
37
+ - **11 new characterization tests** in `src/tests/config.test.ts`
38
+ covering literal `${...}` detection on every source, parent-cwd
39
+ fallback ordering, and the `process.cwd()` last-resort. Suite total:
40
+ **366 tests across 95 suites**.
41
+
42
+ ## [0.4.3] - 2026-04-13
43
+
44
+ ### Fixed
45
+
46
+ - **MCP server workspace root (incomplete fix)** — `loadConfig()` now
47
+ reads `CLAUDE_PROJECT_DIR` ahead of `CLAUDE_CRAP_PLUGIN_ROOT` and
48
+ `process.cwd()`, so SARIF reports stop landing inside the plugin
49
+ cache directory.
50
+ - **`.mcp.json` env pass-through** — added an explicit
51
+ `"CLAUDE_PROJECT_DIR": "${CLAUDE_PROJECT_DIR}"` entry to forward the
52
+ variable to the MCP server process.
53
+ > **Known issue (fixed in 0.4.4):** Claude Code does NOT expand
54
+ > `${CLAUDE_PROJECT_DIR}` inside `.mcp.json` env (only
55
+ > `${CLAUDE_PLUGIN_ROOT}` is expanded), so the literal template
56
+ > string was passed through and broke workspace resolution
57
+ > completely. Upgrade to 0.4.4.
58
+
59
+ ## [0.4.2] - 2026-04-13
60
+
61
+ ### Changed
62
+
63
+ - **Default strictness changed from `strict` to `warn`** so existing
64
+ projects with findings can adopt the plugin without being hard-blocked
65
+ on every task close. Teams that want hard enforcement set
66
+ `"strictness": "strict"` in `.claude-crap.json` explicitly.
67
+
68
+ ### Fixed
69
+
70
+ - **Plugin infrastructure directories excluded from scans** — added
71
+ `plugin/`, `hooks/`, `skills/`, `.claude-plugin/`, and `.claude-sonar/`
72
+ to `DEFAULT_SKIP_DIRS`. These directories used to inflate workspace
73
+ LOC totals and produce false complexity hotspots in the dashboard.
74
+ The hooks-layer `LOC_WALK_SKIP_DIRS` was synced to match so the Stop
75
+ quality gate computes TDR over the same file set as the MCP server.
76
+
77
+ ## [0.4.1] - 2026-04-13
78
+
79
+ ### Fixed
80
+
81
+ - **Launcher dependency validation** — the launcher now verifies that
82
+ every externalized package (`@modelcontextprotocol/sdk`, `fastify`,
83
+ `@fastify/static`, `pino`, `web-tree-sitter`, `tree-sitter-wasms`,
84
+ `picomatch`) actually exists inside `node_modules/` instead of just
85
+ checking that the directory is present. Fixes the case where a new
86
+ dependency was added but cached `node_modules/` is stale.
87
+ - **Cache sync clears stale `node_modules/`** — `build:plugin` now
88
+ deletes `node_modules/` in every cached plugin version after copying
89
+ updated files, forcing the launcher to re-install with the correct
90
+ dependency set on the next session boot.
91
+
92
+ ### Changed
93
+
94
+ - **`handleToolCall` refactored** — the 10-case switch (cyclomatic
95
+ complexity 32) was extracted into 10 named handlers, leaving
96
+ `handleToolCall` as a thin dispatcher (CC 11). Each handler handles
97
+ exactly one MCP tool and is independently testable.
98
+
99
+ ### Added
100
+
101
+ - **`docs/supported-languages.md`** — per-language setup, detection
102
+ rules, scanner behavior, monorepo discovery, file exclusions, and a
103
+ walkthrough for adding a new language. Linked from the README
104
+ Documentation section.
105
+
8
106
  ## [0.4.0] - 2026-04-13
9
107
 
10
108
  ### Added
package/README.md CHANGED
@@ -240,7 +240,7 @@ added via `.claude-crap.json`:
240
240
 
241
241
  ```bash
242
242
  npm install # postinstall builds dist/ automatically
243
- npm test # 339 tests across 89 suites
243
+ npm test # 366 tests across 95 suites
244
244
  npm run build:fast # esbuild dev build (10-20x faster than tsc)
245
245
  npm run doctor # full diagnostic
246
246
  ```
package/dist/config.d.ts CHANGED
@@ -9,8 +9,8 @@
9
9
  * user settings → plugin.json "options" → .mcp.json "env" → this file
10
10
  *
11
11
  * If any environment variable is missing or empty, a safe default is used,
12
- * but the loader NEVER invents stochastic values and NEVER performs I/O.
13
- * This module is the single source of truth for runtime configuration.
12
+ * but the loader NEVER invents stochastic values. This module is the
13
+ * single source of truth for runtime configuration.
14
14
  *
15
15
  * @module config
16
16
  */
@@ -28,7 +28,7 @@ export type MaintainabilityRating = "A" | "B" | "C" | "D" | "E";
28
28
  * configuration at runtime — any change must go through a server restart.
29
29
  */
30
30
  export interface CrapConfig {
31
- /** Absolute path to the plugin root on disk. Defaults to `process.cwd()`. */
31
+ /** Absolute path to the user's workspace. Resolved via {@link discoverWorkspaceRoot}. */
32
32
  readonly pluginRoot: string;
33
33
  /** Directory (relative to the workspace) where consolidated SARIF reports are written. */
34
34
  readonly sarifOutputDir: string;
@@ -43,6 +43,32 @@ export interface CrapConfig {
43
43
  /** Local TCP port the Vue.js dashboard will bind to. */
44
44
  readonly dashboardPort: number;
45
45
  }
46
+ /**
47
+ * Parameters accepted by {@link discoverWorkspaceRoot}. The only field is
48
+ * an injectable `readParentCwd` implementation so that tests can pin the
49
+ * fallback behavior without spawning `lsof` or reading `/proc`.
50
+ */
51
+ export interface DiscoverWorkspaceOptions {
52
+ readParentCwd?: () => string | undefined;
53
+ }
54
+ /**
55
+ * Resolve the user's workspace directory. Strategy, in strict priority
56
+ * order:
57
+ *
58
+ * 1. `CLAUDE_PROJECT_DIR` (sanitized — ignored if it's `${...}`)
59
+ * 2. `CLAUDE_CRAP_PLUGIN_ROOT` (sanitized — legacy explicit override)
60
+ * 3. Parent process cwd (Claude Code's cwd = the workspace)
61
+ * 4. `process.cwd()` (last-resort fallback; usually wrong for
62
+ * MCP servers because Claude Code sets
63
+ * cwd to the plugin cache directory)
64
+ *
65
+ * This function NEVER returns an unexpanded `${...}` template; any source
66
+ * that contains one is skipped as if it were unset.
67
+ *
68
+ * @param options Injection points for tests.
69
+ * @returns A concrete filesystem path.
70
+ */
71
+ export declare function discoverWorkspaceRoot(options?: DiscoverWorkspaceOptions): string;
46
72
  /**
47
73
  * Build the complete {@link CrapConfig} from the current process environment.
48
74
  *
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0FAA0F;IAC1F,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gGAAgG;IAChG,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC;IAC7C,0FAA0F;IAC1F,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,wDAAwD;IACxD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AA0CD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,IAAI,UAAU,CAUvC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0FAA0F;IAC1F,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gGAAgG;IAChG,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC;IAC7C,0FAA0F;IAC1F,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,wDAAwD;IACxD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAmED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,aAAa,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,wBAA6B,GAAG,MAAM,CAapF;AA0CD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,IAAI,UAAU,CAUvC"}
package/dist/config.js CHANGED
@@ -9,11 +9,103 @@
9
9
  * user settings → plugin.json "options" → .mcp.json "env" → this file
10
10
  *
11
11
  * If any environment variable is missing or empty, a safe default is used,
12
- * but the loader NEVER invents stochastic values and NEVER performs I/O.
13
- * This module is the single source of truth for runtime configuration.
12
+ * but the loader NEVER invents stochastic values. This module is the
13
+ * single source of truth for runtime configuration.
14
14
  *
15
15
  * @module config
16
16
  */
17
+ import { execFileSync } from "node:child_process";
18
+ import { readlinkSync } from "node:fs";
19
+ /**
20
+ * Detects an unexpanded `.mcp.json` variable template such as
21
+ * `${CLAUDE_PROJECT_DIR}`. Claude Code only expands `${CLAUDE_PLUGIN_ROOT}`
22
+ * inside `.mcp.json`; every other `${VAR}` is passed through verbatim and
23
+ * must NOT be treated as a real filesystem path.
24
+ */
25
+ function isLiteralVarTemplate(value) {
26
+ if (value === undefined)
27
+ return false;
28
+ return /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value.trim());
29
+ }
30
+ /**
31
+ * Normalize an environment variable that is expected to contain a path.
32
+ * Returns `undefined` if the value is missing, empty, or an unexpanded
33
+ * `${...}` template. Any non-empty concrete string is returned as-is.
34
+ */
35
+ function sanitizeEnvPath(value) {
36
+ if (value === undefined || value === "")
37
+ return undefined;
38
+ if (isLiteralVarTemplate(value))
39
+ return undefined;
40
+ return value;
41
+ }
42
+ /**
43
+ * Read the current working directory of the parent process. Claude Code
44
+ * spawns MCP servers with its own cwd set to the user's workspace, so the
45
+ * parent's cwd is the most reliable fallback when `CLAUDE_PROJECT_DIR` is
46
+ * not inherited (e.g. because Claude Code only exports it for hooks).
47
+ *
48
+ * Returns `undefined` on any platform or failure mode the probe cannot
49
+ * handle — callers must be prepared for a missing result.
50
+ */
51
+ function readParentCwdDefault() {
52
+ try {
53
+ const ppid = process.ppid;
54
+ if (!ppid || ppid === 0)
55
+ return undefined;
56
+ if (process.platform === "linux") {
57
+ // /proc/<pid>/cwd is a symlink to the process's cwd.
58
+ return readlinkSync(`/proc/${ppid}/cwd`);
59
+ }
60
+ if (process.platform === "darwin") {
61
+ // `lsof -a -p <pid> -d cwd -F n` emits a single line starting with
62
+ // `n<path>` for the cwd file descriptor. `-F` keeps the output
63
+ // machine-readable.
64
+ const output = execFileSync("lsof", ["-a", "-p", String(ppid), "-d", "cwd", "-F", "n"], {
65
+ encoding: "utf8",
66
+ stdio: ["ignore", "pipe", "ignore"],
67
+ timeout: 2000,
68
+ });
69
+ const match = output.match(/^n(.+)$/m);
70
+ return match?.[1];
71
+ }
72
+ // Windows and other platforms: no reliable no-dep probe.
73
+ return undefined;
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ }
79
+ /**
80
+ * Resolve the user's workspace directory. Strategy, in strict priority
81
+ * order:
82
+ *
83
+ * 1. `CLAUDE_PROJECT_DIR` (sanitized — ignored if it's `${...}`)
84
+ * 2. `CLAUDE_CRAP_PLUGIN_ROOT` (sanitized — legacy explicit override)
85
+ * 3. Parent process cwd (Claude Code's cwd = the workspace)
86
+ * 4. `process.cwd()` (last-resort fallback; usually wrong for
87
+ * MCP servers because Claude Code sets
88
+ * cwd to the plugin cache directory)
89
+ *
90
+ * This function NEVER returns an unexpanded `${...}` template; any source
91
+ * that contains one is skipped as if it were unset.
92
+ *
93
+ * @param options Injection points for tests.
94
+ * @returns A concrete filesystem path.
95
+ */
96
+ export function discoverWorkspaceRoot(options = {}) {
97
+ const readParentCwd = options.readParentCwd ?? readParentCwdDefault;
98
+ const fromProjectDir = sanitizeEnvPath(process.env.CLAUDE_PROJECT_DIR);
99
+ if (fromProjectDir)
100
+ return fromProjectDir;
101
+ const fromPluginRoot = sanitizeEnvPath(process.env.CLAUDE_CRAP_PLUGIN_ROOT);
102
+ if (fromPluginRoot)
103
+ return fromPluginRoot;
104
+ const fromParent = sanitizeEnvPath(readParentCwd());
105
+ if (fromParent)
106
+ return fromParent;
107
+ return process.cwd();
108
+ }
17
109
  /**
18
110
  * Parse a numeric environment variable, falling back to `fallback` when the
19
111
  * variable is undefined or empty. Throws if the value is present but not a
@@ -66,7 +158,7 @@ function parseRating(raw, fallback) {
66
158
  */
67
159
  export function loadConfig() {
68
160
  return {
69
- pluginRoot: process.env.CLAUDE_CRAP_PLUGIN_ROOT ?? process.cwd(),
161
+ pluginRoot: discoverWorkspaceRoot(),
70
162
  sarifOutputDir: process.env.CLAUDE_CRAP_SARIF_OUTPUT_DIR ?? ".claude-crap/reports",
71
163
  crapThreshold: parseNumber("CLAUDE_CRAP_CRAP_THRESHOLD", process.env.CLAUDE_CRAP_CRAP_THRESHOLD, 30),
72
164
  cyclomaticMax: parseNumber("CLAUDE_CRAP_CYCLOMATIC_MAX", process.env.CLAUDE_CRAP_CYCLOMATIC_MAX, 15),
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAiCH;;;;;;;;;;;GAWG;AACH,SAAS,WAAW,CAAC,IAAY,EAAE,GAAuB,EAAE,QAAgB;IAC1E,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IACrD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,KAAK,GAAG,0BAA0B,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,WAAW,CAAC,GAAuB,EAAE,QAA+B;IAC3E,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,iCAAiC,GAAG,gCAAgC,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,UAAmC,CAAC;AAC7C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,OAAO,CAAC,GAAG,EAAE;QAChE,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,sBAAsB;QAClF,aAAa,EAAE,WAAW,CAAC,4BAA4B,EAAE,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,EAAE,CAAC;QACpG,aAAa,EAAE,WAAW,CAAC,4BAA4B,EAAE,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,EAAE,CAAC;QACpG,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,GAAG,CAAC;QACtE,aAAa,EAAE,WAAW,CAAC,6BAA6B,EAAE,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,EAAE,CAAC;QACtG,aAAa,EAAE,WAAW,CAAC,4BAA4B,EAAE,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,IAAI,CAAC;KACvG,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAiCvC;;;;;GAKG;AACH,SAAS,oBAAoB,CAAC,KAAyB;IACrD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,gCAAgC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AAC7D,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,KAAyB;IAChD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IAC1D,IAAI,oBAAoB,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAClD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,oBAAoB;IAC3B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAC1B,IAAI,CAAC,IAAI,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAE1C,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,qDAAqD;YACrD,OAAO,YAAY,CAAC,SAAS,IAAI,MAAM,CAAC,CAAC;QAC3C,CAAC;QAED,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAClC,mEAAmE;YACnE,+DAA+D;YAC/D,oBAAoB;YACpB,MAAM,MAAM,GAAG,YAAY,CACzB,MAAM,EACN,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,EAClD;gBACE,QAAQ,EAAE,MAAM;gBAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;gBACnC,OAAO,EAAE,IAAI;aACd,CACF,CAAC;YACF,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACvC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;QAED,yDAAyD;QACzD,OAAO,SAAS,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAWD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,qBAAqB,CAAC,UAAoC,EAAE;IAC1E,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,oBAAoB,CAAC;IAEpE,MAAM,cAAc,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACvE,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAE1C,MAAM,cAAc,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IAC5E,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAE1C,MAAM,UAAU,GAAG,eAAe,CAAC,aAAa,EAAE,CAAC,CAAC;IACpD,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAElC,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC;AACvB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,WAAW,CAAC,IAAY,EAAE,GAAuB,EAAE,QAAgB;IAC1E,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IACrD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,KAAK,GAAG,0BAA0B,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,WAAW,CAAC,GAAuB,EAAE,QAA+B;IAC3E,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,iCAAiC,GAAG,gCAAgC,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,UAAmC,CAAC;AAC7C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,UAAU,EAAE,qBAAqB,EAAE;QACnC,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,sBAAsB;QAClF,aAAa,EAAE,WAAW,CAAC,4BAA4B,EAAE,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,EAAE,CAAC;QACpG,aAAa,EAAE,WAAW,CAAC,4BAA4B,EAAE,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,EAAE,CAAC;QACpG,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,GAAG,CAAC;QACtE,aAAa,EAAE,WAAW,CAAC,6BAA6B,EAAE,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,EAAE,CAAC;QACtG,aAAa,EAAE,WAAW,CAAC,4BAA4B,EAAE,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,IAAI,CAAC;KACvG,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crap",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Deterministic QA plugin for Claude Code — CRAP index, Technical Debt Ratio, tree-sitter AST, SARIF 2.1.0, hooks, and a local Vue dashboard.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://code.claude.com/schemas/plugin.json",
3
3
  "name": "claude-crap",
4
- "version": "0.4.2",
4
+ "version": "0.4.4",
5
5
  "description": "Deterministic Quality Assurance plugin for Claude Code. Wraps every Write / Edit / Bash tool call with a PreToolUse gatekeeper, a PostToolUse verifier, and a Stop quality gate backed by CRAP index, Technical Debt Ratio, tree-sitter AST metrics, and SARIF 2.1.0 reports. Forbids the agent from writing functional code before a test safety net exists.",
6
6
  "author": {
7
7
  "name": "Alan Hernandez",
@@ -7358,6 +7358,52 @@ function countLines(source) {
7358
7358
  }
7359
7359
 
7360
7360
  // src/config.ts
7361
+ import { execFileSync } from "node:child_process";
7362
+ import { readlinkSync } from "node:fs";
7363
+ function isLiteralVarTemplate(value) {
7364
+ if (value === void 0) return false;
7365
+ return /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value.trim());
7366
+ }
7367
+ function sanitizeEnvPath(value) {
7368
+ if (value === void 0 || value === "") return void 0;
7369
+ if (isLiteralVarTemplate(value)) return void 0;
7370
+ return value;
7371
+ }
7372
+ function readParentCwdDefault() {
7373
+ try {
7374
+ const ppid = process.ppid;
7375
+ if (!ppid || ppid === 0) return void 0;
7376
+ if (process.platform === "linux") {
7377
+ return readlinkSync(`/proc/${ppid}/cwd`);
7378
+ }
7379
+ if (process.platform === "darwin") {
7380
+ const output = execFileSync(
7381
+ "lsof",
7382
+ ["-a", "-p", String(ppid), "-d", "cwd", "-F", "n"],
7383
+ {
7384
+ encoding: "utf8",
7385
+ stdio: ["ignore", "pipe", "ignore"],
7386
+ timeout: 2e3
7387
+ }
7388
+ );
7389
+ const match = output.match(/^n(.+)$/m);
7390
+ return match?.[1];
7391
+ }
7392
+ return void 0;
7393
+ } catch {
7394
+ return void 0;
7395
+ }
7396
+ }
7397
+ function discoverWorkspaceRoot(options = {}) {
7398
+ const readParentCwd = options.readParentCwd ?? readParentCwdDefault;
7399
+ const fromProjectDir = sanitizeEnvPath(process.env.CLAUDE_PROJECT_DIR);
7400
+ if (fromProjectDir) return fromProjectDir;
7401
+ const fromPluginRoot = sanitizeEnvPath(process.env.CLAUDE_CRAP_PLUGIN_ROOT);
7402
+ if (fromPluginRoot) return fromPluginRoot;
7403
+ const fromParent = sanitizeEnvPath(readParentCwd());
7404
+ if (fromParent) return fromParent;
7405
+ return process.cwd();
7406
+ }
7361
7407
  function parseNumber(name, raw, fallback) {
7362
7408
  if (raw === void 0 || raw === "") return fallback;
7363
7409
  const value = Number(raw);
@@ -7376,7 +7422,7 @@ function parseRating(raw, fallback) {
7376
7422
  }
7377
7423
  function loadConfig() {
7378
7424
  return {
7379
- pluginRoot: process.env.CLAUDE_CRAP_PLUGIN_ROOT ?? process.cwd(),
7425
+ pluginRoot: discoverWorkspaceRoot(),
7380
7426
  sarifOutputDir: process.env.CLAUDE_CRAP_SARIF_OUTPUT_DIR ?? ".claude-crap/reports",
7381
7427
  crapThreshold: parseNumber("CLAUDE_CRAP_CRAP_THRESHOLD", process.env.CLAUDE_CRAP_CRAP_THRESHOLD, 30),
7382
7428
  cyclomaticMax: parseNumber("CLAUDE_CRAP_CYCLOMATIC_MAX", process.env.CLAUDE_CRAP_CYCLOMATIC_MAX, 15),