@kintsugi-ai/hook 0.1.0

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 (43) hide show
  1. package/.kintsugi/logs/hook.log +11 -0
  2. package/dist/agent-detect.d.ts +59 -0
  3. package/dist/agent-detect.d.ts.map +1 -0
  4. package/dist/agent-detect.js +123 -0
  5. package/dist/agent-detect.js.map +1 -0
  6. package/dist/agent-detect.test.d.ts +2 -0
  7. package/dist/agent-detect.test.d.ts.map +1 -0
  8. package/dist/agent-detect.test.js +42 -0
  9. package/dist/agent-detect.test.js.map +1 -0
  10. package/dist/context.d.ts +10 -0
  11. package/dist/context.d.ts.map +1 -0
  12. package/dist/context.js +81 -0
  13. package/dist/context.js.map +1 -0
  14. package/dist/escalation.d.ts +28 -0
  15. package/dist/escalation.d.ts.map +1 -0
  16. package/dist/escalation.js +48 -0
  17. package/dist/escalation.js.map +1 -0
  18. package/dist/flow-matcher.d.ts +9 -0
  19. package/dist/flow-matcher.d.ts.map +1 -0
  20. package/dist/flow-matcher.js +12 -0
  21. package/dist/flow-matcher.js.map +1 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +209 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/logger.d.ts +7 -0
  27. package/dist/logger.d.ts.map +1 -0
  28. package/dist/logger.js +58 -0
  29. package/dist/logger.js.map +1 -0
  30. package/dist/ui-file-detector.d.ts +9 -0
  31. package/dist/ui-file-detector.d.ts.map +1 -0
  32. package/dist/ui-file-detector.js +34 -0
  33. package/dist/ui-file-detector.js.map +1 -0
  34. package/package.json +29 -0
  35. package/src/agent-detect.test.ts +53 -0
  36. package/src/agent-detect.ts +167 -0
  37. package/src/context.ts +80 -0
  38. package/src/escalation.ts +60 -0
  39. package/src/flow-matcher.ts +13 -0
  40. package/src/index.ts +269 -0
  41. package/src/logger.ts +62 -0
  42. package/src/ui-file-detector.ts +40 -0
  43. package/tsconfig.json +8 -0
