@hasna/hooks 0.8.0 → 0.9.1
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/README.md +33 -5
- package/bin/hooks-mcp.js +4862 -0
- package/bin/index.js +592 -170
- package/bin/serve.js +33 -10
- package/dist/db/index.d.ts +8 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2870 -2709
- package/dist/lib/db-writer.d.ts +30 -1
- package/dist/lib/installer.d.ts +40 -1
- package/dist/lib/local-opt-in.d.ts +26 -0
- package/dist/lib/registration.d.ts +75 -0
- package/dist/lib/registry.d.ts +14 -0
- package/dist/lib/sync.d.ts +17 -1
- package/dist/sdk/index.d.ts +103 -0
- package/dist/sdk/index.js +1050 -0
- package/dist/storage.js +15 -1
- package/hooks/hook-agent-rules-version-check/README.md +1 -1
- package/hooks/hook-trash-guard/README.md +147 -0
- package/hooks/hook-trash-guard/package.json +12 -0
- package/hooks/hook-trash-guard/src/hook.ts +1142 -0
- package/package.json +10 -3
- package/scripts/validate-package.ts +31 -4
- package/hooks/codewith-native-common.test.ts +0 -1935
- package/hooks/hook-affected-tests/tsconfig.json +0 -25
- package/hooks/hook-agent-rules-version-check/src/hook.test.ts +0 -104
- package/hooks/hook-agent-rules-version-check/tsconfig.json +0 -25
- package/hooks/hook-announce-start/tsconfig.json +0 -25
- package/hooks/hook-announce-stop/tsconfig.json +0 -25
- package/hooks/hook-autoformat/tsconfig.json +0 -25
- package/hooks/hook-branchprotect/tsconfig.json +0 -25
- package/hooks/hook-checkbugs/tsconfig.json +0 -15
- package/hooks/hook-checkdocs/tsconfig.json +0 -15
- package/hooks/hook-checkfiles/tsconfig.json +0 -15
- package/hooks/hook-checklint/tsconfig.json +0 -15
- package/hooks/hook-checkpoint/tsconfig.json +0 -25
- package/hooks/hook-checksecurity/tsconfig.json +0 -15
- package/hooks/hook-checktasks/tsconfig.json +0 -20
- package/hooks/hook-checktests/tsconfig.json +0 -15
- package/hooks/hook-conflict-detect/tsconfig.json +0 -25
- package/hooks/hook-contextrefresh/tsconfig.json +0 -25
- package/hooks/hook-desktopnotify/tsconfig.json +0 -25
- package/hooks/hook-dm-inject/tsconfig.json +0 -25
- package/hooks/hook-envsetup/tsconfig.json +0 -25
- package/hooks/hook-failure-to-task/tsconfig.json +0 -25
- package/hooks/hook-filelock/tsconfig.json +0 -25
- package/hooks/hook-fleet-blockers-gate/src/hook.test.ts +0 -302
- package/hooks/hook-fleet-blockers-gate/tsconfig.json +0 -25
- package/hooks/hook-fleet-catchup/src/hook.test.ts +0 -156
- package/hooks/hook-fleet-catchup/tsconfig.json +0 -25
- package/hooks/hook-gitguard/tsconfig.json +0 -25
- package/hooks/hook-knowledge-context/src/hook.test.ts +0 -379
- package/hooks/hook-packageage/tsconfig.json +0 -25
- package/hooks/hook-permissionguard/tsconfig.json +0 -25
- package/hooks/hook-phonenotify/tsconfig.json +0 -25
- package/hooks/hook-precompact/tsconfig.json +0 -25
- package/hooks/hook-protectfiles/tsconfig.json +0 -25
- package/hooks/hook-scanoutput/src/hook.test.ts +0 -217
- package/hooks/hook-spiral-detector/src/hook.test.ts +0 -72
- package/hooks/hook-stylescheck/tsconfig.json +0 -25
- package/hooks/hook-typecheck-gate/tsconfig.json +0 -25
- package/hooks/hook-workspace-repos-guard/src/hook.test.ts +0 -466
- package/hooks/hook-workspace-repos-guard/tsconfig.json +0 -21
- package/hooks/mention-context/src/hook.test.ts +0 -68
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
analyzeToolOutput,
|
|
4
|
-
buildHookOutput,
|
|
5
|
-
extractToolOutputText,
|
|
6
|
-
formatWarning,
|
|
7
|
-
MAX_SCAN_BYTES,
|
|
8
|
-
SCAN_TIMEOUT_MS,
|
|
9
|
-
type ScanFn,
|
|
10
|
-
type ScanOutcome,
|
|
11
|
-
} from "./hook";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A stand-in for `scanInputExposures` from @hasna/secrets.
|
|
15
|
-
*
|
|
16
|
-
* The scanner itself is already exercised both ways in its own repo; what these
|
|
17
|
-
* tests own is the hook's behaviour around it — that a finding is surfaced, that
|
|
18
|
-
* a clean scan is silent, that an unreadable scan is NOT reported as clean, and
|
|
19
|
-
* that nothing the scanner does can stop the session.
|
|
20
|
-
*/
|
|
21
|
-
function fakeScanner(result: Partial<ReturnType<ScanFn>> = {}): ScanFn {
|
|
22
|
-
return () =>
|
|
23
|
-
({
|
|
24
|
-
schema: "open-secrets.exposure-scan.v1",
|
|
25
|
-
version: 1,
|
|
26
|
-
source: "input",
|
|
27
|
-
root: "<stdin>",
|
|
28
|
-
redacted: true,
|
|
29
|
-
limits: { findings: 50 },
|
|
30
|
-
stats: { filesScanned: 1, filesSkipped: 0, bytesScanned: 100, errors: [], skipped: [] },
|
|
31
|
-
truncated: false,
|
|
32
|
-
findings: [],
|
|
33
|
-
...result,
|
|
34
|
-
}) as ReturnType<ScanFn>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const finding = (over: Record<string, unknown> = {}) => ({
|
|
38
|
-
id: "f1",
|
|
39
|
-
source: "input",
|
|
40
|
-
detector: "credential_assignment",
|
|
41
|
-
severity: "medium",
|
|
42
|
-
path: "<stdin>",
|
|
43
|
-
line: 12,
|
|
44
|
-
column: 5,
|
|
45
|
-
preview: "HASNA_TODOS_API_KEY=***REDACTED***",
|
|
46
|
-
evidencePath: "<stdin>:12",
|
|
47
|
-
...over,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("hook-scanoutput / extractToolOutputText", () => {
|
|
51
|
-
/**
|
|
52
|
-
* The field name is the whole ballgame. Verified against the Claude Code 2.1.226
|
|
53
|
-
* binary on 2026-08-10: its embedded schema reads
|
|
54
|
-
* "tool_response": { "success": true } // PostToolUse only
|
|
55
|
-
* and its implementation builds
|
|
56
|
-
* hook_event_name:"PostToolUse", tool_name:e, tool_input:r, tool_response:n
|
|
57
|
-
* `tool_response` 21 occurrences, `tool_output` 2 — both of those the telemetry
|
|
58
|
-
* name tengu_dead_probe_hook_updated_mcp_tool_output, neither near PostToolUse.
|
|
59
|
-
*
|
|
60
|
-
* Reading only `tool_output` yields undefined on Claude Code, which this hook
|
|
61
|
-
* would score as an empty input and report clean. These tests lock the field.
|
|
62
|
-
*/
|
|
63
|
-
test("reads tool_response, the field Claude Code actually sends", () => {
|
|
64
|
-
expect(extractToolOutputText({ tool_response: "hello" })).toBe("hello");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("reads a structured tool_response", () => {
|
|
68
|
-
const text = extractToolOutputText({ tool_response: { stdout: "out-part", stderr: "err-part" } });
|
|
69
|
-
expect(text).toContain("out-part");
|
|
70
|
-
expect(text).toContain("err-part");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("still reads tool_output, the older catalog convention", () => {
|
|
74
|
-
expect(extractToolOutputText({ tool_output: "hello" })).toBe("hello");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("concatenates the usual structured stdout/stderr fields", () => {
|
|
78
|
-
const text = extractToolOutputText({ tool_output: { stdout: "out-part", stderr: "err-part" } });
|
|
79
|
-
expect(text).toContain("out-part");
|
|
80
|
-
expect(text).toContain("err-part");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("prefers tool_response when both are present", () => {
|
|
84
|
-
expect(extractToolOutputText({ tool_response: "from-response", tool_output: "from-output" })).toBe("from-response");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("falls back to serialising an unrecognised output shape rather than skipping it", () => {
|
|
88
|
-
// An output shape this hook does not know must not read as 'no output'.
|
|
89
|
-
const text = extractToolOutputText({ tool_response: { unexpectedField: "SOMETHING-IN-HERE" } });
|
|
90
|
-
expect(text).toContain("SOMETHING-IN-HERE");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("returns empty string when there is no output at all", () => {
|
|
94
|
-
expect(extractToolOutputText({})).toBe("");
|
|
95
|
-
expect(extractToolOutputText({ tool_output: undefined })).toBe("");
|
|
96
|
-
expect(extractToolOutputText({ tool_response: undefined })).toBe("");
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe("hook-scanoutput / analyzeToolOutput", () => {
|
|
101
|
-
test("FIRES: a finding in tool output is surfaced with its detector", () => {
|
|
102
|
-
const outcome = analyzeToolOutput("anything", fakeScanner({ findings: [finding()] as never }));
|
|
103
|
-
expect(outcome.status).toBe("found");
|
|
104
|
-
expect(outcome.findingCount).toBe(1);
|
|
105
|
-
expect(outcome.detectors).toEqual(["credential_assignment"]);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("SILENT: ordinary output produces no warning", () => {
|
|
109
|
-
const outcome = analyzeToolOutput("ordinary tool output", fakeScanner());
|
|
110
|
-
expect(outcome.status).toBe("clean");
|
|
111
|
-
expect(outcome.findingCount).toBe(0);
|
|
112
|
-
expect(formatWarning(outcome)).toBe("");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("a clean verdict requires bytes to have actually been read", () => {
|
|
116
|
-
// Zero bytes scanned is not a clean scan; it is a scan that did not happen.
|
|
117
|
-
const outcome = analyzeToolOutput(
|
|
118
|
-
"x",
|
|
119
|
-
fakeScanner({ stats: { filesScanned: 1, filesSkipped: 0, bytesScanned: 0, errors: [], skipped: [] } as never }),
|
|
120
|
-
);
|
|
121
|
-
expect(outcome.status).toBe("unscanned");
|
|
122
|
-
expect(formatWarning(outcome)).toContain("UNSCANNED");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("a truncated scan is reported as unscanned, never as clean", () => {
|
|
126
|
-
const outcome = analyzeToolOutput("x", fakeScanner({ truncated: true, truncatedReason: "max_bytes" } as never));
|
|
127
|
-
expect(outcome.status).toBe("unscanned");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("a skipped input is reported as unscanned, never as clean", () => {
|
|
131
|
-
const outcome = analyzeToolOutput(
|
|
132
|
-
"x",
|
|
133
|
-
fakeScanner({
|
|
134
|
-
stats: {
|
|
135
|
-
filesScanned: 0,
|
|
136
|
-
filesSkipped: 1,
|
|
137
|
-
bytesScanned: 0,
|
|
138
|
-
errors: [],
|
|
139
|
-
skipped: [{ path: "<stdin>", reason: "max_file_bytes", bytes: 9_000_000 }],
|
|
140
|
-
} as never,
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
143
|
-
expect(outcome.status).toBe("unscanned");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("empty tool output is skipped outright and is not called clean", () => {
|
|
147
|
-
const outcome = analyzeToolOutput("", fakeScanner());
|
|
148
|
-
expect(outcome.status).toBe("empty");
|
|
149
|
-
expect(formatWarning(outcome)).toBe("");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("bounds the scan explicitly, because it sits on a tool call's critical path", () => {
|
|
153
|
-
// The Claude installer target writes no `timeout` into settings.json, so the
|
|
154
|
-
// wiring supplies no outer bound and the scanner's own default is 10s.
|
|
155
|
-
let seen: { maxBytes?: number; timeoutMs?: number } | undefined;
|
|
156
|
-
const spy: ScanFn = (opts) => {
|
|
157
|
-
seen = opts;
|
|
158
|
-
return fakeScanner()(opts);
|
|
159
|
-
};
|
|
160
|
-
analyzeToolOutput("x", spy);
|
|
161
|
-
expect(seen?.maxBytes).toBe(MAX_SCAN_BYTES);
|
|
162
|
-
expect(seen?.timeoutMs).toBe(SCAN_TIMEOUT_MS);
|
|
163
|
-
expect(SCAN_TIMEOUT_MS).toBeLessThan(10_000);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("FAIL-OPEN: a scanner that throws degrades to a reported error, never a crash", () => {
|
|
167
|
-
const outcome = analyzeToolOutput("x", (() => {
|
|
168
|
-
throw new Error("scanner exploded");
|
|
169
|
-
}) as ScanFn);
|
|
170
|
-
expect(outcome.status).toBe("error");
|
|
171
|
-
expect(outcome.error).toContain("scanner exploded");
|
|
172
|
-
expect(formatWarning(outcome)).toContain("could not scan");
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("reports every severity, because high-only would miss the measured leak class", () => {
|
|
176
|
-
// Measured on 2026-08-10: 25 real fleet API keys in a bash -x trace were ALL
|
|
177
|
-
// detector=credential_assignment severity=medium. A high-only filter sees none of them.
|
|
178
|
-
const outcome = analyzeToolOutput(
|
|
179
|
-
"x",
|
|
180
|
-
fakeScanner({ findings: [finding(), finding({ id: "f2", severity: "high", detector: "aws_access_key_id" })] as never }),
|
|
181
|
-
);
|
|
182
|
-
expect(outcome.findingCount).toBe(2);
|
|
183
|
-
expect(outcome.detectors).toContain("credential_assignment");
|
|
184
|
-
expect(outcome.detectors).toContain("aws_access_key_id");
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
describe("hook-scanoutput / formatWarning", () => {
|
|
189
|
-
test("names the detector and location but never claims prevention", () => {
|
|
190
|
-
const outcome = analyzeToolOutput("x", fakeScanner({ findings: [finding()] as never }));
|
|
191
|
-
const warning = formatWarning(outcome);
|
|
192
|
-
expect(warning).toContain("credential_assignment");
|
|
193
|
-
expect(warning).toContain("<stdin>:12");
|
|
194
|
-
// This guard reports an exposure that has ALREADY been persisted. Wording that
|
|
195
|
-
// implies it stopped anything would retire the worry without retiring the risk.
|
|
196
|
-
expect(warning.toLowerCase()).not.toContain("blocked");
|
|
197
|
-
expect(warning.toLowerCase()).not.toContain("prevented");
|
|
198
|
-
expect(warning.toLowerCase()).toContain("already");
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test("carries only the scanner's redacted preview, never a raw value", () => {
|
|
202
|
-
const outcome = analyzeToolOutput(
|
|
203
|
-
"x",
|
|
204
|
-
fakeScanner({ findings: [finding({ preview: "KEY=***REDACTED***" })] as never }),
|
|
205
|
-
);
|
|
206
|
-
expect(formatWarning(outcome)).toContain("***REDACTED***");
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe("hook-scanoutput / buildHookOutput", () => {
|
|
211
|
-
test("ALWAYS continues, on every outcome, including a scanner error", () => {
|
|
212
|
-
const outcomes: ScanOutcome["status"][] = ["clean", "found", "unscanned", "empty", "error"];
|
|
213
|
-
for (const status of outcomes) {
|
|
214
|
-
expect(buildHookOutput({ status, findingCount: 0, detectors: [], previews: [] }).continue).toBe(true);
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
});
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
const homes: string[] = [];
|
|
7
|
-
const hookPath = join(import.meta.dir, "hook.ts");
|
|
8
|
-
|
|
9
|
-
async function invoke(home: string, input: object): Promise<Record<string, unknown>> {
|
|
10
|
-
const proc = Bun.spawn(["bun", "run", hookPath], {
|
|
11
|
-
stdin: new Response(JSON.stringify(input)),
|
|
12
|
-
stdout: "pipe",
|
|
13
|
-
stderr: "pipe",
|
|
14
|
-
env: { ...process.env, HOME: home },
|
|
15
|
-
});
|
|
16
|
-
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
17
|
-
expect(exitCode).toBe(0);
|
|
18
|
-
return JSON.parse(stdout);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function input(session: string, command = "bun test", exitCode = 1): object {
|
|
22
|
-
return {
|
|
23
|
-
session_id: session,
|
|
24
|
-
tool_name: "Bash",
|
|
25
|
-
tool_input: { command },
|
|
26
|
-
tool_response: { exit_code: exitCode, stderr: "tests failed\nmore details" },
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
afterEach(() => {
|
|
31
|
-
for (const home of homes.splice(0)) rmSync(home, { recursive: true, force: true });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe("spiral detector", () => {
|
|
35
|
-
test("interrupts on the fifth identical red signature", async () => {
|
|
36
|
-
const home = mkdtempSync(join(tmpdir(), "spiral-detector-"));
|
|
37
|
-
homes.push(home);
|
|
38
|
-
for (let attempt = 1; attempt < 5; attempt++) {
|
|
39
|
-
expect(await invoke(home, input("session-1"))).toEqual({ continue: true });
|
|
40
|
-
}
|
|
41
|
-
expect(await invoke(home, input("session-1"))).toMatchObject({
|
|
42
|
-
continue: false,
|
|
43
|
-
stopReason: expect.stringContaining("after 5 identical command failures"),
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("successes and changed failures reset the streak", async () => {
|
|
48
|
-
const home = mkdtempSync(join(tmpdir(), "spiral-detector-"));
|
|
49
|
-
homes.push(home);
|
|
50
|
-
for (let attempt = 0; attempt < 4; attempt++) await invoke(home, input("session-2"));
|
|
51
|
-
expect(await invoke(home, input("session-2", "bun test", 0))).toEqual({ continue: true });
|
|
52
|
-
for (let attempt = 0; attempt < 4; attempt++) await invoke(home, input("session-2"));
|
|
53
|
-
expect(await invoke(home, input("session-2", "bun run typecheck"))).toEqual({ continue: true });
|
|
54
|
-
for (let attempt = 1; attempt < 5; attempt++) {
|
|
55
|
-
expect(await invoke(home, input("session-2"))).toEqual({ continue: true });
|
|
56
|
-
}
|
|
57
|
-
expect(await invoke(home, input("session-2"))).toMatchObject({ continue: false });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("supports the repository's legacy tool_output field", async () => {
|
|
61
|
-
const home = mkdtempSync(join(tmpdir(), "spiral-detector-"));
|
|
62
|
-
homes.push(home);
|
|
63
|
-
const legacy = {
|
|
64
|
-
session_id: "session-3",
|
|
65
|
-
tool_name: "Bash",
|
|
66
|
-
tool_input: { command: "npm test" },
|
|
67
|
-
tool_output: { exitCode: "2", stderr: "same error" },
|
|
68
|
-
};
|
|
69
|
-
for (let attempt = 1; attempt < 5; attempt++) await invoke(home, legacy);
|
|
70
|
-
expect(await invoke(home, legacy)).toMatchObject({ continue: false });
|
|
71
|
-
});
|
|
72
|
-
});
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ESNext"],
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"allowImportingTsExtensions": true,
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"isolatedModules": true,
|
|
14
|
-
"noEmit": true,
|
|
15
|
-
"noUnusedLocals": true,
|
|
16
|
-
"noUnusedParameters": true,
|
|
17
|
-
"declaration": true,
|
|
18
|
-
"declarationMap": true,
|
|
19
|
-
"outDir": "./dist",
|
|
20
|
-
"rootDir": "./src",
|
|
21
|
-
"types": ["bun-types"]
|
|
22
|
-
},
|
|
23
|
-
"include": ["src/**/*"],
|
|
24
|
-
"exclude": ["node_modules", "dist"]
|
|
25
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ESNext"],
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"allowImportingTsExtensions": true,
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"isolatedModules": true,
|
|
14
|
-
"noEmit": true,
|
|
15
|
-
"noUnusedLocals": true,
|
|
16
|
-
"noUnusedParameters": true,
|
|
17
|
-
"declaration": true,
|
|
18
|
-
"declarationMap": true,
|
|
19
|
-
"outDir": "./dist",
|
|
20
|
-
"rootDir": "./src",
|
|
21
|
-
"types": ["bun-types"]
|
|
22
|
-
},
|
|
23
|
-
"include": ["src/**/*"],
|
|
24
|
-
"exclude": ["node_modules", "dist"]
|
|
25
|
-
}
|