forge-server 0.1.0 → 0.1.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/bin/setup-forge.sh +1 -1
- package/bin/setup.js +99 -0
- package/dist/cli.js +37 -37
- package/dist/cli.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/storage/schema.js +113 -113
- package/dist/storage/schema.js.map +1 -1
- package/dist/storage/sqlite.js +1 -1
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/util/logger.d.ts +1 -1
- package/dist/util/logger.js +1 -1
- package/dist/util/types.js +1 -1
- package/dist/util/types.js.map +1 -1
- package/package.json +8 -2
- package/plugin/.mcp.json +1 -1
- package/.claude/hooks/worktree-create.sh +0 -64
- package/.claude/hooks/worktree-remove.sh +0 -57
- package/.claude/settings.local.json +0 -29
- package/.forge/knowledge/conventions.yaml +0 -1
- package/.forge/knowledge/decisions.yaml +0 -1
- package/.forge/knowledge/gotchas.yaml +0 -1
- package/.forge/knowledge/patterns.yaml +0 -1
- package/.forge/manifest.yaml +0 -6
- package/CLAUDE.md +0 -144
- package/docker-compose.yml +0 -20
- package/docs/plans/2026-02-27-swarm-coordination/architecture.md +0 -203
- package/docs/plans/2026-02-27-swarm-coordination/vision.md +0 -57
- package/docs/plans/completed/2026-02-26-forge-plugin-bundling/architecture.md +0 -1
- package/docs/plans/completed/2026-02-26-forge-plugin-bundling/vision.md +0 -300
- package/docs/plans/completed/2026-02-27-forge-swarm-learning/architecture.md +0 -480
- package/docs/plans/completed/2026-02-27-forge-swarm-learning/verification-checklist.md +0 -462
- package/docs/plans/completed/2026-02-27-git-history-atlassian/git-jira-plan.md +0 -181
- package/src/cli.ts +0 -655
- package/src/context/.gitkeep +0 -0
- package/src/context/codebase.ts +0 -393
- package/src/context/injector.ts +0 -797
- package/src/context/memory.ts +0 -187
- package/src/context/session-index.ts +0 -327
- package/src/context/session.ts +0 -152
- package/src/index.ts +0 -47
- package/src/ingestion/.gitkeep +0 -0
- package/src/ingestion/chunker.ts +0 -277
- package/src/ingestion/embedder.ts +0 -167
- package/src/ingestion/git-analyzer.ts +0 -545
- package/src/ingestion/indexer.ts +0 -984
- package/src/ingestion/markdown-chunker.ts +0 -337
- package/src/ingestion/markdown-knowledge.ts +0 -175
- package/src/ingestion/parser.ts +0 -475
- package/src/ingestion/watcher.ts +0 -182
- package/src/knowledge/.gitkeep +0 -0
- package/src/knowledge/hydrator.ts +0 -246
- package/src/knowledge/registry.ts +0 -463
- package/src/knowledge/search.ts +0 -565
- package/src/knowledge/store.ts +0 -262
- package/src/learning/.gitkeep +0 -0
- package/src/learning/confidence.ts +0 -193
- package/src/learning/patterns.ts +0 -360
- package/src/learning/trajectory.ts +0 -268
- package/src/memory/.gitkeep +0 -0
- package/src/memory/memory-compat.ts +0 -233
- package/src/memory/observation-store.ts +0 -224
- package/src/memory/session-tracker.ts +0 -332
- package/src/pipeline/.gitkeep +0 -0
- package/src/pipeline/engine.ts +0 -1139
- package/src/pipeline/events.ts +0 -253
- package/src/pipeline/parallel.ts +0 -394
- package/src/pipeline/state-machine.ts +0 -199
- package/src/query/.gitkeep +0 -0
- package/src/query/graph-queries.ts +0 -262
- package/src/query/hybrid-search.ts +0 -337
- package/src/query/intent-detector.ts +0 -131
- package/src/query/ranking.ts +0 -161
- package/src/server.ts +0 -352
- package/src/storage/.gitkeep +0 -0
- package/src/storage/falkordb-store.ts +0 -388
- package/src/storage/file-cache.ts +0 -141
- package/src/storage/interfaces.ts +0 -201
- package/src/storage/qdrant-store.ts +0 -557
- package/src/storage/schema.ts +0 -139
- package/src/storage/sqlite.ts +0 -168
- package/src/tools/.gitkeep +0 -0
- package/src/tools/collaboration-tools.ts +0 -208
- package/src/tools/context-tools.ts +0 -493
- package/src/tools/graph-tools.ts +0 -295
- package/src/tools/ingestion-tools.ts +0 -122
- package/src/tools/learning-tools.ts +0 -181
- package/src/tools/memory-tools.ts +0 -234
- package/src/tools/phase-tools.ts +0 -1452
- package/src/tools/pipeline-tools.ts +0 -188
- package/src/tools/registration-tools.ts +0 -450
- package/src/util/.gitkeep +0 -0
- package/src/util/circuit-breaker.ts +0 -193
- package/src/util/config.ts +0 -177
- package/src/util/logger.ts +0 -53
- package/src/util/token-counter.ts +0 -52
- package/src/util/types.ts +0 -710
- package/tests/context/.gitkeep +0 -0
- package/tests/integration/.gitkeep +0 -0
- package/tests/knowledge/.gitkeep +0 -0
- package/tests/learning/.gitkeep +0 -0
- package/tests/pipeline/.gitkeep +0 -0
- package/tests/tools/.gitkeep +0 -0
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -10
- package/vscode-extension/.vscodeignore +0 -7
- package/vscode-extension/README.md +0 -43
- package/vscode-extension/out/edge-collector.js +0 -274
- package/vscode-extension/out/edge-collector.js.map +0 -1
- package/vscode-extension/out/extension.js +0 -264
- package/vscode-extension/out/extension.js.map +0 -1
- package/vscode-extension/out/forge-client.js +0 -318
- package/vscode-extension/out/forge-client.js.map +0 -1
- package/vscode-extension/package-lock.json +0 -59
- package/vscode-extension/package.json +0 -71
- package/vscode-extension/src/edge-collector.ts +0 -320
- package/vscode-extension/src/extension.ts +0 -269
- package/vscode-extension/src/forge-client.ts +0 -364
- package/vscode-extension/tsconfig.json +0 -19
package/src/ingestion/parser.ts
DELETED
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tree-sitter AST parser for TypeScript/TSX/JavaScript/JSX (ADR-3).
|
|
3
|
-
* Extracts entities: functions, classes, interfaces, type aliases, exported variables, imports, exports.
|
|
4
|
-
* Falls back to empty result on parse error (error-tolerant).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { readFile } from 'fs/promises';
|
|
8
|
-
import { extname } from 'path';
|
|
9
|
-
import type { ParseResult, ParsedEntity, ParsedImport } from '../util/types.js';
|
|
10
|
-
import { logger } from '../util/logger.js';
|
|
11
|
-
|
|
12
|
-
// tree-sitter imports - native C addon
|
|
13
|
-
// We use dynamic import with fallback to web-tree-sitter (WASM)
|
|
14
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
-
let Parser: any = null;
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
-
let TypeScriptGrammar: any = null;
|
|
18
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
-
let TSXGrammar: any = null;
|
|
20
|
-
|
|
21
|
-
let parserInitialized = false;
|
|
22
|
-
// Eagerly kick off initialization when the module loads.
|
|
23
|
-
// This allows synchronous callers (parseContent) to find the parser ready
|
|
24
|
-
// by the time test fixtures set up via async beforeAll blocks.
|
|
25
|
-
let _initPromiseEager: Promise<boolean> | null = null;
|
|
26
|
-
|
|
27
|
-
// Start loading immediately (non-blocking, fire-and-forget in module scope)
|
|
28
|
-
// The promise is stored so callers can await it if needed.
|
|
29
|
-
_initPromiseEager = (async () => {
|
|
30
|
-
try {
|
|
31
|
-
const treeSitter = await import('tree-sitter');
|
|
32
|
-
Parser = treeSitter.default;
|
|
33
|
-
const tsGrammar = await import('tree-sitter-typescript');
|
|
34
|
-
TypeScriptGrammar = (tsGrammar.default as { typescript: unknown; tsx: unknown }).typescript;
|
|
35
|
-
TSXGrammar = (tsGrammar.default as { typescript: unknown; tsx: unknown }).tsx;
|
|
36
|
-
parserInitialized = true;
|
|
37
|
-
return true;
|
|
38
|
-
} catch {
|
|
39
|
-
parserInitialized = true;
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
})();
|
|
43
|
-
|
|
44
|
-
export async function initParser(): Promise<boolean> {
|
|
45
|
-
// Reuse the eager init promise if it's still running.
|
|
46
|
-
if (_initPromiseEager) {
|
|
47
|
-
const result = await _initPromiseEager;
|
|
48
|
-
_initPromiseEager = null;
|
|
49
|
-
if (result) {
|
|
50
|
-
logger.info('tree-sitter parser initialized (native)');
|
|
51
|
-
} else {
|
|
52
|
-
logger.warn('tree-sitter native addon failed to load, parser will return empty results', {});
|
|
53
|
-
}
|
|
54
|
-
return result;
|
|
55
|
-
}
|
|
56
|
-
return Parser !== null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Determine language from file extension.
|
|
61
|
-
*/
|
|
62
|
-
export function getLanguage(filePath: string): string | null {
|
|
63
|
-
const ext = extname(filePath).toLowerCase();
|
|
64
|
-
const map: Record<string, string> = {
|
|
65
|
-
'.ts': 'typescript',
|
|
66
|
-
'.tsx': 'tsx',
|
|
67
|
-
'.js': 'javascript',
|
|
68
|
-
'.jsx': 'tsx',
|
|
69
|
-
'.mts': 'typescript',
|
|
70
|
-
'.cts': 'typescript',
|
|
71
|
-
'.mjs': 'javascript',
|
|
72
|
-
'.cjs': 'javascript',
|
|
73
|
-
};
|
|
74
|
-
return map[ext] ?? null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Parse a TypeScript/JavaScript file and extract entities.
|
|
79
|
-
*/
|
|
80
|
-
export async function parseFile(filePath: string, repoId: string): Promise<ParseResult> {
|
|
81
|
-
await initParser();
|
|
82
|
-
|
|
83
|
-
const lang = getLanguage(filePath);
|
|
84
|
-
if (!lang) {
|
|
85
|
-
return {
|
|
86
|
-
entities: [],
|
|
87
|
-
imports: [],
|
|
88
|
-
filePath,
|
|
89
|
-
language: 'unknown',
|
|
90
|
-
success: false,
|
|
91
|
-
error: 'Unsupported file extension',
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let content: string;
|
|
96
|
-
try {
|
|
97
|
-
content = await readFile(filePath, 'utf8');
|
|
98
|
-
} catch (err) {
|
|
99
|
-
return {
|
|
100
|
-
entities: [],
|
|
101
|
-
imports: [],
|
|
102
|
-
filePath,
|
|
103
|
-
language: lang,
|
|
104
|
-
success: false,
|
|
105
|
-
error: `Failed to read file: ${String(err)}`,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return parseContent(content, filePath, repoId, lang);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Parse content string (useful for testing without filesystem).
|
|
114
|
-
*/
|
|
115
|
-
export function parseContent(
|
|
116
|
-
content: string,
|
|
117
|
-
filePath: string,
|
|
118
|
-
repoId: string,
|
|
119
|
-
language: string
|
|
120
|
-
): ParseResult {
|
|
121
|
-
if (!Parser || !TypeScriptGrammar) {
|
|
122
|
-
// Parser not available - return empty but successful result
|
|
123
|
-
return {
|
|
124
|
-
entities: [],
|
|
125
|
-
imports: [],
|
|
126
|
-
filePath,
|
|
127
|
-
language,
|
|
128
|
-
success: false,
|
|
129
|
-
error: 'Parser not initialized (WASM fallback not implemented)',
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
const parser = new Parser();
|
|
135
|
-
|
|
136
|
-
if (language === 'tsx') {
|
|
137
|
-
parser.setLanguage(TSXGrammar);
|
|
138
|
-
} else {
|
|
139
|
-
parser.setLanguage(TypeScriptGrammar);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const tree = parser.parse(content);
|
|
143
|
-
const rootNode = tree.rootNode;
|
|
144
|
-
|
|
145
|
-
const entities: ParsedEntity[] = [];
|
|
146
|
-
const imports: ParsedImport[] = [];
|
|
147
|
-
|
|
148
|
-
// Walk the AST
|
|
149
|
-
walkNode(rootNode, content, filePath, repoId, entities, imports);
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
entities,
|
|
153
|
-
imports,
|
|
154
|
-
filePath,
|
|
155
|
-
language,
|
|
156
|
-
success: true,
|
|
157
|
-
};
|
|
158
|
-
} catch (err) {
|
|
159
|
-
logger.warn('AST parse error', { filePath, error: String(err) });
|
|
160
|
-
return {
|
|
161
|
-
entities: [],
|
|
162
|
-
imports: [],
|
|
163
|
-
filePath,
|
|
164
|
-
language,
|
|
165
|
-
success: false,
|
|
166
|
-
error: String(err),
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
-
function walkNode(
|
|
173
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
174
|
-
node: any,
|
|
175
|
-
content: string,
|
|
176
|
-
filePath: string,
|
|
177
|
-
repoId: string,
|
|
178
|
-
entities: ParsedEntity[],
|
|
179
|
-
imports: ParsedImport[]
|
|
180
|
-
): void {
|
|
181
|
-
const type = node.type as string;
|
|
182
|
-
|
|
183
|
-
// Import declarations — tree-sitter-typescript uses `import_statement`
|
|
184
|
-
if (type === 'import_statement' || type === 'import_declaration') {
|
|
185
|
-
const importNode = extractImport(node, content);
|
|
186
|
-
if (importNode) imports.push(importNode);
|
|
187
|
-
return; // Don't recurse into imports
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Export statements - check what they export
|
|
191
|
-
// tree-sitter-typescript uses `export_statement`
|
|
192
|
-
if (type === 'export_statement') {
|
|
193
|
-
extractExportedEntities(node, content, filePath, repoId, entities);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Function declarations at top level
|
|
198
|
-
if (type === 'function_declaration' || type === 'generator_function_declaration') {
|
|
199
|
-
const entity = extractFunction(node, content, filePath, repoId, false);
|
|
200
|
-
if (entity) entities.push(entity);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Class declarations at top level
|
|
205
|
-
if (type === 'class_declaration') {
|
|
206
|
-
const entity = extractClass(node, content, filePath, repoId, false);
|
|
207
|
-
if (entity) entities.push(entity);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Interface declarations (TypeScript)
|
|
212
|
-
if (type === 'interface_declaration') {
|
|
213
|
-
const entity = extractInterface(node, content, filePath, repoId, false);
|
|
214
|
-
if (entity) entities.push(entity);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Type alias declarations (TypeScript)
|
|
219
|
-
if (type === 'type_alias_declaration') {
|
|
220
|
-
const entity = extractTypeAlias(node, content, filePath, repoId, false);
|
|
221
|
-
if (entity) entities.push(entity);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Lexical declarations (const/let/var) at top level - only exported ones captured in export_statement
|
|
226
|
-
// Variable declarations handled in export context
|
|
227
|
-
|
|
228
|
-
// Recurse into children for top-level nodes only
|
|
229
|
-
// We only care about top-level declarations
|
|
230
|
-
if (
|
|
231
|
-
type === 'program' ||
|
|
232
|
-
type === 'module' ||
|
|
233
|
-
type === 'statement_block'
|
|
234
|
-
) {
|
|
235
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
236
|
-
walkNode(node.child(i), content, filePath, repoId, entities, imports);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
242
|
-
function extractImport(node: any, content: string): ParsedImport | null {
|
|
243
|
-
try {
|
|
244
|
-
// Find the string source (module specifier)
|
|
245
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
246
|
-
let fromPath = '';
|
|
247
|
-
let specifiers = '';
|
|
248
|
-
let isDefault = false;
|
|
249
|
-
|
|
250
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
251
|
-
const child = node.child(i);
|
|
252
|
-
if (child.type === 'string') {
|
|
253
|
-
fromPath = content.slice(child.startIndex + 1, child.endIndex - 1); // strip quotes
|
|
254
|
-
}
|
|
255
|
-
if (child.type === 'import_clause') {
|
|
256
|
-
const clauseText = content.slice(child.startIndex, child.endIndex);
|
|
257
|
-
specifiers = clauseText.trim();
|
|
258
|
-
isDefault = !clauseText.includes('{');
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (!fromPath) return null;
|
|
263
|
-
|
|
264
|
-
return { fromPath, specifiers, isDefault };
|
|
265
|
-
} catch {
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
271
|
-
function extractExportedEntities(
|
|
272
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
273
|
-
node: any,
|
|
274
|
-
content: string,
|
|
275
|
-
filePath: string,
|
|
276
|
-
repoId: string,
|
|
277
|
-
entities: ParsedEntity[]
|
|
278
|
-
): void {
|
|
279
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
280
|
-
let isDefault = false;
|
|
281
|
-
|
|
282
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
283
|
-
const child = node.child(i);
|
|
284
|
-
if (child.type === 'default') {
|
|
285
|
-
isDefault = true;
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (child.type === 'function_declaration' || child.type === 'generator_function_declaration') {
|
|
290
|
-
const entity = extractFunction(child, content, filePath, repoId, true);
|
|
291
|
-
if (entity) {
|
|
292
|
-
entity.isDefault = isDefault;
|
|
293
|
-
entities.push(entity);
|
|
294
|
-
}
|
|
295
|
-
} else if (child.type === 'class_declaration') {
|
|
296
|
-
const entity = extractClass(child, content, filePath, repoId, true);
|
|
297
|
-
if (entity) {
|
|
298
|
-
entity.isDefault = isDefault;
|
|
299
|
-
entities.push(entity);
|
|
300
|
-
}
|
|
301
|
-
} else if (child.type === 'interface_declaration') {
|
|
302
|
-
const entity = extractInterface(child, content, filePath, repoId, true);
|
|
303
|
-
if (entity) entities.push(entity);
|
|
304
|
-
} else if (child.type === 'type_alias_declaration') {
|
|
305
|
-
const entity = extractTypeAlias(child, content, filePath, repoId, true);
|
|
306
|
-
if (entity) entities.push(entity);
|
|
307
|
-
} else if (child.type === 'lexical_declaration' || child.type === 'variable_declaration') {
|
|
308
|
-
// export const foo = ...
|
|
309
|
-
extractVariables(child, content, filePath, repoId, true, entities);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
315
|
-
function extractFunction(node: any, content: string, filePath: string, repoId: string, isExported: boolean): ParsedEntity | null {
|
|
316
|
-
try {
|
|
317
|
-
let name = '';
|
|
318
|
-
let params = '';
|
|
319
|
-
let isAsync = false;
|
|
320
|
-
|
|
321
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
322
|
-
const child = node.child(i);
|
|
323
|
-
if (child.type === 'async') isAsync = true;
|
|
324
|
-
if (child.type === 'identifier') name = content.slice(child.startIndex, child.endIndex);
|
|
325
|
-
if (child.type === 'formal_parameters') {
|
|
326
|
-
params = content.slice(child.startIndex, child.endIndex);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (!name) return null;
|
|
331
|
-
|
|
332
|
-
return {
|
|
333
|
-
name,
|
|
334
|
-
type: 'function',
|
|
335
|
-
filePath,
|
|
336
|
-
repoId,
|
|
337
|
-
startLine: node.startPosition.row + 1,
|
|
338
|
-
endLine: node.endPosition.row + 1,
|
|
339
|
-
isExported,
|
|
340
|
-
isAsync,
|
|
341
|
-
params,
|
|
342
|
-
sourceText: content.slice(node.startIndex, node.endIndex),
|
|
343
|
-
};
|
|
344
|
-
} catch {
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
350
|
-
function extractClass(node: any, content: string, filePath: string, repoId: string, isExported: boolean): ParsedEntity | null {
|
|
351
|
-
try {
|
|
352
|
-
let name = '';
|
|
353
|
-
|
|
354
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
355
|
-
const child = node.child(i);
|
|
356
|
-
if (child.type === 'type_identifier' || child.type === 'identifier') {
|
|
357
|
-
name = content.slice(child.startIndex, child.endIndex);
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (!name) return null;
|
|
363
|
-
|
|
364
|
-
return {
|
|
365
|
-
name,
|
|
366
|
-
type: 'class',
|
|
367
|
-
filePath,
|
|
368
|
-
repoId,
|
|
369
|
-
startLine: node.startPosition.row + 1,
|
|
370
|
-
endLine: node.endPosition.row + 1,
|
|
371
|
-
isExported,
|
|
372
|
-
sourceText: content.slice(node.startIndex, node.endIndex),
|
|
373
|
-
};
|
|
374
|
-
} catch {
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
380
|
-
function extractInterface(node: any, content: string, filePath: string, repoId: string, isExported: boolean): ParsedEntity | null {
|
|
381
|
-
try {
|
|
382
|
-
let name = '';
|
|
383
|
-
|
|
384
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
385
|
-
const child = node.child(i);
|
|
386
|
-
if (child.type === 'type_identifier' || child.type === 'identifier') {
|
|
387
|
-
name = content.slice(child.startIndex, child.endIndex);
|
|
388
|
-
break;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (!name) return null;
|
|
393
|
-
|
|
394
|
-
return {
|
|
395
|
-
name,
|
|
396
|
-
type: 'interface',
|
|
397
|
-
filePath,
|
|
398
|
-
repoId,
|
|
399
|
-
startLine: node.startPosition.row + 1,
|
|
400
|
-
endLine: node.endPosition.row + 1,
|
|
401
|
-
isExported,
|
|
402
|
-
sourceText: content.slice(node.startIndex, node.endIndex),
|
|
403
|
-
};
|
|
404
|
-
} catch {
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
410
|
-
function extractTypeAlias(node: any, content: string, filePath: string, repoId: string, isExported: boolean): ParsedEntity | null {
|
|
411
|
-
try {
|
|
412
|
-
let name = '';
|
|
413
|
-
|
|
414
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
415
|
-
const child = node.child(i);
|
|
416
|
-
if (child.type === 'type_identifier' || child.type === 'identifier') {
|
|
417
|
-
name = content.slice(child.startIndex, child.endIndex);
|
|
418
|
-
break;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (!name) return null;
|
|
423
|
-
|
|
424
|
-
return {
|
|
425
|
-
name,
|
|
426
|
-
type: 'type_alias',
|
|
427
|
-
filePath,
|
|
428
|
-
repoId,
|
|
429
|
-
startLine: node.startPosition.row + 1,
|
|
430
|
-
endLine: node.endPosition.row + 1,
|
|
431
|
-
isExported,
|
|
432
|
-
sourceText: content.slice(node.startIndex, node.endIndex),
|
|
433
|
-
};
|
|
434
|
-
} catch {
|
|
435
|
-
return null;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
440
|
-
function extractVariables(
|
|
441
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
442
|
-
node: any,
|
|
443
|
-
content: string,
|
|
444
|
-
filePath: string,
|
|
445
|
-
repoId: string,
|
|
446
|
-
isExported: boolean,
|
|
447
|
-
entities: ParsedEntity[]
|
|
448
|
-
): void {
|
|
449
|
-
try {
|
|
450
|
-
const isConst = content.slice(node.startIndex, node.startIndex + 5).startsWith('const');
|
|
451
|
-
|
|
452
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
453
|
-
const child = node.child(i);
|
|
454
|
-
if (child.type === 'variable_declarator') {
|
|
455
|
-
const nameNode = child.child(0);
|
|
456
|
-
if (nameNode && (nameNode.type === 'identifier' || nameNode.type === 'type_identifier')) {
|
|
457
|
-
const name = content.slice(nameNode.startIndex, nameNode.endIndex);
|
|
458
|
-
entities.push({
|
|
459
|
-
name,
|
|
460
|
-
type: 'variable',
|
|
461
|
-
filePath,
|
|
462
|
-
repoId,
|
|
463
|
-
startLine: node.startPosition.row + 1,
|
|
464
|
-
endLine: node.endPosition.row + 1,
|
|
465
|
-
isExported,
|
|
466
|
-
isConst,
|
|
467
|
-
sourceText: content.slice(node.startIndex, node.endIndex),
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
} catch {
|
|
473
|
-
// Skip malformed variable declarations
|
|
474
|
-
}
|
|
475
|
-
}
|
package/src/ingestion/watcher.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File system watcher using chokidar (ADR, section 4.5).
|
|
3
|
-
* Watches configured repo directories, debounces rapid saves (500ms),
|
|
4
|
-
* respects .gitignore patterns, and detects git HEAD changes for branch switches.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { EventEmitter } from 'events';
|
|
8
|
-
import { join } from 'path';
|
|
9
|
-
import { readFile } from 'fs/promises';
|
|
10
|
-
import type { RepoConfig } from '../util/types.js';
|
|
11
|
-
import { logger } from '../util/logger.js';
|
|
12
|
-
|
|
13
|
-
// Chokidar dynamic import (ESM compatible)
|
|
14
|
-
type ChokidarWatcher = {
|
|
15
|
-
on(event: string, handler: (path: string) => void): void;
|
|
16
|
-
close(): Promise<void>;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
type WatchEvent = 'add' | 'change' | 'unlink';
|
|
20
|
-
|
|
21
|
-
export interface FileChangeEvent {
|
|
22
|
-
type: WatchEvent;
|
|
23
|
-
filePath: string;
|
|
24
|
-
repoId: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const WATCHED_EXTENSIONS = new Set([
|
|
28
|
-
'.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs',
|
|
29
|
-
'.json', '.yaml', '.yml', '.md', '.mdx', '.graphql', '.gql', '.sql',
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
|
-
const DEFAULT_IGNORED = [
|
|
33
|
-
'**/node_modules/**',
|
|
34
|
-
'**/.git/**',
|
|
35
|
-
'**/dist/**',
|
|
36
|
-
'**/build/**',
|
|
37
|
-
'**/.cache/**',
|
|
38
|
-
'**/*.map',
|
|
39
|
-
'**/*.min.js',
|
|
40
|
-
'**/*.min.css',
|
|
41
|
-
'**/package-lock.json',
|
|
42
|
-
'**/pnpm-lock.yaml',
|
|
43
|
-
'**/.DS_Store',
|
|
44
|
-
'**/Thumbs.db',
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
export class FileWatcher extends EventEmitter {
|
|
48
|
-
private watchers: Map<string, ChokidarWatcher> = new Map();
|
|
49
|
-
private debounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
50
|
-
private gitHeadValues: Map<string, string> = new Map();
|
|
51
|
-
private readonly debounceMs: number;
|
|
52
|
-
|
|
53
|
-
constructor(debounceMs: number = 500) {
|
|
54
|
-
super();
|
|
55
|
-
this.debounceMs = debounceMs;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async watch(repoConfig: RepoConfig, additionalIgnored: string[] = []): Promise<void> {
|
|
59
|
-
const { id: repoId, path: repoPath } = repoConfig;
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const { default: chokidar } = await import('chokidar');
|
|
63
|
-
|
|
64
|
-
const ignored = [...DEFAULT_IGNORED, ...additionalIgnored];
|
|
65
|
-
|
|
66
|
-
const watcher = chokidar.watch(repoPath, {
|
|
67
|
-
ignored,
|
|
68
|
-
persistent: true,
|
|
69
|
-
ignoreInitial: true,
|
|
70
|
-
awaitWriteFinish: {
|
|
71
|
-
stabilityThreshold: 100,
|
|
72
|
-
pollInterval: 50,
|
|
73
|
-
},
|
|
74
|
-
usePolling: false,
|
|
75
|
-
// Windows-specific: use polling as fallback for network drives
|
|
76
|
-
interval: 500,
|
|
77
|
-
binaryInterval: 1000,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Watch git HEAD for branch switches
|
|
81
|
-
const gitHeadPath = join(repoPath, '.git', 'HEAD');
|
|
82
|
-
await this.initGitHead(repoId, gitHeadPath);
|
|
83
|
-
|
|
84
|
-
watcher.on('add', (filePath: string) => this.handleChange('add', filePath, repoId));
|
|
85
|
-
watcher.on('change', (filePath: string) => this.handleChange('change', filePath, repoId));
|
|
86
|
-
watcher.on('unlink', (filePath: string) => this.handleChange('unlink', filePath, repoId));
|
|
87
|
-
watcher.on('error', (error: unknown) => {
|
|
88
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
89
|
-
logger.error('Watcher error', { repoId, error: msg });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// Watch .git/HEAD separately for branch switch detection.
|
|
93
|
-
// The main watcher ignores .git/** so we need a dedicated watcher.
|
|
94
|
-
const gitHeadWatcher = chokidar.watch(gitHeadPath, {
|
|
95
|
-
persistent: true,
|
|
96
|
-
ignoreInitial: true,
|
|
97
|
-
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
98
|
-
usePolling: false,
|
|
99
|
-
interval: 500,
|
|
100
|
-
});
|
|
101
|
-
gitHeadWatcher.on('change', () => this.handleGitHeadChange(repoId, gitHeadPath));
|
|
102
|
-
|
|
103
|
-
this.watchers.set(repoId, watcher);
|
|
104
|
-
logger.info('Watching repo', { repoId, path: repoPath });
|
|
105
|
-
} catch (err) {
|
|
106
|
-
logger.error('Failed to start watcher', { repoId, error: String(err) });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async stop(repoId?: string): Promise<void> {
|
|
111
|
-
if (repoId) {
|
|
112
|
-
const watcher = this.watchers.get(repoId);
|
|
113
|
-
if (watcher) {
|
|
114
|
-
await watcher.close();
|
|
115
|
-
this.watchers.delete(repoId);
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
for (const [id, watcher] of this.watchers) {
|
|
119
|
-
await watcher.close();
|
|
120
|
-
this.watchers.delete(id);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private handleChange(type: WatchEvent, filePath: string, repoId: string): void {
|
|
126
|
-
// Filter by extension
|
|
127
|
-
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
|
|
128
|
-
if (!WATCHED_EXTENSIONS.has(ext) && !filePath.endsWith('HEAD')) return;
|
|
129
|
-
|
|
130
|
-
// Debounce
|
|
131
|
-
const debounceKey = `${repoId}:${filePath}`;
|
|
132
|
-
const existing = this.debounceTimers.get(debounceKey);
|
|
133
|
-
if (existing) clearTimeout(existing);
|
|
134
|
-
|
|
135
|
-
const timer = setTimeout(() => {
|
|
136
|
-
this.debounceTimers.delete(debounceKey);
|
|
137
|
-
this.handleFileChangeDebounced(type, filePath, repoId);
|
|
138
|
-
}, this.debounceMs);
|
|
139
|
-
|
|
140
|
-
this.debounceTimers.set(debounceKey, timer);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
private async handleFileChangeDebounced(
|
|
144
|
-
type: WatchEvent,
|
|
145
|
-
filePath: string,
|
|
146
|
-
repoId: string
|
|
147
|
-
): Promise<void> {
|
|
148
|
-
// Check for git HEAD changes (branch switch detection)
|
|
149
|
-
if (filePath.endsWith('.git/HEAD') || filePath.endsWith('.git\\HEAD')) {
|
|
150
|
-
await this.handleGitHeadChange(repoId, filePath);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const event: FileChangeEvent = { type, filePath, repoId };
|
|
155
|
-
this.emit('file-change', event);
|
|
156
|
-
logger.debug('File change detected', { type, filePath, repoId });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private async initGitHead(repoId: string, headPath: string): Promise<void> {
|
|
160
|
-
try {
|
|
161
|
-
const content = await readFile(headPath, 'utf8');
|
|
162
|
-
this.gitHeadValues.set(repoId, content.trim());
|
|
163
|
-
} catch {
|
|
164
|
-
// .git/HEAD may not exist for non-git repos
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private async handleGitHeadChange(repoId: string, headPath: string): Promise<void> {
|
|
169
|
-
try {
|
|
170
|
-
const newHead = (await readFile(headPath, 'utf8')).trim();
|
|
171
|
-
const oldHead = this.gitHeadValues.get(repoId);
|
|
172
|
-
|
|
173
|
-
if (oldHead !== newHead) {
|
|
174
|
-
this.gitHeadValues.set(repoId, newHead);
|
|
175
|
-
logger.info('Git branch switch detected', { repoId, from: oldHead, to: newHead });
|
|
176
|
-
this.emit('branch-switch', { repoId, from: oldHead, to: newHead });
|
|
177
|
-
}
|
|
178
|
-
} catch {
|
|
179
|
-
// Ignore errors reading git HEAD
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
package/src/knowledge/.gitkeep
DELETED
|
File without changes
|