package/src/context.ts ADDED
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs';
2
+ import type { HookInput } from './agent-detect.js';
3
+
4
+ const MAX_CONTEXT_CHARS = 600;
5
+
6
+ /**
7
+ * Best-effort extraction of "what was the agent asked to do" from the latest
8
+ * turn, to give the vision classifier intent context. Claude Code Stop
9
+ * payloads carry `transcript_path` (JSONL); Antigravity carries
10
+ * `transcriptPath`. Codex has none — return empty and let the classifier
11
+ * decide without context.
12
+ */
13
+ export function extractTurnContext(input: HookInput): string {
14
+ const transcriptPath =
15
+ (typeof input.transcript_path === 'string' && input.transcript_path) ||
16
+ input.transcriptPath;
17
+ if (!transcriptPath) return '';
18
+
19
+ try {
20
+ const content = fs.readFileSync(transcriptPath, 'utf-8');
21
+ const userTexts: string[] = [];
22
+
23
+ for (const line of content.split('\n')) {
24
+ const trimmed = line.trim();
25
+ if (!trimmed) continue;
26
+ let entry: any;
27
+ try {
28
+ entry = JSON.parse(trimmed);
29
+ } catch {
30
+ continue;
31
+ }
32
+ const text = extractUserText(entry);
33
+ if (text) userTexts.push(text);
34
+ }
35
+
36
+ const context = userTexts.slice(-2).join(' | ');
37
+ return context.length > MAX_CONTEXT_CHARS
38
+ ? context.slice(0, MAX_CONTEXT_CHARS) + '…'
39
+ : context;
40
+ } catch (err: unknown) {
41
+ // ENOENT is expected when the agent has no transcript — everything else is unusual
42
+ const isNotFound = typeof err === 'object' && err !== null && 'code' in err && (err as { code: string }).code === 'ENOENT';
43
+ if (!isNotFound) {
44
+ console.warn(`kintsugi: failed to read transcript at ${transcriptPath}:`, err);
45
+ }
46
+ return '';
47
+ }
48
+ }
49
+
50
+ function extractUserText(entry: any): string | undefined {
51
+ // Claude Code transcript entries: { type: "user", message: { role: "user", content: string | [{type:"text",text}] } }
52
+ const message = entry?.message ?? entry;
53
+ const role = message?.role ?? entry?.type;
54
+ if (role !== 'user' && role !== 'human') return undefined;
55
+ // Skip tool results / interrupt notices — only real instructions help
56
+ if (entry?.type === 'tool_result') return undefined;
57
+
58
+ const content = message?.content;
59
+ if (typeof content === 'string' && content.trim()) {
60
+ return isNoise(content) ? undefined : content.trim();
61
+ }
62
+ if (Array.isArray(content)) {
63
+ const text = content
64
+ .filter((c: any) => c?.type === 'text' && typeof c.text === 'string')
65
+ .map((c: any) => c.text)
66
+ .join(' ')
67
+ .trim();
68
+ return text && !isNoise(text) ? text : undefined;
69
+ }
70
+ return undefined;
71
+ }
72
+
73
+ /** Filters transcript noise like system-injected reminders and empty interrupts. */
74
+ function isNoise(text: string): boolean {
75
+ const t = text.trim();
76
+ if (!t) return true;
77
+ if (t.startsWith('<')) return true;
78
+ if (t === '[Request interrupted by user]') return true;
79
+ return false;
80
+ }
@@ -0,0 +1,60 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import type { ComparisonResult } from '@kintsugi-ai/core';
4
+
5
+ export interface EscalationSignal {
6
+ flowName: string;
7
+ result: ComparisonResult;
8
+ oldVideoPath?: string;
9
+ newVideoPath?: string;
10
+ timestamp: string;
11
+ attempt: number;
12
+ maxAttempts: number;
13
+ }
14
+
15
+ function getEscalationFilePath(projectDir: string): string {
16
+ return path.join(projectDir, '.kintsugi', '.escalation.json');
17
+ }
18
+
19
+ /**
20
+ * Writes an escalation signal for the VS Code extension.
21
+ * @param projectDir The project directory.
22
+ * @param signal The escalation signal data.
23
+ */
24
+ export async function writeEscalation(projectDir: string, signal: EscalationSignal): Promise<void> {
25
+ const filePath = getEscalationFilePath(projectDir);
26
+ const dir = path.dirname(filePath);
27
+ await fs.mkdir(dir, { recursive: true });
28
+ await fs.writeFile(filePath, JSON.stringify(signal, null, 2), 'utf-8');
29
+ }
30
+
31
+ /**
32
+ * Reads the current escalation signal.
33
+ * @param projectDir The project directory.
34
+ * @returns The current escalation signal or null if not found.
35
+ */
36
+ export async function readEscalation(projectDir: string): Promise<EscalationSignal | null> {
37
+ const filePath = getEscalationFilePath(projectDir);
38
+ try {
39
+ const content = await fs.readFile(filePath, 'utf-8');
40
+ return JSON.parse(content) as EscalationSignal;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Clears the escalation signal.
48
+ * @param projectDir The project directory.
49
+ */
50
+ export async function clearEscalation(projectDir: string): Promise<void> {
51
+ const filePath = getEscalationFilePath(projectDir);
52
+ try {
53
+ await fs.unlink(filePath);
54
+ } catch (err: unknown) {
55
+ const isNotFound = typeof err === 'object' && err !== null && 'code' in err && (err as { code: string }).code === 'ENOENT';
56
+ if (!isNotFound) {
57
+ throw err;
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,13 @@
1
+ import type { FlowMetadata } from '@kintsugi-ai/core';
2
+
3
+ /**
4
+ * Determines which flows are affected by a file change.
5
+ * @param filePath The modified file path.
6
+ * @param flows The list of available flows.
7
+ * @returns Array of affected flow names.
8
+ */
9
+ export function findAffectedFlows(filePath: string, flows: FlowMetadata[]): string[] {
10
+ // A conservative approach: for now, assume all flows might be affected by the UI change.
11
+ // Future enhancements can check if the file path matches explicit mappings.
12
+ return flows.map(f => f.name);
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import {
6
+ loadConfig,
7
+ listFlows,
8
+ loadFlow,
9
+ captureCurrentState,
10
+ compareFlow,
11
+ formatAgentFeedback,
12
+ buildClassifierContext,
13
+ loadKintsugiEnv,
14
+ DiffResult,
15
+ type ComparisonResult,
16
+ type KintsugiConfig
17
+ } from '@kintsugi-ai/core';
18
+ import {
19
+ detectAgent,
20
+ outputFeedback,
21
+ exitWithResult,
22
+ type AgentType,
23
+ type HookInput
24
+ } from './agent-detect.js';
25
+ import { extractTurnContext } from './context.js';
26
+ import { writeEscalation, readEscalation, clearEscalation } from './escalation.js';
27
+ import { createLogger } from './logger.js';
28
+
29
+ async function main() {
30
+ const earlyWarnings: string[] = [];
31
+
32
+ // Check command line arguments for explicit event flags (e.g. --event stop)
33
+ const isExplicitStopEvent = process.argv.includes('stop') ||
34
+ process.argv.some(arg => arg.toLowerCase().includes('stop'));
35
+
36
+ let stdinData = '';
37
+ try {
38
+ stdinData = fs.readFileSync(0, 'utf-8');
39
+ } catch {
40
+ // stdin may not be available
41
+ earlyWarnings.push('could not read stdin (no piped hook payload?)');
42
+ }
43
+
44
+ let input: HookInput = {};
45
+ if (stdinData.trim()) {
46
+ try {
47
+ input = JSON.parse(stdinData);
48
+ } catch (e) {
49
+ earlyWarnings.push(`stdin is not valid JSON: ${(e as Error).message} preview=${stdinData.slice(0, 500)}`);
50
+ }
51
+ } else {
52
+ earlyWarnings.push('stdin was empty — agent sent no hook payload');
53
+ }
54
+
55
+ const agent = detectAgent(input);
56
+ // Stop events arrive as terminationReason (agy), hook_event_name: "Stop"
57
+ // (Claude Code), or the explicit --event stop flag in registered configs.
58
+ const eventName = typeof input.hook_event_name === 'string'
59
+ ? input.hook_event_name.toLowerCase()
60
+ : undefined;
61
+ const isStopEvent = isExplicitStopEvent || Boolean(input.terminationReason) || eventName === 'stop';
62
+
63
+ // Some agents (Antigravity/agy) run hooks with cwd set to their config
64
+ // dir (e.g. <workspace>/.agents) rather than the workspace root. Resolve
65
+ // the real project dir from the payload's workspacePaths when available.
66
+ const projectDir = input.workspacePaths?.[0] ?? process.cwd();
67
+ const log = createLogger(projectDir);
68
+ log.info('hook invoked', {
69
+ argv: process.argv.slice(2),
70
+ cwd: process.cwd(),
71
+ projectDir,
72
+ stdinBytes: stdinLength(),
73
+ });
74
+ for (const w of earlyWarnings) log.warn(w);
75
+ log.info('agent detected', { agent, isStopEvent, isExplicitStopEvent, inputKeys: Object.keys(input) });
76
+
77
+ if (!isStopEvent) {
78
+ // Kintsugi checks once per turn (Stop event), not on every file edit —
79
+ // per-edit checks would replay flows dozens of times per turn.
80
+ log.info('non-stop event — deferring visual check to turn end');
81
+ outputFeedback(agent, '', { isStopEvent: false, hasRegression: false });
82
+ process.exit(0);
83
+ }
84
+
85
+ await runTurnEndCheck(agent, input, projectDir, log);
86
+ }
87
+
88
+ interface FlowCheckOutcome {
89
+ flowName: string;
90
+ isRegression: boolean;
91
+ comparisonResult?: ComparisonResult;
92
+ oldPath?: string;
93
+ newPath?: string;
94
+ }
95
+
96
+ /**
97
+ * Turn-end visual check: verifies every recorded flow in parallel (each in
98
+ * its own browser context), classifies oversized diffs in parallel, and on a
99
+ * regression blocks the agent's stop with fix-it feedback until it passes or
100
+ * maxAttempts is reached.
101
+ */
102
+ async function runTurnEndCheck(
103
+ agent: AgentType,
104
+ input: HookInput,
105
+ projectDir: string,
106
+ log: ReturnType<typeof createLogger>
107
+ ): Promise<void> {
108
+ // Pick up gitignored <project>/.kintsugi/.env before resolving the token
109
+ loadKintsugiEnv(projectDir);
110
+ const config: KintsugiConfig = await loadConfig(projectDir);
111
+ log.info('config loaded', { devServerUrl: config.devServerUrl, classifierModel: config.classifier.model });
112
+
113
+ // Shared builder (also used by `kintsugi check` and MCP) — bootstraps an
114
+ // anonymous hosted key on first use so the classifier is never silently dark.
115
+ const classifierContext = await buildClassifierContext(config, {
116
+ context: extractTurnContext(input),
117
+ logger: log,
118
+ bootstrapKey: true,
119
+ projectDir,
120
+ });
121
+
122
+ const flows = await listFlows(projectDir);
123
+ log.info('flows discovered', {
124
+ flowsDir: path.join(projectDir, '.kintsugi', 'flows'),
125
+ count: flows.length,
126
+ names: flows.map(f => f.name),
127
+ });
128
+
129
+ if (flows.length === 0) {
130
+ log.info('no recorded flows — nothing to check');
131
+ outputFeedback(agent, '', { isStopEvent: true, hasRegression: false });
132
+ process.exit(0);
133
+ }
134
+
135
+ const maxAttempts = config.agent.maxRetries;
136
+ const outcomes = await Promise.all(
137
+ flows.map(async flow => {
138
+ try {
139
+ return await checkFlow(flow.name, config, classifierContext, projectDir, log);
140
+ } catch (err) {
141
+ log.error('flow check crashed', { flowName: flow.name, error: (err as Error).message });
142
+ return { flowName: flow.name, isRegression: false } as FlowCheckOutcome;
143
+ }
144
+ })
145
+ );
146
+
147
+ const broken = outcomes.filter(o => o.isRegression);
148
+
149
+ if (broken.length === 0) {
150
+ // Clean pass — either nothing broke or the agent just fixed the
151
+ // previous regression. Either way the agent may stop.
152
+ log.info('turn-end check passed — clearing any previous escalation');
153
+ await clearEscalation(projectDir);
154
+ outputFeedback(agent, '', { isStopEvent: true, hasRegression: false });
155
+ process.exit(0);
156
+ }
157
+
158
+ // The fresh check above is the source of truth; only treat a pending
159
+ // escalation as "still unfixed" now that the current state is known broken.
160
+ const pendingEscalation = await readEscalation(projectDir);
161
+ if (pendingEscalation && pendingEscalation.attempt >= maxAttempts) {
162
+ log.info('max fix attempts reached — allowing agent to stop', {
163
+ flowName: pendingEscalation.flowName,
164
+ attempt: pendingEscalation.attempt,
165
+ maxAttempts,
166
+ });
167
+ await clearEscalation(projectDir);
168
+ outputFeedback(agent, '', { isStopEvent: true, hasRegression: false });
169
+ process.exit(0);
170
+ }
171
+
172
+ const first = broken[0];
173
+ const attempt = (pendingEscalation?.attempt || 0) + 1;
174
+ await writeEscalation(projectDir, {
175
+ flowName: first.flowName,
176
+ result: first.comparisonResult!,
177
+ oldVideoPath: first.oldPath ?? '',
178
+ newVideoPath: first.newPath ?? '',
179
+ timestamp: new Date().toISOString(),
180
+ attempt,
181
+ maxAttempts,
182
+ });
183
+
184
+ // Fix-it feedback covers every broken flow: aria diffs, image pairs, per-pair fixes
185
+ const combinedFeedback = broken
186
+ .map(o => formatAgentFeedback(o.comparisonResult!, o.flowName))
187
+ .join('\n\n');
188
+
189
+ log.warn('turn-end regression — blocking agent stop with feedback', {
190
+ flows: broken.map(b => b.flowName),
191
+ attempt,
192
+ maxAttempts,
193
+ });
194
+ outputFeedback(agent, combinedFeedback, { isStopEvent: true, hasRegression: true });
195
+ exitWithResult(agent, true);
196
+ }
197
+
198
+ /** Replays one flow and compares it against its baseline. */
199
+ async function checkFlow(
200
+ flowName: string,
201
+ config: KintsugiConfig,
202
+ classifierContext: Awaited<ReturnType<typeof buildClassifierContext>>,
203
+ projectDir: string,
204
+ log: ReturnType<typeof createLogger>
205
+ ): Promise<FlowCheckOutcome> {
206
+ const flowRecord = await loadFlow(projectDir, flowName);
207
+ if (!flowRecord) {
208
+ log.warn('flow metadata listed but could not be loaded', { flowName });
209
+ return { flowName, isRegression: false };
210
+ }
211
+ log.info('checking flow', { flowName, stepCount: flowRecord.steps.length, baselineScreenshots: flowRecord.screenshotPaths.length });
212
+
213
+ const outputDir = path.join(projectDir, '.kintsugi', '.tmp', flowName);
214
+ fs.mkdirSync(outputDir, { recursive: true });
215
+
216
+ const currentState = await captureCurrentState({
217
+ url: config.devServerUrl,
218
+ flow: flowRecord,
219
+ outputDir,
220
+ viewport: config.viewport,
221
+ });
222
+ log.info('current state captured', { flowName, url: config.devServerUrl, screenshots: currentState.screenshots.length, errors: currentState.errors.length });
223
+
224
+ const comparisonResult = await compareFlow(flowRecord, currentState, { outputDir, classifier: classifierContext });
225
+ log.info('comparison finished', { flowName, result: comparisonResult.result });
226
+
227
+ // Any definite visual change (CHANGED) or broken capture is a regression —
228
+ // the baseline flow no longer matches the live page. INTENTIONAL means the
229
+ // classifier accepted the change.
230
+ const isRegression = comparisonResult.result !== DiffResult.IDENTICAL &&
231
+ comparisonResult.result !== DiffResult.MINOR &&
232
+ comparisonResult.result !== DiffResult.INTENTIONAL;
233
+
234
+ if (!isRegression) {
235
+ if (comparisonResult.result === DiffResult.INTENTIONAL) {
236
+ log.info('large visual change accepted as intentional by classifier', { flowName });
237
+ }
238
+ return { flowName, isRegression: false, comparisonResult };
239
+ }
240
+
241
+ log.warn('REGRESSION detected', { flowName, result: comparisonResult.result });
242
+ const failedIndex = comparisonResult.failedStep ?? 0;
243
+ return {
244
+ flowName,
245
+ isRegression: true,
246
+ comparisonResult,
247
+ oldPath: flowRecord.screenshotPaths[failedIndex] ?? flowRecord.screenshotPaths[0] ?? '',
248
+ newPath: currentState.screenshotPaths[failedIndex] ?? currentState.screenshotPaths[0] ?? '',
249
+ };
250
+ }
251
+
252
+ function stdinLength(): number {
253
+ try {
254
+ return fs.fstatSync(0).size;
255
+ } catch {
256
+ return -1;
257
+ }
258
+ }
259
+
260
+ main().catch((e) => {
261
+ try {
262
+ const log = createLogger(process.cwd());
263
+ log.error('hook crashed', e);
264
+ } catch { /* ignore */ }
265
+ // Exit non-zero so the agent knows something went wrong.
266
+ // Write a diagnostic to stderr for agents that capture it.
267
+ process.stderr.write(`kintsugi hook crashed: ${e instanceof Error ? e.message : String(e)}\n`);
268
+ process.exit(1);
269
+ });
package/src/logger.ts ADDED
@@ -0,0 +1,62 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Debug logging for the kintsugi hook.
6
+ *
7
+ * The #1 failure mode for agent hooks is silent early-exit: the hook runs,
8
+ * decides "nothing to do", and exits 0 without any trace. To debug why a
9
+ * regression was NOT caught, we log every decision point to
10
+ * `<projectDir>/.kintsugi/logs/hook.log` (always) and to stderr (when
11
+ * KINTSUGI_DEBUG=1, since agent CLIs may otherwise swallow or misrender
12
+ * unexpected stderr).
13
+ */
14
+
15
+ const LOG_DIRNAME = path.join('.kintsugi', 'logs');
16
+ const LOG_FILENAME = 'hook.log';
17
+
18
+ export function createLogger(projectDir: string) {
19
+ const debugToStderr = process.env.KINTSUGI_DEBUG === '1';
20
+ const logPath = path.join(projectDir, LOG_DIRNAME, LOG_FILENAME);
21
+
22
+ function write(line: string): void {
23
+ const entry = `[${new Date().toISOString()}] ${line}\n`;
24
+ try {
25
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
26
+ fs.appendFileSync(logPath, entry);
27
+ } catch {
28
+ // Never let logging break the hook
29
+ }
30
+ if (debugToStderr) {
31
+ process.stderr.write(`[kintsugi-hook] ${entry}`);
32
+ }
33
+ }
34
+
35
+ return {
36
+ info: (message: string, data?: unknown) => {
37
+ write(data === undefined ? message : `${message} ${safeStringify(data)}`);
38
+ },
39
+ warn: (message: string, data?: unknown) => {
40
+ write(`WARN: ${data === undefined ? message : `${message} ${safeStringify(data)}`}`);
41
+ },
42
+ error: (message: string, error?: unknown) => {
43
+ write(`ERROR: ${message}${error ? ` ${formatError(error)}` : ''}`);
44
+ },
45
+ logPath,
46
+ };
47
+ }
48
+
49
+ function safeStringify(data: unknown): string {
50
+ try {
51
+ return JSON.stringify(data);
52
+ } catch {
53
+ return String(data);
54
+ }
55
+ }
56
+
57
+ function formatError(error: unknown): string {
58
+ if (error instanceof Error) {
59
+ return `${error.message}\n${error.stack ?? ''}`;
60
+ }
61
+ return safeStringify(error);
62
+ }
@@ -0,0 +1,40 @@
1
+ import path from 'node:path';
2
+
3
+ export const DEFAULT_UI_PATTERNS = [
4
+ '**/*.tsx', '**/*.jsx', '**/*.vue', '**/*.svelte',
5
+ '**/*.html', '**/*.css', '**/*.scss'
6
+ ];
7
+
8
+ const UI_EXTENSIONS = new Set(['.tsx', '.jsx', '.vue', '.svelte', '.html', '.css', '.scss']);
9
+ const NON_UI_SUBSTRINGS = ['.test.', '.spec.', '.config.', '/test/', '/tests/'];
10
+
11
+ /**
12
+ * Detects if a file is a UI file that might require visual regression testing.
13
+ * @param filePath The path to the modified file.
14
+ * @param patterns Optional array of glob patterns to match against.
15
+ * @returns True if the file is considered a UI file.
16
+ */
17
+ export function isUIFile(filePath: string, patterns: string[] = DEFAULT_UI_PATTERNS): boolean {
18
+ const ext = path.extname(filePath).toLowerCase();
19
+
20
+ for (const sub of NON_UI_SUBSTRINGS) {
21
+ if (filePath.includes(sub)) {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ if (UI_EXTENSIONS.has(ext)) {
27
+ return true;
28
+ }
29
+
30
+ // Simplified pattern matching if not matched by extension
31
+ for (const pattern of patterns) {
32
+ // Extract extension from glob patterns like '**/*.tsx' or '*.css'
33
+ const extMatch = pattern.match(/\*\.([a-zA-Z0-9]+)$/);
34
+ if (extMatch && ext === '.' + extMatch[1]) {
35
+ return true;
36
+ }
37
+ }
38
+
39
+ return false;
40
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }