@graphmemory/server 1.2.0 → 1.3.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/LICENSE +84 -12
- package/README.md +7 -1
- package/dist/api/index.js +147 -50
- package/dist/api/rest/index.js +35 -15
- package/dist/api/rest/tools.js +8 -1
- package/dist/api/tools/code/search-code.js +12 -9
- package/dist/api/tools/code/search-files.js +1 -1
- package/dist/api/tools/docs/cross-references.js +3 -2
- package/dist/api/tools/docs/explain-symbol.js +2 -1
- package/dist/api/tools/docs/find-examples.js +2 -1
- package/dist/api/tools/docs/search-files.js +1 -1
- package/dist/api/tools/docs/search-snippets.js +1 -1
- package/dist/api/tools/docs/search.js +5 -4
- package/dist/api/tools/file-index/search-all-files.js +1 -1
- package/dist/api/tools/knowledge/add-attachment.js +14 -3
- package/dist/api/tools/knowledge/remove-attachment.js +5 -1
- package/dist/api/tools/knowledge/search-notes.js +5 -4
- package/dist/api/tools/skills/add-attachment.js +14 -3
- package/dist/api/tools/skills/recall-skills.js +1 -1
- package/dist/api/tools/skills/remove-attachment.js +5 -1
- package/dist/api/tools/skills/search-skills.js +6 -5
- package/dist/api/tools/tasks/add-attachment.js +14 -3
- package/dist/api/tools/tasks/remove-attachment.js +5 -1
- package/dist/api/tools/tasks/search-tasks.js +5 -4
- package/dist/cli/index.js +61 -51
- package/dist/cli/indexer.js +60 -28
- package/dist/graphs/code.js +70 -7
- package/dist/graphs/docs.js +15 -2
- package/dist/graphs/file-index.js +17 -3
- package/dist/graphs/file-lang.js +1 -1
- package/dist/graphs/knowledge.js +20 -3
- package/dist/graphs/skill.js +23 -4
- package/dist/graphs/task.js +23 -4
- package/dist/lib/embedding-codec.js +65 -0
- package/dist/lib/jwt.js +4 -4
- package/dist/lib/multi-config.js +6 -1
- package/dist/lib/parsers/code.js +158 -31
- package/dist/lib/parsers/codeblock.js +11 -6
- package/dist/lib/parsers/docs.js +59 -31
- package/dist/lib/parsers/languages/registry.js +2 -2
- package/dist/lib/parsers/languages/typescript.js +195 -44
- package/dist/lib/project-manager.js +14 -10
- package/dist/lib/search/bm25.js +18 -1
- package/dist/lib/search/code.js +12 -3
- package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
- package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
- package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
- package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
- package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
- package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
- package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
- package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
- package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
- package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
- package/dist/ui/assets/api-BMnBjMMf.js +1 -0
- package/dist/ui/assets/api-BlFF6gX-.js +1 -0
- package/dist/ui/assets/api-CrGJOcaN.js +1 -0
- package/dist/ui/assets/api-DuX-0a_X.js +1 -0
- package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
- package/dist/ui/assets/client-Bq88u7gN.js +1 -0
- package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
- package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
- package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
- package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
- package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
- package/dist/ui/assets/files-0bPg6NH9.js +1 -0
- package/dist/ui/assets/graph-DXGud_wF.js +1 -0
- package/dist/ui/assets/help-CEMQqZUR.js +891 -0
- package/dist/ui/assets/help-DJ52_fxN.js +1 -0
- package/dist/ui/assets/index-BCZDAYZi.js +2 -0
- package/dist/ui/assets/index-D6zSNtzo.css +1 -0
- package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
- package/dist/ui/assets/new-CpD7hOBA.js +1 -0
- package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
- package/dist/ui/assets/new-s8c0M75X.js +1 -0
- package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
- package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
- package/dist/ui/assets/search-EpJhdP2a.js +1 -0
- package/dist/ui/assets/skill-y9pizyqE.js +1 -0
- package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
- package/dist/ui/assets/tasks-CobouTKV.js +1 -0
- package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
- package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
- package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
- package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
- package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
- package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
- package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
- package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
- package/dist/ui/index.html +11 -3
- package/package.json +2 -2
- package/dist/ui/assets/index-0hRezICt.js +0 -1702
package/dist/lib/parsers/code.js
CHANGED
|
@@ -3,43 +3,157 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.clearPathMappingsCache = clearPathMappingsCache;
|
|
6
7
|
exports.parseCodeFile = parseCodeFile;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const languages_1 = require("../../lib/parsers/languages");
|
|
10
11
|
const file_lang_1 = require("../../graphs/file-lang");
|
|
12
|
+
// Strip line and block comments from JSONC, preserving string contents.
|
|
13
|
+
function stripJsoncComments(text) {
|
|
14
|
+
let result = '';
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < text.length) {
|
|
17
|
+
// String literal — copy verbatim
|
|
18
|
+
if (text[i] === '"') {
|
|
19
|
+
const start = i++;
|
|
20
|
+
while (i < text.length && text[i] !== '"') {
|
|
21
|
+
if (text[i] === '\\')
|
|
22
|
+
i++; // skip escaped char
|
|
23
|
+
i++;
|
|
24
|
+
}
|
|
25
|
+
i++; // closing quote
|
|
26
|
+
result += text.slice(start, i);
|
|
27
|
+
// Line comment
|
|
28
|
+
}
|
|
29
|
+
else if (text[i] === '/' && text[i + 1] === '/') {
|
|
30
|
+
while (i < text.length && text[i] !== '\n')
|
|
31
|
+
i++;
|
|
32
|
+
// Block comment
|
|
33
|
+
}
|
|
34
|
+
else if (text[i] === '/' && text[i + 1] === '*') {
|
|
35
|
+
i += 2;
|
|
36
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
|
|
37
|
+
i++;
|
|
38
|
+
i += 2;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
result += text[i++];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
11
46
|
// ---------------------------------------------------------------------------
|
|
12
47
|
// Import resolution — replaces ts-morph's getModuleSpecifierSourceFile()
|
|
13
48
|
// ---------------------------------------------------------------------------
|
|
14
49
|
const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs'];
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
50
|
+
function hasFile(p) {
|
|
51
|
+
try {
|
|
52
|
+
return fs_1.default.statSync(p).isFile();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Try resolving a base path with extensions and index files. */
|
|
59
|
+
function tryResolve(base) {
|
|
20
60
|
if (hasFile(base))
|
|
21
61
|
return base;
|
|
22
|
-
// Try adding extensions (e.g. './foo' → './foo.ts')
|
|
23
62
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return candidate;
|
|
63
|
+
if (hasFile(base + ext))
|
|
64
|
+
return base + ext;
|
|
27
65
|
}
|
|
28
|
-
// Try index files (e.g. './foo' → './foo/index.ts')
|
|
29
66
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
30
|
-
const
|
|
31
|
-
if (hasFile(
|
|
32
|
-
return
|
|
67
|
+
const idx = path_1.default.join(base, 'index' + ext);
|
|
68
|
+
if (hasFile(idx))
|
|
69
|
+
return idx;
|
|
33
70
|
}
|
|
34
71
|
return null;
|
|
35
72
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
73
|
+
/** Resolve a relative import specifier to an absolute file path, or null. */
|
|
74
|
+
function resolveRelativeImport(fromFile, specifier) {
|
|
75
|
+
const dir = path_1.default.dirname(fromFile);
|
|
76
|
+
return tryResolve(path_1.default.resolve(dir, specifier));
|
|
77
|
+
}
|
|
78
|
+
/** Cache: directory → parsed path mappings (null = no tsconfig found up to root). */
|
|
79
|
+
const _pathMappings = new Map();
|
|
80
|
+
/** Clear cached path mappings (call between projects or on config change). */
|
|
81
|
+
function clearPathMappingsCache() { _pathMappings.clear(); }
|
|
82
|
+
/**
|
|
83
|
+
* Find the nearest tsconfig.json / jsconfig.json walking up from `dir` to `root`.
|
|
84
|
+
* Cached per directory — each directory remembers its resolved mappings.
|
|
85
|
+
*/
|
|
86
|
+
function findPathMappings(dir, root) {
|
|
87
|
+
if (_pathMappings.has(dir))
|
|
88
|
+
return _pathMappings.get(dir);
|
|
89
|
+
// Try this directory
|
|
90
|
+
const result = _parseTsConfig(dir);
|
|
91
|
+
if (result) {
|
|
92
|
+
_pathMappings.set(dir, result);
|
|
93
|
+
return result;
|
|
39
94
|
}
|
|
40
|
-
|
|
41
|
-
|
|
95
|
+
// Walk up unless we've reached the project root
|
|
96
|
+
const parent = path_1.default.dirname(dir);
|
|
97
|
+
if (dir === root || parent === dir) {
|
|
98
|
+
_pathMappings.set(dir, null);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const parentResult = findPathMappings(parent, root);
|
|
102
|
+
_pathMappings.set(dir, parentResult);
|
|
103
|
+
return parentResult;
|
|
104
|
+
}
|
|
105
|
+
function _parseTsConfig(dir) {
|
|
106
|
+
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
|
107
|
+
const configPath = path_1.default.join(dir, name);
|
|
108
|
+
if (!hasFile(configPath))
|
|
109
|
+
continue;
|
|
110
|
+
try {
|
|
111
|
+
// Strip JSONC comments while preserving string contents
|
|
112
|
+
const raw = stripJsoncComments(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
113
|
+
const config = JSON.parse(raw);
|
|
114
|
+
const compilerOptions = config.compilerOptions;
|
|
115
|
+
if (!compilerOptions?.paths)
|
|
116
|
+
continue;
|
|
117
|
+
const baseUrl = compilerOptions.baseUrl
|
|
118
|
+
? path_1.default.resolve(dir, compilerOptions.baseUrl)
|
|
119
|
+
: dir;
|
|
120
|
+
const mappings = [];
|
|
121
|
+
for (const [pattern, targets] of Object.entries(compilerOptions.paths)) {
|
|
122
|
+
// Pattern like "@/*" → prefix "@/", or "utils/*" → prefix "utils/"
|
|
123
|
+
const prefix = pattern.endsWith('/*') ? pattern.slice(0, -1) : pattern;
|
|
124
|
+
const resolvedTargets = targets
|
|
125
|
+
.map(t => {
|
|
126
|
+
const target = t.endsWith('/*') ? t.slice(0, -1) : t;
|
|
127
|
+
return path_1.default.resolve(baseUrl, target);
|
|
128
|
+
});
|
|
129
|
+
mappings.push({ prefix, targets: resolvedTargets });
|
|
130
|
+
}
|
|
131
|
+
if (mappings.length > 0)
|
|
132
|
+
return mappings;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Malformed config — skip
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
/** Resolve a path-aliased import (e.g. @/lib/foo) using nearest tsconfig paths. */
|
|
141
|
+
function resolveAliasImport(specifier, fromFile, projectDir) {
|
|
142
|
+
const fileDir = path_1.default.dirname(fromFile);
|
|
143
|
+
const mappings = findPathMappings(fileDir, projectDir);
|
|
144
|
+
if (!mappings)
|
|
145
|
+
return null;
|
|
146
|
+
for (const mapping of mappings) {
|
|
147
|
+
if (specifier.startsWith(mapping.prefix)) {
|
|
148
|
+
const rest = specifier.slice(mapping.prefix.length);
|
|
149
|
+
for (const targetDir of mapping.targets) {
|
|
150
|
+
const resolved = tryResolve(path_1.default.join(targetDir, rest));
|
|
151
|
+
if (resolved)
|
|
152
|
+
return resolved;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
42
155
|
}
|
|
156
|
+
return null;
|
|
43
157
|
}
|
|
44
158
|
// ---------------------------------------------------------------------------
|
|
45
159
|
// Main parser
|
|
@@ -62,8 +176,8 @@ async function parseCodeFile(absolutePath, codeDir, mtime) {
|
|
|
62
176
|
};
|
|
63
177
|
}
|
|
64
178
|
const source = fs_1.default.readFileSync(absolutePath, 'utf-8');
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
179
|
+
const tree = await (0, languages_1.parseSource)(source, language);
|
|
180
|
+
if (!tree) {
|
|
67
181
|
return {
|
|
68
182
|
fileId,
|
|
69
183
|
mtime,
|
|
@@ -74,17 +188,23 @@ async function parseCodeFile(absolutePath, codeDir, mtime) {
|
|
|
74
188
|
edges: [],
|
|
75
189
|
};
|
|
76
190
|
}
|
|
191
|
+
const rootNode = tree.rootNode;
|
|
77
192
|
const mapper = (0, languages_1.getMapper)(language);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
193
|
+
let symbols, edgeInfos, imports, fileDocComment, importSummary, lastLine;
|
|
194
|
+
try {
|
|
195
|
+
symbols = mapper.extractSymbols(rootNode);
|
|
196
|
+
edgeInfos = mapper.extractEdges(rootNode);
|
|
197
|
+
imports = mapper.extractImports(rootNode);
|
|
198
|
+
fileDocComment = extractFileDocComment(rootNode);
|
|
199
|
+
importSummary = buildImportSummary(rootNode);
|
|
200
|
+
lastLine = (rootNode.endPosition?.row ?? 0) + 1;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
tree.delete();
|
|
204
|
+
}
|
|
81
205
|
const nodes = [];
|
|
82
206
|
const edges = [];
|
|
83
207
|
const fileNodeId = fileId;
|
|
84
|
-
// --- File root node ---
|
|
85
|
-
const fileDocComment = extractFileDocComment(rootNode);
|
|
86
|
-
const importSummary = buildImportSummary(rootNode);
|
|
87
|
-
const lastLine = (rootNode.endPosition?.row ?? 0) + 1;
|
|
88
208
|
nodes.push({
|
|
89
209
|
id: fileNodeId,
|
|
90
210
|
attrs: makeFileAttrs(fileId, fileDocComment, importSummary, lastLine, mtime),
|
|
@@ -147,13 +267,20 @@ async function parseCodeFile(absolutePath, codeDir, mtime) {
|
|
|
147
267
|
}
|
|
148
268
|
// --- Import edges: file → imported file ---
|
|
149
269
|
for (const imp of imports) {
|
|
150
|
-
|
|
270
|
+
let targetAbsolute = null;
|
|
271
|
+
if (imp.specifier.startsWith('./') || imp.specifier.startsWith('../')) {
|
|
272
|
+
// Relative import
|
|
273
|
+
targetAbsolute = resolveRelativeImport(absolutePath, imp.specifier);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
// Try path alias resolution (e.g. @/lib/foo, ~/utils)
|
|
277
|
+
targetAbsolute = resolveAliasImport(imp.specifier, absolutePath, codeDir);
|
|
278
|
+
}
|
|
151
279
|
if (!targetAbsolute)
|
|
152
280
|
continue;
|
|
153
|
-
const targetRel = path_1.default.relative(codeDir, targetAbsolute);
|
|
154
|
-
if (targetRel.startsWith('..') || path_1.default.isAbsolute(targetRel))
|
|
155
|
-
continue;
|
|
156
281
|
const targetFileId = path_1.default.relative(codeDir, targetAbsolute);
|
|
282
|
+
if (targetFileId.startsWith('..') || path_1.default.isAbsolute(targetFileId))
|
|
283
|
+
continue;
|
|
157
284
|
if (targetFileId !== fileNodeId) {
|
|
158
285
|
edges.push({
|
|
159
286
|
from: fileNodeId,
|
|
@@ -6,7 +6,7 @@ const languages_1 = require("../../lib/parsers/languages");
|
|
|
6
6
|
const TAG_TO_LANGUAGE = {
|
|
7
7
|
ts: 'typescript',
|
|
8
8
|
typescript: 'typescript',
|
|
9
|
-
tsx: '
|
|
9
|
+
tsx: 'tsx',
|
|
10
10
|
js: 'javascript',
|
|
11
11
|
javascript: 'javascript',
|
|
12
12
|
jsx: 'javascript',
|
|
@@ -20,12 +20,17 @@ async function extractSymbols(code, language) {
|
|
|
20
20
|
if (!lang || !(0, languages_1.isLanguageSupported)(lang))
|
|
21
21
|
return [];
|
|
22
22
|
try {
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
23
|
+
const tree = await (0, languages_1.parseSource)(code, lang);
|
|
24
|
+
if (!tree)
|
|
25
25
|
return [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
try {
|
|
27
|
+
const mapper = (0, languages_1.getMapper)(lang);
|
|
28
|
+
const symbols = mapper.extractSymbols(tree.rootNode);
|
|
29
|
+
return symbols.map(s => s.name).filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
tree.delete();
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
catch {
|
|
31
36
|
return [];
|
package/dist/lib/parsers/docs.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.parseFile = parseFile;
|
|
7
|
+
exports.clearWikiIndexCache = clearWikiIndexCache;
|
|
7
8
|
const path_1 = __importDefault(require("path"));
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
9
10
|
const codeblock_1 = require("../../lib/parsers/codeblock");
|
|
@@ -119,8 +120,10 @@ function extractFileTitle(content, filePath) {
|
|
|
119
120
|
function extractLinks(content, fromFile, projectDir) {
|
|
120
121
|
const results = new Set();
|
|
121
122
|
const fileDir = path_1.default.dirname(fromFile);
|
|
123
|
+
// Strip fenced code blocks before extracting links (links inside code are not real references)
|
|
124
|
+
const textOnly = content.replace(FENCE_RE, '');
|
|
122
125
|
// [text](./path.md)
|
|
123
|
-
const mdLinks =
|
|
126
|
+
const mdLinks = textOnly.matchAll(/\[[^\]]*\]\(([^)#\s]+)/g);
|
|
124
127
|
for (const [, href] of mdLinks) {
|
|
125
128
|
if (isExternal(href))
|
|
126
129
|
continue;
|
|
@@ -130,7 +133,7 @@ function extractLinks(content, fromFile, projectDir) {
|
|
|
130
133
|
results.add(fileId);
|
|
131
134
|
}
|
|
132
135
|
// [[wiki link]] or [[wiki link|alias]]
|
|
133
|
-
const wikiLinks =
|
|
136
|
+
const wikiLinks = textOnly.matchAll(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g);
|
|
134
137
|
for (const [, name] of wikiLinks) {
|
|
135
138
|
const resolved = findWikiFile(name.trim(), projectDir);
|
|
136
139
|
if (!resolved)
|
|
@@ -160,40 +163,65 @@ function toFileId(absolutePath, projectDir) {
|
|
|
160
163
|
return path_1.default.relative(projectDir, withMd);
|
|
161
164
|
return null;
|
|
162
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Cache: projectDir → Map<lowercased basename (without .md), absolute path>.
|
|
168
|
+
* Built once per project via a single recursive walk, then reused for all wiki link lookups.
|
|
169
|
+
*/
|
|
170
|
+
const _wikiIndex = new Map();
|
|
171
|
+
/** Clear cached wiki link index (call when files change or between projects). */
|
|
172
|
+
function clearWikiIndexCache(projectDir) {
|
|
173
|
+
if (projectDir)
|
|
174
|
+
_wikiIndex.delete(projectDir);
|
|
175
|
+
else
|
|
176
|
+
_wikiIndex.clear();
|
|
177
|
+
}
|
|
178
|
+
function getWikiIndex(projectDir) {
|
|
179
|
+
if (_wikiIndex.has(projectDir))
|
|
180
|
+
return _wikiIndex.get(projectDir);
|
|
181
|
+
const index = new Map();
|
|
182
|
+
const MAX_DEPTH = 10;
|
|
183
|
+
function walk(dir, depth) {
|
|
184
|
+
if (depth >= MAX_DEPTH)
|
|
185
|
+
return;
|
|
186
|
+
let entries;
|
|
187
|
+
try {
|
|
188
|
+
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
if (entry.name.startsWith('.') || entry.isSymbolicLink())
|
|
195
|
+
continue;
|
|
196
|
+
const full = path_1.default.join(dir, entry.name);
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
walk(full, depth + 1);
|
|
199
|
+
}
|
|
200
|
+
else if (entry.isFile()) {
|
|
201
|
+
// Index by basename without extension (lowercased)
|
|
202
|
+
const key = path_1.default.basename(entry.name, path_1.default.extname(entry.name)).toLowerCase();
|
|
203
|
+
if (!index.has(key))
|
|
204
|
+
index.set(key, full);
|
|
205
|
+
// Also index by full filename (lowercased)
|
|
206
|
+
const fullKey = entry.name.toLowerCase();
|
|
207
|
+
if (!index.has(fullKey))
|
|
208
|
+
index.set(fullKey, full);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
walk(projectDir, 0);
|
|
213
|
+
_wikiIndex.set(projectDir, index);
|
|
214
|
+
return index;
|
|
215
|
+
}
|
|
163
216
|
function findWikiFile(name, projectDir) {
|
|
217
|
+
// Direct path check first (fast path)
|
|
164
218
|
const direct = path_1.default.join(projectDir, name);
|
|
165
219
|
if (fs_1.default.existsSync(direct))
|
|
166
220
|
return direct;
|
|
167
221
|
const withMd = direct + '.md';
|
|
168
222
|
if (fs_1.default.existsSync(withMd))
|
|
169
223
|
return withMd;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
function searchRecursive(name, dir, depth = 0) {
|
|
174
|
-
if (depth >= MAX_SEARCH_DEPTH)
|
|
175
|
-
return null;
|
|
176
|
-
let entries;
|
|
177
|
-
try {
|
|
178
|
-
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
for (const entry of entries) {
|
|
184
|
-
if (entry.name.startsWith('.'))
|
|
185
|
-
continue;
|
|
186
|
-
const full = path_1.default.join(dir, entry.name);
|
|
187
|
-
if (entry.isDirectory()) {
|
|
188
|
-
const found = searchRecursive(name, full, depth + 1);
|
|
189
|
-
if (found)
|
|
190
|
-
return found;
|
|
191
|
-
}
|
|
192
|
-
else if (entry.isFile()) {
|
|
193
|
-
if (entry.name === name || entry.name === `${name}.md` || path_1.default.basename(entry.name, '.md') === name) {
|
|
194
|
-
return full;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return null;
|
|
224
|
+
// Fall back to cached index lookup
|
|
225
|
+
const index = getWikiIndex(projectDir);
|
|
226
|
+
return index.get(name.toLowerCase()) ?? index.get(name.toLowerCase() + '.md') ?? null;
|
|
199
227
|
}
|
|
@@ -48,7 +48,7 @@ function isLanguageSupported(languageName) {
|
|
|
48
48
|
}
|
|
49
49
|
/** Reusable parser per language (avoids WASM memory leak from creating Parser on every call). */
|
|
50
50
|
const parsers = new Map();
|
|
51
|
-
/** Parse source code with the appropriate language grammar. Returns
|
|
51
|
+
/** Parse source code with the appropriate language grammar. Returns tree (caller must call tree.delete() when done) or null. */
|
|
52
52
|
async function parseSource(code, languageName) {
|
|
53
53
|
const entry = languages.get(languageName);
|
|
54
54
|
if (!entry)
|
|
@@ -62,7 +62,7 @@ async function parseSource(code, languageName) {
|
|
|
62
62
|
parsers.set(languageName, parser);
|
|
63
63
|
}
|
|
64
64
|
const tree = parser.parse(code);
|
|
65
|
-
return tree
|
|
65
|
+
return tree ?? null;
|
|
66
66
|
}
|
|
67
67
|
/** Get the mapper for a language. Returns undefined for unsupported languages. */
|
|
68
68
|
function getMapper(languageName) {
|