@graphmemory/server 1.1.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 +66 -101
- package/dist/api/index.js +279 -169
- package/dist/api/rest/index.js +36 -16
- package/dist/api/rest/tools.js +8 -1
- package/dist/api/rest/websocket.js +22 -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/create-relation.js +2 -2
- package/dist/api/tools/knowledge/delete-relation.js +2 -2
- package/dist/api/tools/knowledge/find-linked-notes.js +1 -1
- 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/create-task-link.js +1 -1
- package/dist/api/tools/tasks/delete-task-link.js +1 -1
- package/dist/api/tools/tasks/find-linked-tasks.js +1 -1
- 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 +69 -311
- package/dist/cli/indexer.js +61 -29
- package/dist/graphs/code.js +70 -7
- package/dist/graphs/docs.js +15 -2
- package/dist/graphs/file-index.js +20 -6
- package/dist/graphs/file-lang.js +1 -1
- package/dist/graphs/knowledge.js +20 -3
- package/dist/graphs/manager-types.js +1 -1
- 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/file-mirror.js +7 -7
- package/dist/lib/frontmatter.js +3 -2
- package/dist/lib/jwt.js +4 -4
- package/dist/lib/mirror-watcher.js +5 -4
- package/dist/lib/multi-config.js +60 -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 +10 -4
- package/dist/lib/parsers/languages/typescript.js +195 -48
- 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/lib/watcher.js +17 -9
- 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-D6oxrVF7.js +0 -1759
package/dist/lib/multi-config.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.GRAPH_NAMES = void 0;
|
|
|
7
7
|
exports.embeddingFingerprint = embeddingFingerprint;
|
|
8
8
|
exports.formatAuthor = formatAuthor;
|
|
9
9
|
exports.loadMultiConfig = loadMultiConfig;
|
|
10
|
+
exports.defaultConfig = defaultConfig;
|
|
10
11
|
const fs_1 = __importDefault(require("fs"));
|
|
11
12
|
const os_1 = __importDefault(require("os"));
|
|
12
13
|
const path_1 = __importDefault(require("path"));
|
|
@@ -49,6 +50,7 @@ const userSchema = zod_1.z.object({
|
|
|
49
50
|
});
|
|
50
51
|
const graphConfigSchema = zod_1.z.object({
|
|
51
52
|
enabled: zod_1.z.boolean().optional(),
|
|
53
|
+
readonly: zod_1.z.boolean().optional(),
|
|
52
54
|
include: zod_1.z.string().optional(),
|
|
53
55
|
exclude: excludeSchema,
|
|
54
56
|
model: modelConfigSchema.optional(),
|
|
@@ -97,7 +99,7 @@ const serverSchema = zod_1.z.object({
|
|
|
97
99
|
embeddingApi: embeddingApiSchema.optional(),
|
|
98
100
|
defaultAccess: accessLevelSchema.optional(),
|
|
99
101
|
access: accessMapSchema,
|
|
100
|
-
jwtSecret: zod_1.z.string().optional(),
|
|
102
|
+
jwtSecret: zod_1.z.string().min(16).optional(),
|
|
101
103
|
accessTokenTtl: zod_1.z.string().optional(),
|
|
102
104
|
refreshTokenTtl: zod_1.z.string().optional(),
|
|
103
105
|
rateLimit: rateLimitSchema.optional(),
|
|
@@ -106,6 +108,7 @@ const serverSchema = zod_1.z.object({
|
|
|
106
108
|
});
|
|
107
109
|
const wsGraphConfigSchema = zod_1.z.object({
|
|
108
110
|
enabled: zod_1.z.boolean().optional(),
|
|
111
|
+
readonly: zod_1.z.boolean().optional(),
|
|
109
112
|
exclude: excludeSchema,
|
|
110
113
|
model: modelConfigSchema.optional(),
|
|
111
114
|
embedding: embeddingConfigSchema.optional(),
|
|
@@ -163,6 +166,7 @@ const MODEL_DEFAULTS = {
|
|
|
163
166
|
name: 'Xenova/bge-m3',
|
|
164
167
|
pooling: 'cls',
|
|
165
168
|
normalize: true,
|
|
169
|
+
dtype: 'q8',
|
|
166
170
|
queryPrefix: '',
|
|
167
171
|
documentPrefix: '',
|
|
168
172
|
};
|
|
@@ -299,6 +303,7 @@ function loadMultiConfig(yamlPath) {
|
|
|
299
303
|
const graphExclude = [...projectExclude, ...parseExclude(gc?.exclude)];
|
|
300
304
|
graphConfigs[gn] = {
|
|
301
305
|
enabled: gc?.enabled ?? true,
|
|
306
|
+
readonly: gc?.readonly ?? false,
|
|
302
307
|
include: gc?.include ?? (gn === 'docs' ? PROJECT_DEFAULTS.docsInclude : gn === 'code' ? PROJECT_DEFAULTS.codeInclude : undefined),
|
|
303
308
|
exclude: graphExclude,
|
|
304
309
|
model: resolveModel(gc?.model, projectModel),
|
|
@@ -346,6 +351,7 @@ function loadMultiConfig(yamlPath) {
|
|
|
346
351
|
const gc = rawGraphs[gn];
|
|
347
352
|
graphConfigs[gn] = {
|
|
348
353
|
enabled: gc?.enabled ?? true,
|
|
354
|
+
readonly: gc?.readonly ?? false,
|
|
349
355
|
exclude: [...wsExclude, ...parseExclude(gc?.exclude)],
|
|
350
356
|
model: resolveModel(gc?.model, wsModel),
|
|
351
357
|
embedding: resolveEmbedding(gc?.embedding, wsEmbedding),
|
|
@@ -391,3 +397,56 @@ function loadMultiConfig(yamlPath) {
|
|
|
391
397
|
}
|
|
392
398
|
return { author: globalAuthor, server, users, projects, workspaces };
|
|
393
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Build a default MultiConfig for a single project rooted at `projectDir`.
|
|
402
|
+
* Used when no config file is found — zero-config startup.
|
|
403
|
+
*/
|
|
404
|
+
function defaultConfig(projectDir) {
|
|
405
|
+
const absDir = path_1.default.resolve(projectDir);
|
|
406
|
+
const id = path_1.default.basename(absDir);
|
|
407
|
+
const server = {
|
|
408
|
+
host: SERVER_DEFAULTS.host,
|
|
409
|
+
port: SERVER_DEFAULTS.port,
|
|
410
|
+
sessionTimeout: SERVER_DEFAULTS.sessionTimeout,
|
|
411
|
+
modelsDir: path_1.default.resolve(SERVER_DEFAULTS.modelsDir),
|
|
412
|
+
model: MODEL_DEFAULTS,
|
|
413
|
+
embedding: EMBEDDING_DEFAULTS,
|
|
414
|
+
defaultAccess: SERVER_DEFAULTS.defaultAccess,
|
|
415
|
+
accessTokenTtl: SERVER_DEFAULTS.accessTokenTtl,
|
|
416
|
+
refreshTokenTtl: SERVER_DEFAULTS.refreshTokenTtl,
|
|
417
|
+
rateLimit: RATE_LIMIT_DEFAULTS,
|
|
418
|
+
maxFileSize: SERVER_DEFAULTS.maxFileSize,
|
|
419
|
+
exclude: [...SERVER_DEFAULTS.exclude],
|
|
420
|
+
};
|
|
421
|
+
const graphConfigs = {};
|
|
422
|
+
for (const gn of exports.GRAPH_NAMES) {
|
|
423
|
+
graphConfigs[gn] = {
|
|
424
|
+
enabled: true,
|
|
425
|
+
readonly: false,
|
|
426
|
+
include: gn === 'docs' ? PROJECT_DEFAULTS.docsInclude : gn === 'code' ? PROJECT_DEFAULTS.codeInclude : undefined,
|
|
427
|
+
exclude: [...server.exclude],
|
|
428
|
+
model: MODEL_DEFAULTS,
|
|
429
|
+
embedding: EMBEDDING_DEFAULTS,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const project = {
|
|
433
|
+
projectDir: absDir,
|
|
434
|
+
graphMemory: path_1.default.join(absDir, '.graph-memory'),
|
|
435
|
+
exclude: [...server.exclude],
|
|
436
|
+
chunkDepth: PROJECT_DEFAULTS.chunkDepth,
|
|
437
|
+
maxFileSize: server.maxFileSize,
|
|
438
|
+
model: MODEL_DEFAULTS,
|
|
439
|
+
embedding: EMBEDDING_DEFAULTS,
|
|
440
|
+
graphConfigs,
|
|
441
|
+
author: AUTHOR_DEFAULT,
|
|
442
|
+
};
|
|
443
|
+
const projects = new Map();
|
|
444
|
+
projects.set(id, project);
|
|
445
|
+
return {
|
|
446
|
+
author: AUTHOR_DEFAULT,
|
|
447
|
+
server,
|
|
448
|
+
users: {},
|
|
449
|
+
projects,
|
|
450
|
+
workspaces: new Map(),
|
|
451
|
+
};
|
|
452
|
+
}
|
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
|
}
|
|
@@ -46,17 +46,23 @@ async function loadLanguage(entry) {
|
|
|
46
46
|
function isLanguageSupported(languageName) {
|
|
47
47
|
return languages.has(languageName);
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
49
|
+
/** Reusable parser per language (avoids WASM memory leak from creating Parser on every call). */
|
|
50
|
+
const parsers = new Map();
|
|
51
|
+
/** Parse source code with the appropriate language grammar. Returns tree (caller must call tree.delete() when done) or null. */
|
|
50
52
|
async function parseSource(code, languageName) {
|
|
51
53
|
const entry = languages.get(languageName);
|
|
52
54
|
if (!entry)
|
|
53
55
|
return null;
|
|
54
56
|
await initParser();
|
|
55
57
|
const lang = await loadLanguage(entry);
|
|
56
|
-
|
|
57
|
-
parser
|
|
58
|
+
let parser = parsers.get(languageName);
|
|
59
|
+
if (!parser) {
|
|
60
|
+
parser = new _wts.Parser();
|
|
61
|
+
parser.setLanguage(lang);
|
|
62
|
+
parsers.set(languageName, parser);
|
|
63
|
+
}
|
|
58
64
|
const tree = parser.parse(code);
|
|
59
|
-
return tree
|
|
65
|
+
return tree ?? null;
|
|
60
66
|
}
|
|
61
67
|
/** Get the mapper for a language. Returns undefined for unsupported languages. */
|
|
62
68
|
function getMapper(languageName) {
|