claude-crap 0.1.2
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 +308 -0
- package/LICENSE +21 -0
- package/README.md +550 -0
- package/bin/claude-crap.mjs +141 -0
- package/dist/adapters/bandit.d.ts +48 -0
- package/dist/adapters/bandit.d.ts.map +1 -0
- package/dist/adapters/bandit.js +145 -0
- package/dist/adapters/bandit.js.map +1 -0
- package/dist/adapters/common.d.ts +73 -0
- package/dist/adapters/common.d.ts.map +1 -0
- package/dist/adapters/common.js +78 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/eslint.d.ts +52 -0
- package/dist/adapters/eslint.d.ts.map +1 -0
- package/dist/adapters/eslint.js +142 -0
- package/dist/adapters/eslint.js.map +1 -0
- package/dist/adapters/index.d.ts +47 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +64 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/semgrep.d.ts +30 -0
- package/dist/adapters/semgrep.d.ts.map +1 -0
- package/dist/adapters/semgrep.js +130 -0
- package/dist/adapters/semgrep.js.map +1 -0
- package/dist/adapters/stryker.d.ts +55 -0
- package/dist/adapters/stryker.d.ts.map +1 -0
- package/dist/adapters/stryker.js +165 -0
- package/dist/adapters/stryker.js.map +1 -0
- package/dist/ast/cyclomatic.d.ts +48 -0
- package/dist/ast/cyclomatic.d.ts.map +1 -0
- package/dist/ast/cyclomatic.js +106 -0
- package/dist/ast/cyclomatic.js.map +1 -0
- package/dist/ast/index.d.ts +26 -0
- package/dist/ast/index.d.ts.map +1 -0
- package/dist/ast/index.js +23 -0
- package/dist/ast/index.js.map +1 -0
- package/dist/ast/language-config.d.ts +70 -0
- package/dist/ast/language-config.d.ts.map +1 -0
- package/dist/ast/language-config.js +192 -0
- package/dist/ast/language-config.js.map +1 -0
- package/dist/ast/tree-sitter-engine.d.ts +133 -0
- package/dist/ast/tree-sitter-engine.d.ts.map +1 -0
- package/dist/ast/tree-sitter-engine.js +270 -0
- package/dist/ast/tree-sitter-engine.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +78 -0
- package/dist/config.js.map +1 -0
- package/dist/crap-config.d.ts +97 -0
- package/dist/crap-config.d.ts.map +1 -0
- package/dist/crap-config.js +144 -0
- package/dist/crap-config.js.map +1 -0
- package/dist/dashboard/server.d.ts +65 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +147 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +574 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/crap.d.ts +71 -0
- package/dist/metrics/crap.d.ts.map +1 -0
- package/dist/metrics/crap.js +67 -0
- package/dist/metrics/crap.js.map +1 -0
- package/dist/metrics/index.d.ts +31 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +27 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/score.d.ts +143 -0
- package/dist/metrics/score.d.ts.map +1 -0
- package/dist/metrics/score.js +224 -0
- package/dist/metrics/score.js.map +1 -0
- package/dist/metrics/tdr.d.ts +106 -0
- package/dist/metrics/tdr.d.ts.map +1 -0
- package/dist/metrics/tdr.js +117 -0
- package/dist/metrics/tdr.js.map +1 -0
- package/dist/metrics/workspace-walker.d.ts +43 -0
- package/dist/metrics/workspace-walker.d.ts.map +1 -0
- package/dist/metrics/workspace-walker.js +137 -0
- package/dist/metrics/workspace-walker.js.map +1 -0
- package/dist/sarif/index.d.ts +21 -0
- package/dist/sarif/index.d.ts.map +1 -0
- package/dist/sarif/index.js +19 -0
- package/dist/sarif/index.js.map +1 -0
- package/dist/sarif/sarif-builder.d.ts +128 -0
- package/dist/sarif/sarif-builder.d.ts.map +1 -0
- package/dist/sarif/sarif-builder.js +79 -0
- package/dist/sarif/sarif-builder.js.map +1 -0
- package/dist/sarif/sarif-store.d.ts +205 -0
- package/dist/sarif/sarif-store.d.ts.map +1 -0
- package/dist/sarif/sarif-store.js +246 -0
- package/dist/sarif/sarif-store.js.map +1 -0
- package/dist/sarif/sarif-validator.d.ts +45 -0
- package/dist/sarif/sarif-validator.d.ts.map +1 -0
- package/dist/sarif/sarif-validator.js +138 -0
- package/dist/sarif/sarif-validator.js.map +1 -0
- package/dist/schemas/tool-schemas.d.ts +216 -0
- package/dist/schemas/tool-schemas.d.ts.map +1 -0
- package/dist/schemas/tool-schemas.js +208 -0
- package/dist/schemas/tool-schemas.js.map +1 -0
- package/dist/sdk.d.ts +45 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +44 -0
- package/dist/sdk.js.map +1 -0
- package/dist/tools/index.d.ts +24 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/test-harness.d.ts +75 -0
- package/dist/tools/test-harness.d.ts.map +1 -0
- package/dist/tools/test-harness.js +137 -0
- package/dist/tools/test-harness.js.map +1 -0
- package/dist/workspace-guard.d.ts +53 -0
- package/dist/workspace-guard.d.ts.map +1 -0
- package/dist/workspace-guard.js +61 -0
- package/dist/workspace-guard.js.map +1 -0
- package/package.json +133 -0
- package/plugin/.claude-plugin/plugin.json +29 -0
- package/plugin/.mcp.json +18 -0
- package/plugin/CLAUDE.md +143 -0
- package/plugin/bundle/dashboard/public/index.html +368 -0
- package/plugin/bundle/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/plugin/bundle/mcp-server.mjs +8718 -0
- package/plugin/bundle/mcp-server.mjs.map +7 -0
- package/plugin/bundle/tdr-engine.mjs +50 -0
- package/plugin/bundle/tdr-engine.mjs.map +7 -0
- package/plugin/hooks/hooks.json +62 -0
- package/plugin/hooks/lib/crap-config.mjs +152 -0
- package/plugin/hooks/lib/gatekeeper-rules.mjs +257 -0
- package/plugin/hooks/lib/hook-io.mjs +151 -0
- package/plugin/hooks/lib/quality-gate.mjs +329 -0
- package/plugin/hooks/lib/test-harness.mjs +152 -0
- package/plugin/hooks/post-tool-use.mjs +245 -0
- package/plugin/hooks/pre-tool-use.mjs +290 -0
- package/plugin/hooks/session-start.mjs +109 -0
- package/plugin/hooks/stop-quality-gate.mjs +226 -0
- package/plugin/package.json +18 -0
- package/plugin/skills/adopt/SKILL.md +74 -0
- package/plugin/skills/analyze/SKILL.md +77 -0
- package/plugin/skills/check-test/SKILL.md +50 -0
- package/plugin/skills/score/SKILL.md +31 -0
- package/scripts/bug-report.mjs +328 -0
- package/scripts/build-fast.mjs +130 -0
- package/scripts/bundle-plugin.mjs +74 -0
- package/scripts/doctor.mjs +320 -0
- package/scripts/install.mjs +192 -0
- package/scripts/lib/cli-ui.mjs +122 -0
- package/scripts/postinstall.mjs +127 -0
- package/scripts/run-tests.mjs +95 -0
- package/scripts/status.mjs +110 -0
- package/scripts/uninstall.mjs +72 -0
- package/src/adapters/bandit.ts +191 -0
- package/src/adapters/common.ts +133 -0
- package/src/adapters/eslint.ts +187 -0
- package/src/adapters/index.ts +78 -0
- package/src/adapters/semgrep.ts +150 -0
- package/src/adapters/stryker.ts +218 -0
- package/src/ast/cyclomatic.ts +131 -0
- package/src/ast/index.ts +33 -0
- package/src/ast/language-config.ts +231 -0
- package/src/ast/tree-sitter-engine.ts +385 -0
- package/src/config.ts +109 -0
- package/src/crap-config.ts +196 -0
- package/src/dashboard/public/index.html +368 -0
- package/src/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/src/dashboard/server.ts +205 -0
- package/src/index.ts +696 -0
- package/src/metrics/crap.ts +101 -0
- package/src/metrics/index.ts +51 -0
- package/src/metrics/score.ts +329 -0
- package/src/metrics/tdr.ts +155 -0
- package/src/metrics/workspace-walker.ts +146 -0
- package/src/sarif/index.ts +31 -0
- package/src/sarif/sarif-builder.ts +139 -0
- package/src/sarif/sarif-store.ts +347 -0
- package/src/sarif/sarif-validator.ts +145 -0
- package/src/schemas/tool-schemas.ts +225 -0
- package/src/sdk.ts +110 -0
- package/src/tests/adapters/bandit.test.ts +111 -0
- package/src/tests/adapters/dispatch.test.ts +100 -0
- package/src/tests/adapters/eslint.test.ts +138 -0
- package/src/tests/adapters/semgrep.test.ts +125 -0
- package/src/tests/adapters/stryker.test.ts +103 -0
- package/src/tests/crap-config.test.ts +228 -0
- package/src/tests/crap.test.ts +59 -0
- package/src/tests/cyclomatic.test.ts +87 -0
- package/src/tests/dashboard-http.test.ts +108 -0
- package/src/tests/dashboard-integrity.test.ts +128 -0
- package/src/tests/integration/mcp-server.integration.test.ts +352 -0
- package/src/tests/pre-tool-use-hook.test.ts +178 -0
- package/src/tests/sarif-store.test.ts +241 -0
- package/src/tests/sarif-validator.test.ts +164 -0
- package/src/tests/score.test.ts +260 -0
- package/src/tests/skills-frontmatter.test.ts +172 -0
- package/src/tests/stop-quality-gate-strictness.test.ts +243 -0
- package/src/tests/tdr.test.ts +86 -0
- package/src/tests/test-harness.test.ts +153 -0
- package/src/tests/workspace-guard.test.ts +111 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/test-harness.ts +158 -0
- package/src/workspace-guard.ts +64 -0
- package/tsconfig.json +27 -0
package/dist/sdk.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root public SDK for `claude-crap`.
|
|
3
|
+
*
|
|
4
|
+
* This is the module you get when you do
|
|
5
|
+
* `import ... from "claude-crap"`. It is intentionally
|
|
6
|
+
* **side-effect-free**: importing this file does NOT start the MCP
|
|
7
|
+
* server, does NOT open the dashboard, does NOT touch the filesystem.
|
|
8
|
+
* Only the executable entrypoint in `dist/index.js` boots the
|
|
9
|
+
* server — that file is invoked by the `.mcp.json` command and the
|
|
10
|
+
* CLI bin, never as a library.
|
|
11
|
+
*
|
|
12
|
+
* Structure:
|
|
13
|
+
*
|
|
14
|
+
* - `./metrics` — CRAP, TDR, project score, workspace walker
|
|
15
|
+
* - `./sarif` — SARIF 2.1.0 builder and on-disk store
|
|
16
|
+
* - `./ast` — tree-sitter engine, cyclomatic complexity, language config
|
|
17
|
+
* - `./tools` — test-harness resolver used by `require_test_harness`
|
|
18
|
+
*
|
|
19
|
+
* Prefer deep imports
|
|
20
|
+
* (`import { computeCrap } from "claude-crap/metrics"`) over
|
|
21
|
+
* pulling everything through the root — they give TypeScript more
|
|
22
|
+
* precise type information and help tree-shakers drop unused modules.
|
|
23
|
+
*
|
|
24
|
+
* The symbols re-exported here are the ones most code paths need:
|
|
25
|
+
*
|
|
26
|
+
* - `computeCrap`, `computeTdr`, `computeProjectScore`
|
|
27
|
+
* - `renderProjectScoreMarkdown`
|
|
28
|
+
* - `classifyTdr`, `ratingIsWorseThan`
|
|
29
|
+
* - `SarifStore`, `buildSarifDocument`
|
|
30
|
+
* - `TreeSitterEngine`
|
|
31
|
+
*
|
|
32
|
+
* @module claude-crap
|
|
33
|
+
*/
|
|
34
|
+
// --- metrics ---------------------------------------------------------------
|
|
35
|
+
export { computeCrap, computeTdr, classifyTdr, ratingIsWorseThan, ratingToRank, computeProjectScore, renderProjectScoreMarkdown, estimateWorkspaceLoc, } from "./metrics/index.js";
|
|
36
|
+
// --- sarif -----------------------------------------------------------------
|
|
37
|
+
export { SarifStore, buildSarifDocument } from "./sarif/index.js";
|
|
38
|
+
// --- ast -------------------------------------------------------------------
|
|
39
|
+
export { TreeSitterEngine, computeCyclomaticComplexity, detectLanguageFromPath, LANGUAGE_TABLE, } from "./ast/index.js";
|
|
40
|
+
// --- tools -----------------------------------------------------------------
|
|
41
|
+
export { findTestFile, isTestFile, candidatePaths } from "./tools/index.js";
|
|
42
|
+
// --- adapters --------------------------------------------------------------
|
|
43
|
+
export { adaptScannerOutput, adaptSemgrep, adaptEslint, adaptBandit, adaptStryker, KNOWN_SCANNERS, } from "./adapters/index.js";
|
|
44
|
+
//# sourceMappingURL=sdk.js.map
|
package/dist/sdk.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk.js","sourceRoot":"","sources":["../src/sdk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,8EAA8E;AAC9E,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAiB5B,8EAA8E;AAC9E,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAWlE,8EAA8E;AAC9E,OAAO,EACL,gBAAgB,EAChB,2BAA2B,EAC3B,sBAAsB,EACtB,cAAc,GACf,MAAM,gBAAgB,CAAC;AAWxB,8EAA8E;AAC9E,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAG5E,8EAA8E;AAC9E,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACX,WAAW,EACX,YAAY,EACZ,cAAc,GACf,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public SDK entry point for the tool backends that sit behind
|
|
3
|
+
* the `claude-crap` MCP server.
|
|
4
|
+
*
|
|
5
|
+
* These are the same pure functions the MCP server calls into — the
|
|
6
|
+
* server layer just wraps them in JSON-RPC. Downstream consumers can
|
|
7
|
+
* reuse them directly from any Node.js context without running the
|
|
8
|
+
* MCP server.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import {
|
|
14
|
+
* findTestFile,
|
|
15
|
+
* isTestFile,
|
|
16
|
+
* candidatePaths,
|
|
17
|
+
* } from "claude-crap/tools";
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @module tools
|
|
21
|
+
*/
|
|
22
|
+
export { candidatePaths, findTestFile, isTestFile } from "./test-harness.js";
|
|
23
|
+
export type { TestFileResolution } from "./test-harness.js";
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC7E,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public SDK entry point for the tool backends that sit behind
|
|
3
|
+
* the `claude-crap` MCP server.
|
|
4
|
+
*
|
|
5
|
+
* These are the same pure functions the MCP server calls into — the
|
|
6
|
+
* server layer just wraps them in JSON-RPC. Downstream consumers can
|
|
7
|
+
* reuse them directly from any Node.js context without running the
|
|
8
|
+
* MCP server.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import {
|
|
14
|
+
* findTestFile,
|
|
15
|
+
* isTestFile,
|
|
16
|
+
* candidatePaths,
|
|
17
|
+
* } from "claude-crap/tools";
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @module tools
|
|
21
|
+
*/
|
|
22
|
+
export { candidatePaths, findTestFile, isTestFile } from "./test-harness.js";
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic test-file resolver used by the `require_test_harness`
|
|
3
|
+
* MCP tool.
|
|
4
|
+
*
|
|
5
|
+
* Given a production source file (for example `src/foo/bar.ts`), this
|
|
6
|
+
* module enumerates the conventional locations where a matching test
|
|
7
|
+
* file would live and returns the first existing match — or `null` when
|
|
8
|
+
* none of the candidates exist.
|
|
9
|
+
*
|
|
10
|
+
* This is a TypeScript twin of `hooks/lib/test-harness.mjs`. The two
|
|
11
|
+
* are intentionally independent so neither side has to import files
|
|
12
|
+
* from outside its own project tree:
|
|
13
|
+
*
|
|
14
|
+
* - Hooks use the `.mjs` copy (vanilla JS, zero deps, runs everywhere).
|
|
15
|
+
* - The MCP server uses this typed copy so its consumers get full
|
|
16
|
+
* type safety and so the server stays a self-contained npm package.
|
|
17
|
+
*
|
|
18
|
+
* Both copies implement the same conventions and are validated against
|
|
19
|
+
* the same unit tests — see `src/tests/test-harness.test.ts`.
|
|
20
|
+
*
|
|
21
|
+
* @module tools/test-harness
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Result of probing the filesystem for a matching test file.
|
|
25
|
+
*/
|
|
26
|
+
export interface TestFileResolution {
|
|
27
|
+
/** Absolute path of the first matching test file, or `null` when none exists. */
|
|
28
|
+
readonly testFile: string | null;
|
|
29
|
+
/** Absolute paths of every location the resolver tried. */
|
|
30
|
+
readonly candidates: ReadonlyArray<string>;
|
|
31
|
+
/** `true` when the input path itself is a test file. */
|
|
32
|
+
readonly isTestFile: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Return `true` when the given path is already a test file. Matching is
|
|
36
|
+
* done against the basename (`foo.test.ts`, `test_foo.py`) and against
|
|
37
|
+
* common test directory names in the path (`__tests__`, `tests`, `test`).
|
|
38
|
+
*
|
|
39
|
+
* @param filePath An absolute or relative source path.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isTestFile(filePath: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Enumerate every plausible test file path for a given production source
|
|
44
|
+
* file. Does not touch the filesystem — the caller is expected to probe
|
|
45
|
+
* existence separately (see {@link findTestFile}).
|
|
46
|
+
*
|
|
47
|
+
* Supported conventions, in the order they are probed:
|
|
48
|
+
*
|
|
49
|
+
* 1. Sibling `<base>.test.<ext>` / `<base>.spec.<ext>`
|
|
50
|
+
* 2. Sibling `__tests__/<base>.test.<ext>`
|
|
51
|
+
* 3. Mirror tree under `tests/`, `test/`, or `__tests__/` at the
|
|
52
|
+
* workspace root (e.g. `tests/src/foo/bar.test.ts`)
|
|
53
|
+
* 4. **Nearest-ancestor flat test directory**: walk up from the source
|
|
54
|
+
* file's directory toward the workspace root, and at every ancestor
|
|
55
|
+
* check for `tests/<base>.test.<ext>`. Matches layouts where tests
|
|
56
|
+
* live in a single flat directory near the source (this project
|
|
57
|
+
* uses it for `src/mcp-server/src/tests/`).
|
|
58
|
+
* 5. Python-specific: sibling `test_<base>.py` and mirror-tree
|
|
59
|
+
* `tests/.../test_<base>.py`.
|
|
60
|
+
*
|
|
61
|
+
* @param workspaceRoot Absolute workspace root.
|
|
62
|
+
* @param filePath Absolute path to the production file.
|
|
63
|
+
* @returns Ordered list of absolute candidate paths.
|
|
64
|
+
*/
|
|
65
|
+
export declare function candidatePaths(workspaceRoot: string, filePath: string): ReadonlyArray<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Probe the filesystem and return the first candidate that exists, or
|
|
68
|
+
* `null` when none of them do. Returns early with `isTestFile: true`
|
|
69
|
+
* when the input is already a test file.
|
|
70
|
+
*
|
|
71
|
+
* @param workspaceRoot Absolute workspace root.
|
|
72
|
+
* @param filePath Absolute or relative path to the production file.
|
|
73
|
+
*/
|
|
74
|
+
export declare function findTestFile(workspaceRoot: string, filePath: string): Promise<TestFileResolution>;
|
|
75
|
+
//# sourceMappingURL=test-harness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-harness.d.ts","sourceRoot":"","sources":["../../src/tools/test-harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC3C,wDAAwD;IACxD,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAKD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMpD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAkD7F;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,CAAC,CAe7B"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic test-file resolver used by the `require_test_harness`
|
|
3
|
+
* MCP tool.
|
|
4
|
+
*
|
|
5
|
+
* Given a production source file (for example `src/foo/bar.ts`), this
|
|
6
|
+
* module enumerates the conventional locations where a matching test
|
|
7
|
+
* file would live and returns the first existing match — or `null` when
|
|
8
|
+
* none of the candidates exist.
|
|
9
|
+
*
|
|
10
|
+
* This is a TypeScript twin of `hooks/lib/test-harness.mjs`. The two
|
|
11
|
+
* are intentionally independent so neither side has to import files
|
|
12
|
+
* from outside its own project tree:
|
|
13
|
+
*
|
|
14
|
+
* - Hooks use the `.mjs` copy (vanilla JS, zero deps, runs everywhere).
|
|
15
|
+
* - The MCP server uses this typed copy so its consumers get full
|
|
16
|
+
* type safety and so the server stays a self-contained npm package.
|
|
17
|
+
*
|
|
18
|
+
* Both copies implement the same conventions and are validated against
|
|
19
|
+
* the same unit tests — see `src/tests/test-harness.test.ts`.
|
|
20
|
+
*
|
|
21
|
+
* @module tools/test-harness
|
|
22
|
+
*/
|
|
23
|
+
import { promises as fs } from "node:fs";
|
|
24
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
25
|
+
/** Matches `.test.` and `.spec.` suffixes inside a file basename. */
|
|
26
|
+
const TEST_SUFFIX_PATTERN = /\.(test|spec)\./;
|
|
27
|
+
/**
|
|
28
|
+
* Return `true` when the given path is already a test file. Matching is
|
|
29
|
+
* done against the basename (`foo.test.ts`, `test_foo.py`) and against
|
|
30
|
+
* common test directory names in the path (`__tests__`, `tests`, `test`).
|
|
31
|
+
*
|
|
32
|
+
* @param filePath An absolute or relative source path.
|
|
33
|
+
*/
|
|
34
|
+
export function isTestFile(filePath) {
|
|
35
|
+
const base = basename(filePath);
|
|
36
|
+
if (TEST_SUFFIX_PATTERN.test(base))
|
|
37
|
+
return true;
|
|
38
|
+
if (base.startsWith("test_") && base.endsWith(".py"))
|
|
39
|
+
return true;
|
|
40
|
+
const parts = filePath.split(sep);
|
|
41
|
+
return parts.includes("__tests__") || parts.includes("tests") || parts.includes("test");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Enumerate every plausible test file path for a given production source
|
|
45
|
+
* file. Does not touch the filesystem — the caller is expected to probe
|
|
46
|
+
* existence separately (see {@link findTestFile}).
|
|
47
|
+
*
|
|
48
|
+
* Supported conventions, in the order they are probed:
|
|
49
|
+
*
|
|
50
|
+
* 1. Sibling `<base>.test.<ext>` / `<base>.spec.<ext>`
|
|
51
|
+
* 2. Sibling `__tests__/<base>.test.<ext>`
|
|
52
|
+
* 3. Mirror tree under `tests/`, `test/`, or `__tests__/` at the
|
|
53
|
+
* workspace root (e.g. `tests/src/foo/bar.test.ts`)
|
|
54
|
+
* 4. **Nearest-ancestor flat test directory**: walk up from the source
|
|
55
|
+
* file's directory toward the workspace root, and at every ancestor
|
|
56
|
+
* check for `tests/<base>.test.<ext>`. Matches layouts where tests
|
|
57
|
+
* live in a single flat directory near the source (this project
|
|
58
|
+
* uses it for `src/mcp-server/src/tests/`).
|
|
59
|
+
* 5. Python-specific: sibling `test_<base>.py` and mirror-tree
|
|
60
|
+
* `tests/.../test_<base>.py`.
|
|
61
|
+
*
|
|
62
|
+
* @param workspaceRoot Absolute workspace root.
|
|
63
|
+
* @param filePath Absolute path to the production file.
|
|
64
|
+
* @returns Ordered list of absolute candidate paths.
|
|
65
|
+
*/
|
|
66
|
+
export function candidatePaths(workspaceRoot, filePath) {
|
|
67
|
+
const absSource = resolve(filePath);
|
|
68
|
+
const ext = extname(absSource);
|
|
69
|
+
const base = basename(absSource, ext);
|
|
70
|
+
const dir = dirname(absSource);
|
|
71
|
+
const absWorkspace = resolve(workspaceRoot);
|
|
72
|
+
const relFromRoot = relative(absWorkspace, absSource);
|
|
73
|
+
const relDir = dirname(relFromRoot);
|
|
74
|
+
const candidates = new Set();
|
|
75
|
+
// 1. Sibling <base>.test.<ext> / <base>.spec.<ext>
|
|
76
|
+
candidates.add(join(dir, `${base}.test${ext}`));
|
|
77
|
+
candidates.add(join(dir, `${base}.spec${ext}`));
|
|
78
|
+
// 2. Sibling __tests__/<base>.test.<ext>
|
|
79
|
+
candidates.add(join(dir, "__tests__", `${base}.test${ext}`));
|
|
80
|
+
candidates.add(join(dir, "__tests__", `${base}.spec${ext}`));
|
|
81
|
+
// 3. Mirror tree under tests/, test/, or __tests__ at the workspace root.
|
|
82
|
+
for (const testRoot of ["tests", "test", "__tests__"]) {
|
|
83
|
+
candidates.add(join(absWorkspace, testRoot, relDir, `${base}.test${ext}`));
|
|
84
|
+
candidates.add(join(absWorkspace, testRoot, relDir, `${base}.spec${ext}`));
|
|
85
|
+
candidates.add(join(absWorkspace, testRoot, relDir, `${base}${ext}`));
|
|
86
|
+
}
|
|
87
|
+
// 4. Nearest-ancestor flat test directory. Walk up from `dir` to
|
|
88
|
+
// `absWorkspace`, and at each ancestor probe for a flat
|
|
89
|
+
// `tests/<base>.test.<ext>` (or `test/`, `__tests__/`) layout.
|
|
90
|
+
let current = dir;
|
|
91
|
+
while (current.length >= absWorkspace.length) {
|
|
92
|
+
for (const testRoot of ["tests", "test", "__tests__"]) {
|
|
93
|
+
candidates.add(join(current, testRoot, `${base}.test${ext}`));
|
|
94
|
+
candidates.add(join(current, testRoot, `${base}.spec${ext}`));
|
|
95
|
+
candidates.add(join(current, testRoot, `${base}${ext}`));
|
|
96
|
+
}
|
|
97
|
+
if (current === absWorkspace)
|
|
98
|
+
break;
|
|
99
|
+
const parent = dirname(current);
|
|
100
|
+
if (parent === current)
|
|
101
|
+
break; // reached filesystem root, stop
|
|
102
|
+
current = parent;
|
|
103
|
+
}
|
|
104
|
+
// 5. Python-specific variants.
|
|
105
|
+
if (ext === ".py") {
|
|
106
|
+
candidates.add(join(dir, `test_${base}.py`));
|
|
107
|
+
candidates.add(join(absWorkspace, "tests", `test_${base}.py`));
|
|
108
|
+
candidates.add(join(absWorkspace, "tests", relDir, `test_${base}.py`));
|
|
109
|
+
}
|
|
110
|
+
return Array.from(candidates);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Probe the filesystem and return the first candidate that exists, or
|
|
114
|
+
* `null` when none of them do. Returns early with `isTestFile: true`
|
|
115
|
+
* when the input is already a test file.
|
|
116
|
+
*
|
|
117
|
+
* @param workspaceRoot Absolute workspace root.
|
|
118
|
+
* @param filePath Absolute or relative path to the production file.
|
|
119
|
+
*/
|
|
120
|
+
export async function findTestFile(workspaceRoot, filePath) {
|
|
121
|
+
const absolute = isAbsolute(filePath) ? filePath : resolve(workspaceRoot, filePath);
|
|
122
|
+
if (isTestFile(absolute)) {
|
|
123
|
+
return { testFile: absolute, candidates: [absolute], isTestFile: true };
|
|
124
|
+
}
|
|
125
|
+
const candidates = candidatePaths(workspaceRoot, absolute);
|
|
126
|
+
for (const candidate of candidates) {
|
|
127
|
+
try {
|
|
128
|
+
await fs.access(candidate);
|
|
129
|
+
return { testFile: candidate, candidates, isTestFile: false };
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Probe next candidate.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { testFile: null, candidates, isTestFile: false };
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=test-harness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-harness.js","sourceRoot":"","sources":["../../src/tools/test-harness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace path containment guard.
|
|
3
|
+
*
|
|
4
|
+
* Every MCP tool that accepts a user-supplied file path routes it
|
|
5
|
+
* through {@link resolveWithinWorkspace} before touching the
|
|
6
|
+
* filesystem. The guard rejects any resolved absolute path that is
|
|
7
|
+
* outside the configured workspace root, so the agent cannot be
|
|
8
|
+
* tricked (via prompt injection through scanner output, or via its
|
|
9
|
+
* own confusion) into reading files that live next to the project
|
|
10
|
+
* but outside it.
|
|
11
|
+
*
|
|
12
|
+
* F-A01-01: the original guard in `src/index.ts` used a naive
|
|
13
|
+
* `candidate.startsWith(workspace)` check, which suffers from prefix
|
|
14
|
+
* confusion — for a workspace like `/Users/x/claude-crap`, an
|
|
15
|
+
* absolute input path such as `/Users/x/claude-crap-evil/secret.ts`
|
|
16
|
+
* would pass the check because the two share the literal prefix up
|
|
17
|
+
* to the final segment. This module replaces that check with a
|
|
18
|
+
* separator-aware comparison: the candidate is only accepted if it
|
|
19
|
+
* equals the workspace exactly OR begins with `workspace + sep`.
|
|
20
|
+
*
|
|
21
|
+
* This module is intentionally pure (no I/O, no global state) so it
|
|
22
|
+
* can be unit-tested without any fixtures.
|
|
23
|
+
*
|
|
24
|
+
* @module workspace-guard
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a user-supplied file path against a workspace root, returning
|
|
28
|
+
* the absolute path only if it is contained inside the root. Throws a
|
|
29
|
+
* descriptive error when the resolved candidate escapes the workspace.
|
|
30
|
+
*
|
|
31
|
+
* Rules enforced (must stay in sync with
|
|
32
|
+
* `src/tests/workspace-guard.test.ts`):
|
|
33
|
+
*
|
|
34
|
+
* 1. Relative paths are resolved against `workspaceRoot`.
|
|
35
|
+
* 2. Absolute paths are accepted as-is for resolution.
|
|
36
|
+
* 3. The resolved candidate must equal `workspaceRoot` OR begin with
|
|
37
|
+
* `workspaceRoot + sep`. Sibling directories that merely share a
|
|
38
|
+
* prefix (e.g. `/tmp/workspace-evil` vs `/tmp/workspace`) are
|
|
39
|
+
* rejected.
|
|
40
|
+
* 4. The comparison uses the platform's native path separator, so
|
|
41
|
+
* the guard works on both POSIX and Windows.
|
|
42
|
+
*
|
|
43
|
+
* @param workspaceRoot Absolute or relative path to the workspace root.
|
|
44
|
+
* Non-absolute values are resolved against the
|
|
45
|
+
* current working directory, which matches the
|
|
46
|
+
* behavior of the previous in-lined guard.
|
|
47
|
+
* @param filePath User-supplied path. May be absolute or relative
|
|
48
|
+
* to the workspace root.
|
|
49
|
+
* @returns The absolute, workspace-contained path.
|
|
50
|
+
* @throws `Error` when the candidate escapes the workspace.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveWithinWorkspace(workspaceRoot: string, filePath: string): string;
|
|
53
|
+
//# sourceMappingURL=workspace-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-guard.d.ts","sourceRoot":"","sources":["../src/workspace-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAStF"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace path containment guard.
|
|
3
|
+
*
|
|
4
|
+
* Every MCP tool that accepts a user-supplied file path routes it
|
|
5
|
+
* through {@link resolveWithinWorkspace} before touching the
|
|
6
|
+
* filesystem. The guard rejects any resolved absolute path that is
|
|
7
|
+
* outside the configured workspace root, so the agent cannot be
|
|
8
|
+
* tricked (via prompt injection through scanner output, or via its
|
|
9
|
+
* own confusion) into reading files that live next to the project
|
|
10
|
+
* but outside it.
|
|
11
|
+
*
|
|
12
|
+
* F-A01-01: the original guard in `src/index.ts` used a naive
|
|
13
|
+
* `candidate.startsWith(workspace)` check, which suffers from prefix
|
|
14
|
+
* confusion — for a workspace like `/Users/x/claude-crap`, an
|
|
15
|
+
* absolute input path such as `/Users/x/claude-crap-evil/secret.ts`
|
|
16
|
+
* would pass the check because the two share the literal prefix up
|
|
17
|
+
* to the final segment. This module replaces that check with a
|
|
18
|
+
* separator-aware comparison: the candidate is only accepted if it
|
|
19
|
+
* equals the workspace exactly OR begins with `workspace + sep`.
|
|
20
|
+
*
|
|
21
|
+
* This module is intentionally pure (no I/O, no global state) so it
|
|
22
|
+
* can be unit-tested without any fixtures.
|
|
23
|
+
*
|
|
24
|
+
* @module workspace-guard
|
|
25
|
+
*/
|
|
26
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a user-supplied file path against a workspace root, returning
|
|
29
|
+
* the absolute path only if it is contained inside the root. Throws a
|
|
30
|
+
* descriptive error when the resolved candidate escapes the workspace.
|
|
31
|
+
*
|
|
32
|
+
* Rules enforced (must stay in sync with
|
|
33
|
+
* `src/tests/workspace-guard.test.ts`):
|
|
34
|
+
*
|
|
35
|
+
* 1. Relative paths are resolved against `workspaceRoot`.
|
|
36
|
+
* 2. Absolute paths are accepted as-is for resolution.
|
|
37
|
+
* 3. The resolved candidate must equal `workspaceRoot` OR begin with
|
|
38
|
+
* `workspaceRoot + sep`. Sibling directories that merely share a
|
|
39
|
+
* prefix (e.g. `/tmp/workspace-evil` vs `/tmp/workspace`) are
|
|
40
|
+
* rejected.
|
|
41
|
+
* 4. The comparison uses the platform's native path separator, so
|
|
42
|
+
* the guard works on both POSIX and Windows.
|
|
43
|
+
*
|
|
44
|
+
* @param workspaceRoot Absolute or relative path to the workspace root.
|
|
45
|
+
* Non-absolute values are resolved against the
|
|
46
|
+
* current working directory, which matches the
|
|
47
|
+
* behavior of the previous in-lined guard.
|
|
48
|
+
* @param filePath User-supplied path. May be absolute or relative
|
|
49
|
+
* to the workspace root.
|
|
50
|
+
* @returns The absolute, workspace-contained path.
|
|
51
|
+
* @throws `Error` when the candidate escapes the workspace.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveWithinWorkspace(workspaceRoot, filePath) {
|
|
54
|
+
const workspace = resolve(workspaceRoot);
|
|
55
|
+
const candidate = isAbsolute(filePath) ? resolve(filePath) : resolve(workspace, filePath);
|
|
56
|
+
if (candidate !== workspace && !candidate.startsWith(workspace + sep)) {
|
|
57
|
+
throw new Error(`[claude-crap] Refusing to access '${filePath}' — path escapes the workspace root`);
|
|
58
|
+
}
|
|
59
|
+
return candidate;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=workspace-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-guard.js","sourceRoot":"","sources":["../src/workspace-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,sBAAsB,CAAC,aAAqB,EAAE,QAAgB;IAC5E,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACzC,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC1F,IAAI,SAAS,KAAK,SAAS,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,SAAS,GAAG,GAAG,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CACb,qCAAqC,QAAQ,qCAAqC,CACnF,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-crap",
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"claude-code-plugin",
|
|
8
|
+
"quality-assurance",
|
|
9
|
+
"sast",
|
|
10
|
+
"sarif",
|
|
11
|
+
"crap-index",
|
|
12
|
+
"technical-debt",
|
|
13
|
+
"quality-gate",
|
|
14
|
+
"shift-left",
|
|
15
|
+
"deterministic",
|
|
16
|
+
"mutation-testing",
|
|
17
|
+
"mcp-server",
|
|
18
|
+
"mcp",
|
|
19
|
+
"tree-sitter"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://github.com/ahernandez-developer/claude-crap",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/ahernandez-developer/claude-crap/issues"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/ahernandez-developer/claude-crap.git"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"author": "Alan Hernandez",
|
|
31
|
+
"type": "module",
|
|
32
|
+
"main": "./dist/sdk.js",
|
|
33
|
+
"types": "./dist/sdk.d.ts",
|
|
34
|
+
"bin": {
|
|
35
|
+
"claude-crap": "./bin/claude-crap.mjs"
|
|
36
|
+
},
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/sdk.d.ts",
|
|
40
|
+
"import": "./dist/sdk.js"
|
|
41
|
+
},
|
|
42
|
+
"./metrics": {
|
|
43
|
+
"types": "./dist/metrics/index.d.ts",
|
|
44
|
+
"import": "./dist/metrics/index.js"
|
|
45
|
+
},
|
|
46
|
+
"./sarif": {
|
|
47
|
+
"types": "./dist/sarif/index.d.ts",
|
|
48
|
+
"import": "./dist/sarif/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./ast": {
|
|
51
|
+
"types": "./dist/ast/index.d.ts",
|
|
52
|
+
"import": "./dist/ast/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./tools": {
|
|
55
|
+
"types": "./dist/tools/index.d.ts",
|
|
56
|
+
"import": "./dist/tools/index.js"
|
|
57
|
+
},
|
|
58
|
+
"./adapters": {
|
|
59
|
+
"types": "./dist/adapters/index.d.ts",
|
|
60
|
+
"import": "./dist/adapters/index.js"
|
|
61
|
+
},
|
|
62
|
+
"./package.json": "./package.json"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=20.0.0",
|
|
66
|
+
"bun": ">=1.0.0"
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"CHANGELOG.md",
|
|
70
|
+
"CLAUDE.md",
|
|
71
|
+
"LICENSE",
|
|
72
|
+
"README.md",
|
|
73
|
+
"bin",
|
|
74
|
+
"dist",
|
|
75
|
+
"plugin",
|
|
76
|
+
"scripts",
|
|
77
|
+
"src",
|
|
78
|
+
"tsconfig.json"
|
|
79
|
+
],
|
|
80
|
+
"scripts": {
|
|
81
|
+
"build": "tsc -p tsconfig.json",
|
|
82
|
+
"build:fast": "node ./scripts/build-fast.mjs",
|
|
83
|
+
"build:watch": "tsc -p tsconfig.json --watch",
|
|
84
|
+
"start": "node ./plugin/bundle/mcp-server.mjs --transport stdio",
|
|
85
|
+
"dev": "tsx ./src/index.ts --transport stdio",
|
|
86
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
87
|
+
"test": "node ./scripts/run-tests.mjs \"./src/tests/**/*.test.ts\"",
|
|
88
|
+
"test:metrics": "node ./scripts/run-tests.mjs \"./src/tests/crap.test.ts\" \"./src/tests/tdr.test.ts\" \"./src/tests/score.test.ts\"",
|
|
89
|
+
"test:sarif": "node ./scripts/run-tests.mjs \"./src/tests/sarif-store.test.ts\" \"./src/tests/sarif-validator.test.ts\"",
|
|
90
|
+
"test:ast": "node ./scripts/run-tests.mjs \"./src/tests/cyclomatic.test.ts\"",
|
|
91
|
+
"test:harness": "node ./scripts/run-tests.mjs \"./src/tests/test-harness.test.ts\"",
|
|
92
|
+
"test:adapters": "node ./scripts/run-tests.mjs \"./src/tests/adapters/**/*.test.ts\"",
|
|
93
|
+
"test:integration": "node ./scripts/run-tests.mjs \"./src/tests/integration/**/*.test.ts\"",
|
|
94
|
+
"doctor": "node ./bin/claude-crap.mjs doctor",
|
|
95
|
+
"status": "node ./bin/claude-crap.mjs status",
|
|
96
|
+
"bug-report": "node ./scripts/bug-report.mjs",
|
|
97
|
+
"clean": "rm -rf ./dist",
|
|
98
|
+
"build:plugin": "node ./scripts/bundle-plugin.mjs",
|
|
99
|
+
"postinstall": "node ./scripts/postinstall.mjs",
|
|
100
|
+
"prepublishOnly": "npm run clean && npm run build && npm run build:plugin && npm test && npm audit --omit=dev --audit-level=high",
|
|
101
|
+
"release": "np",
|
|
102
|
+
"release:patch": "np patch --no-cleanup",
|
|
103
|
+
"release:minor": "np minor --no-cleanup",
|
|
104
|
+
"release:major": "np major --no-cleanup"
|
|
105
|
+
},
|
|
106
|
+
"np": {
|
|
107
|
+
"yarn": false,
|
|
108
|
+
"contents": ".",
|
|
109
|
+
"testScript": "test",
|
|
110
|
+
"2fa": false
|
|
111
|
+
},
|
|
112
|
+
"publishConfig": {
|
|
113
|
+
"access": "public"
|
|
114
|
+
},
|
|
115
|
+
"dependencies": {
|
|
116
|
+
"@fastify/static": "^8.0.3",
|
|
117
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
118
|
+
"ajv": "^8.17.1",
|
|
119
|
+
"ajv-formats": "^3.0.1",
|
|
120
|
+
"fast-glob": "^3.3.2",
|
|
121
|
+
"fastify": "^5.2.0",
|
|
122
|
+
"pino": "^9.5.0",
|
|
123
|
+
"tree-sitter-wasms": "^0.1.12",
|
|
124
|
+
"web-tree-sitter": "^0.24.4"
|
|
125
|
+
},
|
|
126
|
+
"devDependencies": {
|
|
127
|
+
"@types/node": "^22.10.2",
|
|
128
|
+
"esbuild": "^0.28.0",
|
|
129
|
+
"np": "^11.0.2",
|
|
130
|
+
"tsx": "^4.19.2",
|
|
131
|
+
"typescript": "^5.7.2"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://code.claude.com/schemas/plugin.json",
|
|
3
|
+
"name": "claude-crap",
|
|
4
|
+
"version": "0.1.2",
|
|
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
|
+
"author": {
|
|
7
|
+
"name": "Alan Hernandez",
|
|
8
|
+
"url": "https://github.com/ahernandez-developer"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/ahernandez-developer/claude-crap",
|
|
11
|
+
"repository": "https://github.com/ahernandez-developer/claude-crap",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude-code",
|
|
15
|
+
"claude-code-plugin",
|
|
16
|
+
"quality-assurance",
|
|
17
|
+
"sast",
|
|
18
|
+
"sarif",
|
|
19
|
+
"crap-index",
|
|
20
|
+
"technical-debt",
|
|
21
|
+
"quality-gate",
|
|
22
|
+
"shift-left",
|
|
23
|
+
"deterministic",
|
|
24
|
+
"mutation-testing",
|
|
25
|
+
"mcp-server",
|
|
26
|
+
"mcp",
|
|
27
|
+
"tree-sitter"
|
|
28
|
+
]
|
|
29
|
+
}
|
package/plugin/.mcp.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://code.claude.com/schemas/mcp.json",
|
|
3
|
+
"mcpServers": {
|
|
4
|
+
"claude-crap": {
|
|
5
|
+
"type": "stdio",
|
|
6
|
+
"command": "node",
|
|
7
|
+
"args": [
|
|
8
|
+
"${CLAUDE_PLUGIN_ROOT}/bundle/mcp-server.mjs",
|
|
9
|
+
"--transport",
|
|
10
|
+
"stdio"
|
|
11
|
+
],
|
|
12
|
+
"env": {
|
|
13
|
+
"NODE_ENV": "production",
|
|
14
|
+
"CLAUDE_CRAP_PLUGIN_ROOT": "${CLAUDE_PLUGIN_ROOT}"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|