clast-ai 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.
- package/.claude-plugin/plugin.json +22 -0
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +419 -0
- package/commands/clast-status.md +9 -0
- package/dist/config/defaults.d.ts +19 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +44 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +48 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/index.d.ts +16 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +54 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/queries.d.ts +44 -0
- package/dist/db/queries.d.ts.map +1 -0
- package/dist/db/queries.js +251 -0
- package/dist/db/queries.js.map +1 -0
- package/dist/db/schema.d.ts +3 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +94 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/types.d.ts +44 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/llm/comment-generator.d.ts +26 -0
- package/dist/llm/comment-generator.d.ts.map +1 -0
- package/dist/llm/comment-generator.js +106 -0
- package/dist/llm/comment-generator.js.map +1 -0
- package/dist/llm/index.d.ts +24 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +54 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/prompts.d.ts +9 -0
- package/dist/llm/prompts.d.ts.map +1 -0
- package/dist/llm/prompts.js +79 -0
- package/dist/llm/prompts.js.map +1 -0
- package/dist/parser/call-resolver.d.ts +15 -0
- package/dist/parser/call-resolver.d.ts.map +1 -0
- package/dist/parser/call-resolver.js +16 -0
- package/dist/parser/call-resolver.js.map +1 -0
- package/dist/parser/extractor.d.ts +17 -0
- package/dist/parser/extractor.d.ts.map +1 -0
- package/dist/parser/extractor.js +254 -0
- package/dist/parser/extractor.js.map +1 -0
- package/dist/parser/index.d.ts +24 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +190 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/languages.d.ts +11 -0
- package/dist/parser/languages.d.ts.map +1 -0
- package/dist/parser/languages.js +74 -0
- package/dist/parser/languages.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +88 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/register-tools.d.ts +7 -0
- package/dist/server/register-tools.d.ts.map +1 -0
- package/dist/server/register-tools.js +17 -0
- package/dist/server/register-tools.js.map +1 -0
- package/dist/server/tools/clast-call-graph.d.ts +4 -0
- package/dist/server/tools/clast-call-graph.d.ts.map +1 -0
- package/dist/server/tools/clast-call-graph.js +70 -0
- package/dist/server/tools/clast-call-graph.js.map +1 -0
- package/dist/server/tools/clast-comment.d.ts +5 -0
- package/dist/server/tools/clast-comment.d.ts.map +1 -0
- package/dist/server/tools/clast-comment.js +64 -0
- package/dist/server/tools/clast-comment.js.map +1 -0
- package/dist/server/tools/clast-file-summary.d.ts +4 -0
- package/dist/server/tools/clast-file-summary.d.ts.map +1 -0
- package/dist/server/tools/clast-file-summary.js +42 -0
- package/dist/server/tools/clast-file-summary.js.map +1 -0
- package/dist/server/tools/clast-get-context.d.ts +4 -0
- package/dist/server/tools/clast-get-context.d.ts.map +1 -0
- package/dist/server/tools/clast-get-context.js +80 -0
- package/dist/server/tools/clast-get-context.js.map +1 -0
- package/dist/server/tools/clast-reindex.d.ts +5 -0
- package/dist/server/tools/clast-reindex.d.ts.map +1 -0
- package/dist/server/tools/clast-reindex.js +36 -0
- package/dist/server/tools/clast-reindex.js.map +1 -0
- package/dist/server/tools/clast-search.d.ts +4 -0
- package/dist/server/tools/clast-search.d.ts.map +1 -0
- package/dist/server/tools/clast-search.js +29 -0
- package/dist/server/tools/clast-search.js.map +1 -0
- package/dist/server/tools/clast-status.d.ts +5 -0
- package/dist/server/tools/clast-status.d.ts.map +1 -0
- package/dist/server/tools/clast-status.js +23 -0
- package/dist/server/tools/clast-status.js.map +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/watcher/gitignore.d.ts +9 -0
- package/dist/watcher/gitignore.d.ts.map +1 -0
- package/dist/watcher/gitignore.js +38 -0
- package/dist/watcher/gitignore.js.map +1 -0
- package/dist/watcher/index.d.ts +23 -0
- package/dist/watcher/index.d.ts.map +1 -0
- package/dist/watcher/index.js +111 -0
- package/dist/watcher/index.js.map +1 -0
- package/package.json +65 -0
- package/skills/clast-explore/SKILL.md +41 -0
- package/src/config/defaults.ts +61 -0
- package/src/config/index.ts +55 -0
- package/src/db/index.ts +63 -0
- package/src/db/queries.ts +332 -0
- package/src/db/schema.ts +106 -0
- package/src/db/types.ts +47 -0
- package/src/llm/comment-generator.ts +131 -0
- package/src/llm/index.ts +80 -0
- package/src/llm/prompts.ts +95 -0
- package/src/parser/call-resolver.ts +17 -0
- package/src/parser/extractor.ts +303 -0
- package/src/parser/index.ts +226 -0
- package/src/parser/languages.ts +86 -0
- package/src/server/index.ts +109 -0
- package/src/server/register-tools.ts +30 -0
- package/src/server/tools/clast-call-graph.ts +82 -0
- package/src/server/tools/clast-comment.ts +76 -0
- package/src/server/tools/clast-file-summary.ts +59 -0
- package/src/server/tools/clast-get-context.ts +97 -0
- package/src/server/tools/clast-reindex.ts +52 -0
- package/src/server/tools/clast-search.ts +38 -0
- package/src/server/tools/clast-status.ts +37 -0
- package/src/types.ts +69 -0
- package/src/watcher/gitignore.ts +42 -0
- package/src/watcher/index.ts +131 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { ClastDatabase } from '../../db/index.js';
|
|
4
|
+
|
|
5
|
+
export function registerGetContextTool(server: McpServer, db: ClastDatabase): void {
|
|
6
|
+
server.tool(
|
|
7
|
+
'clast_get_context',
|
|
8
|
+
'Get full context for a symbol: its definition, comments/documentation, callers, callees, and parent class (if method). This is the primary tool for understanding a symbol before editing — use instead of reading the whole file.',
|
|
9
|
+
{
|
|
10
|
+
name: z.string().describe('Symbol name to look up'),
|
|
11
|
+
includeBody: z.boolean().default(false).describe('Include the full function/class body text'),
|
|
12
|
+
},
|
|
13
|
+
async ({ name, includeBody }) => {
|
|
14
|
+
const ctx = db.queries.getNodeWithContext(name);
|
|
15
|
+
|
|
16
|
+
if (ctx.nodes.length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{
|
|
19
|
+
type: 'text' as const,
|
|
20
|
+
text: JSON.stringify({ error: `No symbol found with name "${name}"` }),
|
|
21
|
+
}],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = ctx.nodes.map(node => {
|
|
26
|
+
const nodeComments = ctx.comments.filter(c => c.node_id === node.id);
|
|
27
|
+
const entry: Record<string, unknown> = {
|
|
28
|
+
name: node.name,
|
|
29
|
+
type: node.type,
|
|
30
|
+
filePath: node.file_path,
|
|
31
|
+
location: `${node.file_path}:${node.start_line}-${node.end_line}`,
|
|
32
|
+
signature: node.signature,
|
|
33
|
+
language: node.language,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (includeBody) {
|
|
37
|
+
entry.body = node.body_text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Comments: prioritize original, also show generated
|
|
41
|
+
const originalComment = nodeComments.find(c => c.source === 'original');
|
|
42
|
+
const generatedComment = nodeComments.find(c => c.source === 'generated');
|
|
43
|
+
|
|
44
|
+
if (originalComment) {
|
|
45
|
+
entry.comment = originalComment.text;
|
|
46
|
+
entry.commentSource = 'original';
|
|
47
|
+
}
|
|
48
|
+
if (generatedComment) {
|
|
49
|
+
entry.generatedDescription = generatedComment.text;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Parent (for methods inside classes)
|
|
53
|
+
if (node.parent_id) {
|
|
54
|
+
const parent = db.queries.getNodeById(node.parent_id);
|
|
55
|
+
if (parent) {
|
|
56
|
+
entry.parentClass = {
|
|
57
|
+
name: parent.name,
|
|
58
|
+
type: parent.type,
|
|
59
|
+
filePath: parent.file_path,
|
|
60
|
+
startLine: parent.start_line,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return entry;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Callers and callees (deduplicated across all matching nodes)
|
|
69
|
+
const callers = ctx.callers.map(n => ({
|
|
70
|
+
name: n.name,
|
|
71
|
+
type: n.type,
|
|
72
|
+
filePath: n.file_path,
|
|
73
|
+
startLine: n.start_line,
|
|
74
|
+
signature: n.signature,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const callees = ctx.callees.map(n => ({
|
|
78
|
+
name: n.name,
|
|
79
|
+
type: n.type,
|
|
80
|
+
filePath: n.file_path,
|
|
81
|
+
startLine: n.start_line,
|
|
82
|
+
signature: n.signature,
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
content: [{
|
|
87
|
+
type: 'text' as const,
|
|
88
|
+
text: JSON.stringify({
|
|
89
|
+
definitions: result,
|
|
90
|
+
callers,
|
|
91
|
+
callees,
|
|
92
|
+
}, null, 2),
|
|
93
|
+
}],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import type { ClastDatabase } from '../../db/index.js';
|
|
5
|
+
import type { ParserManager } from '../../parser/index.js';
|
|
6
|
+
|
|
7
|
+
export function registerReindexTool(
|
|
8
|
+
server: McpServer,
|
|
9
|
+
db: ClastDatabase,
|
|
10
|
+
parser: ParserManager,
|
|
11
|
+
projectRoot: string
|
|
12
|
+
): void {
|
|
13
|
+
server.tool(
|
|
14
|
+
'clast_reindex',
|
|
15
|
+
'Force re-index a specific file or the entire repository. Use when the index seems stale or after large changes.',
|
|
16
|
+
{
|
|
17
|
+
filePath: z.string().optional().describe('Specific file to reindex (relative path). Omit to reindex the entire repo.'),
|
|
18
|
+
},
|
|
19
|
+
async ({ filePath }) => {
|
|
20
|
+
if (filePath) {
|
|
21
|
+
const absPath = join(projectRoot, filePath);
|
|
22
|
+
const parsed = await parser.incrementalUpdate(absPath, projectRoot, db);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
content: [{
|
|
26
|
+
type: 'text' as const,
|
|
27
|
+
text: JSON.stringify({
|
|
28
|
+
file: filePath,
|
|
29
|
+
reindexed: parsed,
|
|
30
|
+
message: parsed ? 'File reindexed successfully' : 'File unchanged or unsupported',
|
|
31
|
+
}),
|
|
32
|
+
}],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = await parser.fullIndex(projectRoot, db);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
content: [{
|
|
40
|
+
type: 'text' as const,
|
|
41
|
+
text: JSON.stringify({
|
|
42
|
+
filesProcessed: result.filesProcessed,
|
|
43
|
+
nodesFound: result.nodesFound,
|
|
44
|
+
edgesResolved: result.edgesResolved,
|
|
45
|
+
durationMs: result.durationMs,
|
|
46
|
+
message: `Full reindex completed in ${result.durationMs}ms`,
|
|
47
|
+
}),
|
|
48
|
+
}],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { ClastDatabase } from '../../db/index.js';
|
|
4
|
+
|
|
5
|
+
export function registerSearchTool(server: McpServer, db: ClastDatabase): void {
|
|
6
|
+
server.tool(
|
|
7
|
+
'clast_search',
|
|
8
|
+
'Search AST nodes (functions, classes, methods, etc.) by name pattern and optional type filter. Returns matching symbols with file locations and signatures.',
|
|
9
|
+
{
|
|
10
|
+
query: z.string().describe('Search pattern (matches against symbol names, supports partial matches)'),
|
|
11
|
+
type: z.enum([
|
|
12
|
+
'function', 'method', 'class', 'interface', 'enum',
|
|
13
|
+
'import', 'export', 'variable', 'type_alias', 'struct', 'trait', 'module',
|
|
14
|
+
]).optional().describe('Filter by symbol type'),
|
|
15
|
+
limit: z.number().int().min(1).max(100).default(20).describe('Maximum results to return'),
|
|
16
|
+
},
|
|
17
|
+
async ({ query, type, limit }) => {
|
|
18
|
+
const results = db.queries.searchNodes(query, type, limit);
|
|
19
|
+
|
|
20
|
+
const formatted = results.map(n => ({
|
|
21
|
+
name: n.name,
|
|
22
|
+
type: n.type,
|
|
23
|
+
filePath: n.file_path,
|
|
24
|
+
startLine: n.start_line,
|
|
25
|
+
endLine: n.end_line,
|
|
26
|
+
signature: n.signature,
|
|
27
|
+
language: n.language,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
content: [{
|
|
32
|
+
type: 'text' as const,
|
|
33
|
+
text: JSON.stringify(formatted, null, 2),
|
|
34
|
+
}],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { ClastDatabase } from '../../db/index.js';
|
|
3
|
+
import type { FileWatcher } from '../../watcher/index.js';
|
|
4
|
+
|
|
5
|
+
export function registerStatusTool(
|
|
6
|
+
server: McpServer,
|
|
7
|
+
db: ClastDatabase,
|
|
8
|
+
watcher: FileWatcher
|
|
9
|
+
): void {
|
|
10
|
+
server.tool(
|
|
11
|
+
'clast_status',
|
|
12
|
+
'Show the current indexing status: files indexed, total symbols, total call edges, watcher state, and pending updates.',
|
|
13
|
+
{},
|
|
14
|
+
async () => {
|
|
15
|
+
const status = db.queries.getStatus();
|
|
16
|
+
|
|
17
|
+
const result = {
|
|
18
|
+
filesIndexed: status.files_indexed,
|
|
19
|
+
totalNodes: status.total_nodes,
|
|
20
|
+
totalEdges: status.total_edges,
|
|
21
|
+
totalComments: status.total_comments,
|
|
22
|
+
lastIndexed: status.last_indexed
|
|
23
|
+
? new Date(status.last_indexed).toISOString()
|
|
24
|
+
: null,
|
|
25
|
+
watcherActive: watcher.isActive,
|
|
26
|
+
pendingFiles: watcher.pendingCount,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
content: [{
|
|
31
|
+
type: 'text' as const,
|
|
32
|
+
text: JSON.stringify(result, null, 2),
|
|
33
|
+
}],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type AstNodeType =
|
|
2
|
+
| 'function'
|
|
3
|
+
| 'method'
|
|
4
|
+
| 'class'
|
|
5
|
+
| 'interface'
|
|
6
|
+
| 'enum'
|
|
7
|
+
| 'import'
|
|
8
|
+
| 'export'
|
|
9
|
+
| 'variable'
|
|
10
|
+
| 'type_alias'
|
|
11
|
+
| 'struct'
|
|
12
|
+
| 'trait'
|
|
13
|
+
| 'module';
|
|
14
|
+
|
|
15
|
+
export interface AstNode {
|
|
16
|
+
id?: number;
|
|
17
|
+
filePath: string;
|
|
18
|
+
name: string;
|
|
19
|
+
type: AstNodeType;
|
|
20
|
+
signature: string;
|
|
21
|
+
startLine: number;
|
|
22
|
+
endLine: number;
|
|
23
|
+
startCol: number;
|
|
24
|
+
endCol: number;
|
|
25
|
+
parentId: number | null;
|
|
26
|
+
language: string;
|
|
27
|
+
bodyText: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CallEdge {
|
|
31
|
+
id?: number;
|
|
32
|
+
callerId: number;
|
|
33
|
+
calleeId: number | null;
|
|
34
|
+
calleeName: string;
|
|
35
|
+
filePath: string;
|
|
36
|
+
line: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FileInfo {
|
|
40
|
+
filePath: string;
|
|
41
|
+
hash: string;
|
|
42
|
+
language: string;
|
|
43
|
+
lastIndexed: number;
|
|
44
|
+
nodeCount: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CommentInfo {
|
|
48
|
+
id?: number;
|
|
49
|
+
nodeId: number;
|
|
50
|
+
text: string;
|
|
51
|
+
source: 'original' | 'generated';
|
|
52
|
+
generatedAt: number | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ParseResult {
|
|
56
|
+
nodes: AstNode[];
|
|
57
|
+
edges: CallEdge[];
|
|
58
|
+
comments: CommentInfo[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface IndexStatus {
|
|
62
|
+
filesIndexed: number;
|
|
63
|
+
totalNodes: number;
|
|
64
|
+
totalEdges: number;
|
|
65
|
+
totalComments: number;
|
|
66
|
+
lastIndexed: number | null;
|
|
67
|
+
watcherActive: boolean;
|
|
68
|
+
pendingFiles: number;
|
|
69
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse .gitignore files into glob patterns suitable for chokidar's `ignored` option.
|
|
6
|
+
*/
|
|
7
|
+
export function loadGitignorePatterns(projectRoot: string): string[] {
|
|
8
|
+
const patterns: string[] = [];
|
|
9
|
+
const gitignorePath = join(projectRoot, '.gitignore');
|
|
10
|
+
|
|
11
|
+
if (!existsSync(gitignorePath)) return patterns;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(gitignorePath, 'utf-8');
|
|
15
|
+
for (const line of content.split('\n')) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
// Skip comments and empty lines
|
|
18
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
19
|
+
// Negation patterns are not supported by chokidar's simple ignored
|
|
20
|
+
if (trimmed.startsWith('!')) continue;
|
|
21
|
+
|
|
22
|
+
patterns.push(trimmed);
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Silently ignore read errors
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return patterns;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the full ignored patterns list combining .gitignore and config.
|
|
33
|
+
*/
|
|
34
|
+
export function buildIgnorePatterns(
|
|
35
|
+
projectRoot: string,
|
|
36
|
+
configIgnored: string[]
|
|
37
|
+
): string[] {
|
|
38
|
+
const gitignorePatterns = loadGitignorePatterns(projectRoot);
|
|
39
|
+
// Deduplicate
|
|
40
|
+
const all = new Set([...configIgnored, ...gitignorePatterns]);
|
|
41
|
+
return Array.from(all);
|
|
42
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { watch, type FSWatcher } from 'chokidar';
|
|
2
|
+
import { extname } from 'node:path';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { getSupportedExtensions } from '../parser/languages.js';
|
|
5
|
+
import { buildIgnorePatterns } from './gitignore.js';
|
|
6
|
+
import type { ParserManager } from '../parser/index.js';
|
|
7
|
+
import type { ClastDatabase } from '../db/index.js';
|
|
8
|
+
import type { ClastConfig } from '../config/index.js';
|
|
9
|
+
|
|
10
|
+
export class FileWatcher extends EventEmitter {
|
|
11
|
+
private watcher: FSWatcher | null = null;
|
|
12
|
+
private projectRoot: string;
|
|
13
|
+
private config: ClastConfig;
|
|
14
|
+
private db: ClastDatabase;
|
|
15
|
+
private parser: ParserManager;
|
|
16
|
+
private debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
17
|
+
private _pendingCount = 0;
|
|
18
|
+
private supportedExts: Set<string>;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
projectRoot: string,
|
|
22
|
+
config: ClastConfig,
|
|
23
|
+
db: ClastDatabase,
|
|
24
|
+
parser: ParserManager
|
|
25
|
+
) {
|
|
26
|
+
super();
|
|
27
|
+
this.projectRoot = projectRoot;
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.db = db;
|
|
30
|
+
this.parser = parser;
|
|
31
|
+
this.supportedExts = new Set(getSupportedExtensions(config.languages));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get isActive(): boolean {
|
|
35
|
+
return this.watcher !== null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get pendingCount(): number {
|
|
39
|
+
return this._pendingCount;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
start(): void {
|
|
43
|
+
if (this.watcher) return;
|
|
44
|
+
if (!this.config.watch.enabled) return;
|
|
45
|
+
|
|
46
|
+
const ignoredPatterns = buildIgnorePatterns(this.projectRoot, this.config.ignoredPaths);
|
|
47
|
+
|
|
48
|
+
this.watcher = watch(this.projectRoot, {
|
|
49
|
+
ignored: [
|
|
50
|
+
/(^|[/\\])\./,
|
|
51
|
+
...ignoredPatterns.map(p => {
|
|
52
|
+
if (!p.includes('*') && !p.includes('/')) {
|
|
53
|
+
return `**/${p}/**`;
|
|
54
|
+
}
|
|
55
|
+
return p;
|
|
56
|
+
}),
|
|
57
|
+
],
|
|
58
|
+
persistent: true,
|
|
59
|
+
ignoreInitial: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
this.watcher.on('add', path => this.handleFileChange(path));
|
|
63
|
+
this.watcher.on('change', path => this.handleFileChange(path));
|
|
64
|
+
this.watcher.on('unlink', path => this.handleFileDelete(path));
|
|
65
|
+
this.watcher.on('error', err => {
|
|
66
|
+
console.error('[clast] Watcher error:', err);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
stop(): void {
|
|
71
|
+
if (!this.watcher) return;
|
|
72
|
+
|
|
73
|
+
for (const timer of this.debounceTimers.values()) {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
}
|
|
76
|
+
this.debounceTimers.clear();
|
|
77
|
+
|
|
78
|
+
this.watcher.close();
|
|
79
|
+
this.watcher = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private handleFileChange(filePath: string): void {
|
|
83
|
+
const ext = extname(filePath);
|
|
84
|
+
if (!this.supportedExts.has(ext)) return;
|
|
85
|
+
|
|
86
|
+
const existing = this.debounceTimers.get(filePath);
|
|
87
|
+
if (existing) clearTimeout(existing);
|
|
88
|
+
|
|
89
|
+
this._pendingCount++;
|
|
90
|
+
this.debounceTimers.set(
|
|
91
|
+
filePath,
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
this.debounceTimers.delete(filePath);
|
|
94
|
+
this._pendingCount--;
|
|
95
|
+
this.processFile(filePath);
|
|
96
|
+
}, this.config.watch.debounceMs)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private handleFileDelete(filePath: string): void {
|
|
101
|
+
const ext = extname(filePath);
|
|
102
|
+
if (!this.supportedExts.has(ext)) return;
|
|
103
|
+
|
|
104
|
+
const existing = this.debounceTimers.get(filePath);
|
|
105
|
+
if (existing) {
|
|
106
|
+
clearTimeout(existing);
|
|
107
|
+
this.debounceTimers.delete(filePath);
|
|
108
|
+
this._pendingCount--;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
this.parser.removeFile(filePath, this.projectRoot, this.db);
|
|
113
|
+
this.emit('deleted', { filePath });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
this.emit('error', { filePath, error: err as Error });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async processFile(filePath: string): Promise<void> {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = await this.parser.incrementalUpdate(filePath, this.projectRoot, this.db);
|
|
122
|
+
if (parsed) {
|
|
123
|
+
const relPath = filePath.replace(/\\/g, '/');
|
|
124
|
+
const nodes = this.db.queries.getNodesByFile(relPath);
|
|
125
|
+
this.emit('indexed', { filePath, nodesFound: nodes.length });
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
this.emit('error', { filePath, error: err as Error });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
20
|
+
}
|