@prih/mcp-graph-memory 1.0.3
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 +15 -0
- package/README.md +512 -0
- package/dist/api/index.js +473 -0
- package/dist/api/rest/code.js +78 -0
- package/dist/api/rest/docs.js +80 -0
- package/dist/api/rest/files.js +64 -0
- package/dist/api/rest/graph.js +56 -0
- package/dist/api/rest/index.js +117 -0
- package/dist/api/rest/knowledge.js +238 -0
- package/dist/api/rest/skills.js +284 -0
- package/dist/api/rest/tasks.js +272 -0
- package/dist/api/rest/tools.js +126 -0
- package/dist/api/rest/validation.js +191 -0
- package/dist/api/rest/websocket.js +65 -0
- package/dist/api/tools/code/get-file-symbols.js +30 -0
- package/dist/api/tools/code/get-symbol.js +22 -0
- package/dist/api/tools/code/list-files.js +18 -0
- package/dist/api/tools/code/search-code.js +27 -0
- package/dist/api/tools/code/search-files.js +22 -0
- package/dist/api/tools/context/get-context.js +19 -0
- package/dist/api/tools/docs/cross-references.js +76 -0
- package/dist/api/tools/docs/explain-symbol.js +55 -0
- package/dist/api/tools/docs/find-examples.js +52 -0
- package/dist/api/tools/docs/get-node.js +24 -0
- package/dist/api/tools/docs/get-toc.js +22 -0
- package/dist/api/tools/docs/list-snippets.js +46 -0
- package/dist/api/tools/docs/list-topics.js +18 -0
- package/dist/api/tools/docs/search-files.js +22 -0
- package/dist/api/tools/docs/search-snippets.js +43 -0
- package/dist/api/tools/docs/search.js +27 -0
- package/dist/api/tools/file-index/get-file-info.js +21 -0
- package/dist/api/tools/file-index/list-all-files.js +28 -0
- package/dist/api/tools/file-index/search-all-files.js +24 -0
- package/dist/api/tools/knowledge/add-attachment.js +31 -0
- package/dist/api/tools/knowledge/create-note.js +20 -0
- package/dist/api/tools/knowledge/create-relation.js +29 -0
- package/dist/api/tools/knowledge/delete-note.js +19 -0
- package/dist/api/tools/knowledge/delete-relation.js +23 -0
- package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
- package/dist/api/tools/knowledge/get-note.js +20 -0
- package/dist/api/tools/knowledge/list-notes.js +18 -0
- package/dist/api/tools/knowledge/list-relations.js +17 -0
- package/dist/api/tools/knowledge/remove-attachment.js +19 -0
- package/dist/api/tools/knowledge/search-notes.js +25 -0
- package/dist/api/tools/knowledge/update-note.js +34 -0
- package/dist/api/tools/skills/add-attachment.js +31 -0
- package/dist/api/tools/skills/bump-usage.js +19 -0
- package/dist/api/tools/skills/create-skill-link.js +25 -0
- package/dist/api/tools/skills/create-skill.js +26 -0
- package/dist/api/tools/skills/delete-skill-link.js +23 -0
- package/dist/api/tools/skills/delete-skill.js +20 -0
- package/dist/api/tools/skills/find-linked-skills.js +25 -0
- package/dist/api/tools/skills/get-skill.js +21 -0
- package/dist/api/tools/skills/link-skill.js +23 -0
- package/dist/api/tools/skills/list-skills.js +20 -0
- package/dist/api/tools/skills/recall-skills.js +18 -0
- package/dist/api/tools/skills/remove-attachment.js +19 -0
- package/dist/api/tools/skills/search-skills.js +25 -0
- package/dist/api/tools/skills/update-skill.js +58 -0
- package/dist/api/tools/tasks/add-attachment.js +31 -0
- package/dist/api/tools/tasks/create-task-link.js +25 -0
- package/dist/api/tools/tasks/create-task.js +25 -0
- package/dist/api/tools/tasks/delete-task-link.js +23 -0
- package/dist/api/tools/tasks/delete-task.js +20 -0
- package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
- package/dist/api/tools/tasks/get-task.js +20 -0
- package/dist/api/tools/tasks/link-task.js +23 -0
- package/dist/api/tools/tasks/list-tasks.js +24 -0
- package/dist/api/tools/tasks/move-task.js +38 -0
- package/dist/api/tools/tasks/remove-attachment.js +19 -0
- package/dist/api/tools/tasks/search-tasks.js +25 -0
- package/dist/api/tools/tasks/update-task.js +55 -0
- package/dist/cli/index.js +451 -0
- package/dist/cli/indexer.js +277 -0
- package/dist/graphs/attachment-types.js +74 -0
- package/dist/graphs/code-types.js +10 -0
- package/dist/graphs/code.js +172 -0
- package/dist/graphs/docs.js +198 -0
- package/dist/graphs/file-index-types.js +10 -0
- package/dist/graphs/file-index.js +310 -0
- package/dist/graphs/file-lang.js +119 -0
- package/dist/graphs/knowledge-types.js +32 -0
- package/dist/graphs/knowledge.js +764 -0
- package/dist/graphs/manager-types.js +87 -0
- package/dist/graphs/skill-types.js +10 -0
- package/dist/graphs/skill.js +1013 -0
- package/dist/graphs/task-types.js +17 -0
- package/dist/graphs/task.js +960 -0
- package/dist/lib/embedder.js +101 -0
- package/dist/lib/events-log.js +400 -0
- package/dist/lib/file-import.js +327 -0
- package/dist/lib/file-mirror.js +446 -0
- package/dist/lib/frontmatter.js +17 -0
- package/dist/lib/mirror-watcher.js +637 -0
- package/dist/lib/multi-config.js +254 -0
- package/dist/lib/parsers/code.js +246 -0
- package/dist/lib/parsers/codeblock.js +66 -0
- package/dist/lib/parsers/docs.js +196 -0
- package/dist/lib/project-manager.js +418 -0
- package/dist/lib/promise-queue.js +22 -0
- package/dist/lib/search/bm25.js +167 -0
- package/dist/lib/search/code.js +103 -0
- package/dist/lib/search/docs.js +108 -0
- package/dist/lib/search/file-index.js +31 -0
- package/dist/lib/search/files.js +61 -0
- package/dist/lib/search/knowledge.js +101 -0
- package/dist/lib/search/skills.js +104 -0
- package/dist/lib/search/tasks.js +103 -0
- package/dist/lib/watcher.js +67 -0
- package/package.json +83 -0
- package/ui/README.md +54 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GRAPH_NAMES = void 0;
|
|
7
|
+
exports.embeddingFingerprint = embeddingFingerprint;
|
|
8
|
+
exports.formatAuthor = formatAuthor;
|
|
9
|
+
exports.loadMultiConfig = loadMultiConfig;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const yaml_1 = require("yaml");
|
|
14
|
+
const zod_1 = require("zod");
|
|
15
|
+
const HOME = os_1.default.homedir();
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Zod schemas
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const authorSchema = zod_1.z.object({
|
|
20
|
+
name: zod_1.z.string(),
|
|
21
|
+
email: zod_1.z.string(),
|
|
22
|
+
});
|
|
23
|
+
const embeddingConfigSchema = zod_1.z.object({
|
|
24
|
+
model: zod_1.z.string(),
|
|
25
|
+
pooling: zod_1.z.enum(['mean', 'cls']).optional(),
|
|
26
|
+
normalize: zod_1.z.boolean().optional(),
|
|
27
|
+
dtype: zod_1.z.string().optional(),
|
|
28
|
+
queryPrefix: zod_1.z.string().optional(),
|
|
29
|
+
documentPrefix: zod_1.z.string().optional(),
|
|
30
|
+
batchSize: zod_1.z.number().int().positive().optional(),
|
|
31
|
+
});
|
|
32
|
+
const graphEmbeddingOverridesSchema = zod_1.z.object({
|
|
33
|
+
docs: embeddingConfigSchema.partial().optional(),
|
|
34
|
+
code: embeddingConfigSchema.partial().optional(),
|
|
35
|
+
knowledge: embeddingConfigSchema.partial().optional(),
|
|
36
|
+
tasks: embeddingConfigSchema.partial().optional(),
|
|
37
|
+
files: embeddingConfigSchema.partial().optional(),
|
|
38
|
+
skills: embeddingConfigSchema.partial().optional(),
|
|
39
|
+
});
|
|
40
|
+
const projectSchema = zod_1.z.object({
|
|
41
|
+
projectDir: zod_1.z.string(),
|
|
42
|
+
graphMemory: zod_1.z.string().optional(),
|
|
43
|
+
docsPattern: zod_1.z.string().optional(),
|
|
44
|
+
codePattern: zod_1.z.string().optional(),
|
|
45
|
+
excludePattern: zod_1.z.string().optional(),
|
|
46
|
+
tsconfig: zod_1.z.string().optional(),
|
|
47
|
+
chunkDepth: zod_1.z.number().int().positive().optional(),
|
|
48
|
+
maxTokensDefault: zod_1.z.number().int().positive().optional(),
|
|
49
|
+
embedMaxChars: zod_1.z.number().int().positive().optional(),
|
|
50
|
+
embedding: embeddingConfigSchema.optional(),
|
|
51
|
+
graphs: graphEmbeddingOverridesSchema.optional(),
|
|
52
|
+
author: authorSchema.optional(),
|
|
53
|
+
});
|
|
54
|
+
const serverSchema = zod_1.z.object({
|
|
55
|
+
host: zod_1.z.string().optional(),
|
|
56
|
+
port: zod_1.z.number().int().positive().optional(),
|
|
57
|
+
sessionTimeout: zod_1.z.number().int().positive().optional(),
|
|
58
|
+
modelsDir: zod_1.z.string().optional(),
|
|
59
|
+
embedding: embeddingConfigSchema.optional(),
|
|
60
|
+
});
|
|
61
|
+
const wsGraphOverridesSchema = zod_1.z.object({
|
|
62
|
+
knowledge: embeddingConfigSchema.partial().optional(),
|
|
63
|
+
tasks: embeddingConfigSchema.partial().optional(),
|
|
64
|
+
skills: embeddingConfigSchema.partial().optional(),
|
|
65
|
+
});
|
|
66
|
+
const workspaceSchema = zod_1.z.object({
|
|
67
|
+
projects: zod_1.z.array(zod_1.z.string()),
|
|
68
|
+
graphMemory: zod_1.z.string().optional(),
|
|
69
|
+
mirrorDir: zod_1.z.string().optional(),
|
|
70
|
+
embedding: embeddingConfigSchema.optional(),
|
|
71
|
+
graphs: wsGraphOverridesSchema.optional(),
|
|
72
|
+
author: authorSchema.optional(),
|
|
73
|
+
});
|
|
74
|
+
const configFileSchema = zod_1.z.object({
|
|
75
|
+
author: authorSchema.optional(),
|
|
76
|
+
server: serverSchema.optional(),
|
|
77
|
+
projects: zod_1.z.record(zod_1.z.string(), projectSchema),
|
|
78
|
+
workspaces: zod_1.z.record(zod_1.z.string(), workspaceSchema).optional(),
|
|
79
|
+
});
|
|
80
|
+
exports.GRAPH_NAMES = ['docs', 'code', 'knowledge', 'tasks', 'files', 'skills'];
|
|
81
|
+
/**
|
|
82
|
+
* Build a stable fingerprint string from embedding config fields that affect stored vectors.
|
|
83
|
+
* Used to detect config changes that require re-indexing.
|
|
84
|
+
*
|
|
85
|
+
* queryPrefix is intentionally excluded: it only affects query-time embeddings,
|
|
86
|
+
* not the stored document vectors — so changing it does not require re-indexing.
|
|
87
|
+
*/
|
|
88
|
+
function embeddingFingerprint(config) {
|
|
89
|
+
return `${config.model}|${config.pooling}|${config.normalize}|${config.dtype ?? ''}|${config.documentPrefix}`;
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Helpers
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
const AUTHOR_DEFAULT = { name: '', email: '' };
|
|
95
|
+
/**
|
|
96
|
+
* Format an author as a git-style string: "Name <email>".
|
|
97
|
+
* Returns empty string if name is not set.
|
|
98
|
+
*/
|
|
99
|
+
function formatAuthor(author) {
|
|
100
|
+
if (!author.name)
|
|
101
|
+
return '';
|
|
102
|
+
return `${author.name} <${author.email}>`;
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Defaults
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
const EMBEDDING_DEFAULTS = {
|
|
108
|
+
model: 'Xenova/bge-m3',
|
|
109
|
+
pooling: 'cls',
|
|
110
|
+
normalize: true,
|
|
111
|
+
queryPrefix: '',
|
|
112
|
+
documentPrefix: '',
|
|
113
|
+
batchSize: 1,
|
|
114
|
+
};
|
|
115
|
+
const SERVER_DEFAULTS = {
|
|
116
|
+
host: '127.0.0.1',
|
|
117
|
+
port: 3000,
|
|
118
|
+
sessionTimeout: 1800,
|
|
119
|
+
modelsDir: path_1.default.join(HOME, '.graph-memory/models'),
|
|
120
|
+
embedding: EMBEDDING_DEFAULTS,
|
|
121
|
+
};
|
|
122
|
+
const PROJECT_DEFAULTS = {
|
|
123
|
+
docsPattern: '**/*.md',
|
|
124
|
+
codePattern: '**/*.{js,ts,jsx,tsx}',
|
|
125
|
+
excludePattern: 'node_modules/**',
|
|
126
|
+
chunkDepth: 4,
|
|
127
|
+
maxTokensDefault: 4000,
|
|
128
|
+
embedMaxChars: 2000,
|
|
129
|
+
};
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Resolution
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
/**
|
|
134
|
+
* Merge embedding configs with inheritance:
|
|
135
|
+
* override fields → base fields → defaults.
|
|
136
|
+
*/
|
|
137
|
+
function mergeEmbeddingConfig(base, override) {
|
|
138
|
+
if (!override)
|
|
139
|
+
return base;
|
|
140
|
+
return {
|
|
141
|
+
model: override.model ?? base.model,
|
|
142
|
+
pooling: override.pooling ?? base.pooling,
|
|
143
|
+
normalize: override.normalize ?? base.normalize,
|
|
144
|
+
dtype: override.dtype ?? base.dtype,
|
|
145
|
+
queryPrefix: override.queryPrefix ?? base.queryPrefix,
|
|
146
|
+
documentPrefix: override.documentPrefix ?? base.documentPrefix,
|
|
147
|
+
batchSize: override.batchSize ?? base.batchSize,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Resolve a full EmbeddingConfig from the Zod-parsed raw embedding object,
|
|
152
|
+
* falling back to defaults for missing fields.
|
|
153
|
+
*/
|
|
154
|
+
function resolveEmbeddingConfig(raw, fallback) {
|
|
155
|
+
if (!raw)
|
|
156
|
+
return fallback;
|
|
157
|
+
return {
|
|
158
|
+
model: raw.model,
|
|
159
|
+
pooling: raw.pooling ?? fallback.pooling,
|
|
160
|
+
normalize: raw.normalize ?? fallback.normalize,
|
|
161
|
+
dtype: raw.dtype ?? fallback.dtype,
|
|
162
|
+
queryPrefix: raw.queryPrefix ?? fallback.queryPrefix,
|
|
163
|
+
documentPrefix: raw.documentPrefix ?? fallback.documentPrefix,
|
|
164
|
+
batchSize: raw.batchSize ?? fallback.batchSize,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Load
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Load and validate a `graph-memory.yaml` config file.
|
|
172
|
+
* Resolves all paths to absolute, applies defaults.
|
|
173
|
+
*/
|
|
174
|
+
function loadMultiConfig(yamlPath) {
|
|
175
|
+
const raw = fs_1.default.readFileSync(yamlPath, 'utf-8');
|
|
176
|
+
const parsed = (0, yaml_1.parse)(raw);
|
|
177
|
+
const validated = configFileSchema.parse(parsed);
|
|
178
|
+
const srv = validated.server ?? {};
|
|
179
|
+
const globalAuthor = validated.author ?? AUTHOR_DEFAULT;
|
|
180
|
+
const globalEmbedding = resolveEmbeddingConfig(srv.embedding, EMBEDDING_DEFAULTS);
|
|
181
|
+
const server = {
|
|
182
|
+
host: srv.host ?? SERVER_DEFAULTS.host,
|
|
183
|
+
port: srv.port ?? SERVER_DEFAULTS.port,
|
|
184
|
+
sessionTimeout: srv.sessionTimeout ?? SERVER_DEFAULTS.sessionTimeout,
|
|
185
|
+
modelsDir: path_1.default.resolve(srv.modelsDir ?? SERVER_DEFAULTS.modelsDir),
|
|
186
|
+
embedding: globalEmbedding,
|
|
187
|
+
};
|
|
188
|
+
const projects = new Map();
|
|
189
|
+
for (const [id, raw] of Object.entries(validated.projects)) {
|
|
190
|
+
const projectDir = path_1.default.resolve(raw.projectDir);
|
|
191
|
+
const graphMemory = raw.graphMemory
|
|
192
|
+
? path_1.default.resolve(projectDir, raw.graphMemory)
|
|
193
|
+
: path_1.default.join(projectDir, '.graph-memory');
|
|
194
|
+
// Project-level embedding (inherits from server)
|
|
195
|
+
const projectEmbedding = resolveEmbeddingConfig(raw.embedding, globalEmbedding);
|
|
196
|
+
// Per-graph embeddings (inherit from project)
|
|
197
|
+
const graphOverrides = raw.graphs ?? {};
|
|
198
|
+
const graphEmbeddings = {};
|
|
199
|
+
for (const gn of exports.GRAPH_NAMES) {
|
|
200
|
+
graphEmbeddings[gn] = mergeEmbeddingConfig(projectEmbedding, graphOverrides[gn]);
|
|
201
|
+
}
|
|
202
|
+
projects.set(id, {
|
|
203
|
+
projectDir,
|
|
204
|
+
graphMemory,
|
|
205
|
+
docsPattern: raw.docsPattern ?? PROJECT_DEFAULTS.docsPattern,
|
|
206
|
+
codePattern: raw.codePattern ?? PROJECT_DEFAULTS.codePattern,
|
|
207
|
+
excludePattern: raw.excludePattern ?? PROJECT_DEFAULTS.excludePattern,
|
|
208
|
+
tsconfig: raw.tsconfig,
|
|
209
|
+
chunkDepth: raw.chunkDepth ?? PROJECT_DEFAULTS.chunkDepth,
|
|
210
|
+
maxTokensDefault: raw.maxTokensDefault ?? PROJECT_DEFAULTS.maxTokensDefault,
|
|
211
|
+
embedMaxChars: raw.embedMaxChars ?? PROJECT_DEFAULTS.embedMaxChars,
|
|
212
|
+
embedding: projectEmbedding,
|
|
213
|
+
graphEmbeddings,
|
|
214
|
+
author: raw.author ?? globalAuthor,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// --- Workspaces ---
|
|
218
|
+
const workspaces = new Map();
|
|
219
|
+
if (validated.workspaces) {
|
|
220
|
+
for (const [wsId, raw] of Object.entries(validated.workspaces)) {
|
|
221
|
+
// Validate that all referenced projects exist
|
|
222
|
+
for (const projId of raw.projects) {
|
|
223
|
+
if (!projects.has(projId)) {
|
|
224
|
+
throw new Error(`Workspace "${wsId}" references unknown project "${projId}"`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const firstProject = projects.get(raw.projects[0]);
|
|
228
|
+
const graphMemory = raw.graphMemory
|
|
229
|
+
? path_1.default.resolve(raw.graphMemory)
|
|
230
|
+
: path_1.default.join(firstProject.projectDir, '.graph-memory', 'workspace');
|
|
231
|
+
const mirrorDir = raw.mirrorDir
|
|
232
|
+
? path_1.default.resolve(raw.mirrorDir)
|
|
233
|
+
: graphMemory;
|
|
234
|
+
// Workspace-level embedding (inherits from server)
|
|
235
|
+
const wsEmbedding = resolveEmbeddingConfig(raw.embedding, globalEmbedding);
|
|
236
|
+
// Per-graph embeddings for workspace's shared graphs (knowledge, tasks, skills)
|
|
237
|
+
const graphOverrides = raw.graphs ?? {};
|
|
238
|
+
const graphEmbeddings = {
|
|
239
|
+
knowledge: mergeEmbeddingConfig(wsEmbedding, graphOverrides.knowledge),
|
|
240
|
+
tasks: mergeEmbeddingConfig(wsEmbedding, graphOverrides.tasks),
|
|
241
|
+
skills: mergeEmbeddingConfig(wsEmbedding, graphOverrides.skills),
|
|
242
|
+
};
|
|
243
|
+
workspaces.set(wsId, {
|
|
244
|
+
projects: raw.projects,
|
|
245
|
+
graphMemory,
|
|
246
|
+
mirrorDir,
|
|
247
|
+
embedding: wsEmbedding,
|
|
248
|
+
graphEmbeddings,
|
|
249
|
+
author: raw.author ?? globalAuthor,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { author: globalAuthor, server, projects, workspaces };
|
|
254
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getProject = getProject;
|
|
7
|
+
exports.resetProject = resetProject;
|
|
8
|
+
exports.parseCodeFile = parseCodeFile;
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const ts_morph_1 = require("ts-morph");
|
|
11
|
+
// Keyed cache: one ts-morph Project per codeDir — avoids re-parsing tsconfig
|
|
12
|
+
const _projects = new Map();
|
|
13
|
+
function getProject(codeDir, tsconfig) {
|
|
14
|
+
const existing = _projects.get(codeDir);
|
|
15
|
+
if (existing)
|
|
16
|
+
return existing;
|
|
17
|
+
const tsconfigPath = tsconfig ?? path_1.default.join(codeDir, 'tsconfig.json');
|
|
18
|
+
let project;
|
|
19
|
+
try {
|
|
20
|
+
project = new ts_morph_1.Project({
|
|
21
|
+
tsConfigFilePath: tsconfigPath,
|
|
22
|
+
skipAddingFilesFromTsConfig: true, // we add files manually
|
|
23
|
+
skipFileDependencyResolution: false,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// No tsconfig — use minimal compiler options
|
|
28
|
+
project = new ts_morph_1.Project({
|
|
29
|
+
compilerOptions: { allowJs: true, strict: false },
|
|
30
|
+
skipFileDependencyResolution: true,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
_projects.set(codeDir, project);
|
|
34
|
+
return project;
|
|
35
|
+
}
|
|
36
|
+
/** Reset cached project for a specific codeDir, or all if omitted. */
|
|
37
|
+
function resetProject(codeDir) {
|
|
38
|
+
if (codeDir) {
|
|
39
|
+
_projects.delete(codeDir);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
_projects.clear();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function parseCodeFile(absolutePath, codeDir, mtime, project) {
|
|
46
|
+
const fileId = path_1.default.relative(codeDir, absolutePath);
|
|
47
|
+
// Add or refresh the source file in the project
|
|
48
|
+
let sourceFile = project.getSourceFile(absolutePath);
|
|
49
|
+
if (sourceFile) {
|
|
50
|
+
sourceFile.refreshFromFileSystemSync();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
sourceFile = project.addSourceFileAtPath(absolutePath);
|
|
54
|
+
}
|
|
55
|
+
const nodes = [];
|
|
56
|
+
const edges = [];
|
|
57
|
+
const fileNodeId = fileId;
|
|
58
|
+
// --- File root node ---
|
|
59
|
+
const fileDocComment = extractFileDocComment(sourceFile);
|
|
60
|
+
const importSummary = buildImportSummary(sourceFile);
|
|
61
|
+
nodes.push({
|
|
62
|
+
id: fileNodeId,
|
|
63
|
+
attrs: {
|
|
64
|
+
kind: 'file',
|
|
65
|
+
fileId,
|
|
66
|
+
name: path_1.default.basename(fileId),
|
|
67
|
+
signature: fileId,
|
|
68
|
+
docComment: fileDocComment,
|
|
69
|
+
body: importSummary,
|
|
70
|
+
startLine: 1,
|
|
71
|
+
endLine: sourceFile.getEndLineNumber(),
|
|
72
|
+
isExported: false,
|
|
73
|
+
embedding: [],
|
|
74
|
+
fileEmbedding: [],
|
|
75
|
+
mtime,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
// --- Top-level declarations ---
|
|
79
|
+
for (const decl of sourceFile.getStatements()) {
|
|
80
|
+
processStatement(decl, fileId, fileNodeId, mtime, nodes, edges);
|
|
81
|
+
}
|
|
82
|
+
// --- Import edges: file → imported file ---
|
|
83
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
84
|
+
const moduleSpecifier = imp.getModuleSpecifierValue();
|
|
85
|
+
if (!isRelative(moduleSpecifier))
|
|
86
|
+
continue;
|
|
87
|
+
const resolved = imp.getModuleSpecifierSourceFile();
|
|
88
|
+
if (!resolved)
|
|
89
|
+
continue;
|
|
90
|
+
const targetAbsolute = resolved.getFilePath();
|
|
91
|
+
if (!targetAbsolute.startsWith(codeDir))
|
|
92
|
+
continue;
|
|
93
|
+
const targetFileId = path_1.default.relative(codeDir, targetAbsolute);
|
|
94
|
+
const targetNodeId = targetFileId;
|
|
95
|
+
if (targetNodeId !== fileNodeId) {
|
|
96
|
+
edges.push({
|
|
97
|
+
from: fileNodeId,
|
|
98
|
+
to: targetNodeId,
|
|
99
|
+
attrs: { kind: 'imports' },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { fileId, mtime, nodes, edges };
|
|
104
|
+
}
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Statement processing
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
function processStatement(node, fileId, parentId, mtime, nodes, edges) {
|
|
109
|
+
// Function declarations
|
|
110
|
+
if (ts_morph_1.Node.isFunctionDeclaration(node)) {
|
|
111
|
+
const name = node.getName();
|
|
112
|
+
if (!name)
|
|
113
|
+
return;
|
|
114
|
+
const id = makeId(fileId, name);
|
|
115
|
+
nodes.push({ id, attrs: buildAttrs('function', fileId, name, node, mtime) });
|
|
116
|
+
edges.push({ from: parentId, to: id, attrs: { kind: 'contains' } });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Class declarations
|
|
120
|
+
if (ts_morph_1.Node.isClassDeclaration(node)) {
|
|
121
|
+
const name = node.getName();
|
|
122
|
+
if (!name)
|
|
123
|
+
return;
|
|
124
|
+
const id = makeId(fileId, name);
|
|
125
|
+
nodes.push({ id, attrs: buildAttrs('class', fileId, name, node, mtime) });
|
|
126
|
+
edges.push({ from: parentId, to: id, attrs: { kind: 'contains' } });
|
|
127
|
+
// extends
|
|
128
|
+
const base = node.getBaseClass();
|
|
129
|
+
if (base) {
|
|
130
|
+
const baseName = base.getName?.() ?? base.getSymbol()?.getName();
|
|
131
|
+
if (baseName) {
|
|
132
|
+
edges.push({ from: id, to: makeId(fileId, baseName), attrs: { kind: 'extends' } });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// implements
|
|
136
|
+
for (const impl of node.getImplements()) {
|
|
137
|
+
const implName = impl.getExpression().getText();
|
|
138
|
+
edges.push({ from: id, to: makeId(fileId, implName), attrs: { kind: 'implements' } });
|
|
139
|
+
}
|
|
140
|
+
// Methods
|
|
141
|
+
for (const method of node.getMethods()) {
|
|
142
|
+
const methodName = method.getName();
|
|
143
|
+
const methodId = makeId(fileId, name, methodName);
|
|
144
|
+
nodes.push({ id: methodId, attrs: buildAttrs('method', fileId, methodName, method, mtime) });
|
|
145
|
+
edges.push({ from: id, to: methodId, attrs: { kind: 'contains' } });
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Interface declarations
|
|
150
|
+
if (ts_morph_1.Node.isInterfaceDeclaration(node)) {
|
|
151
|
+
const name = node.getName();
|
|
152
|
+
const id = makeId(fileId, name);
|
|
153
|
+
nodes.push({ id, attrs: buildAttrs('interface', fileId, name, node, mtime) });
|
|
154
|
+
edges.push({ from: parentId, to: id, attrs: { kind: 'contains' } });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Type alias declarations
|
|
158
|
+
if (ts_morph_1.Node.isTypeAliasDeclaration(node)) {
|
|
159
|
+
const name = node.getName();
|
|
160
|
+
const id = makeId(fileId, name);
|
|
161
|
+
nodes.push({ id, attrs: buildAttrs('type', fileId, name, node, mtime) });
|
|
162
|
+
edges.push({ from: parentId, to: id, attrs: { kind: 'contains' } });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Enum declarations
|
|
166
|
+
if (ts_morph_1.Node.isEnumDeclaration(node)) {
|
|
167
|
+
const name = node.getName();
|
|
168
|
+
const id = makeId(fileId, name);
|
|
169
|
+
nodes.push({ id, attrs: buildAttrs('enum', fileId, name, node, mtime) });
|
|
170
|
+
edges.push({ from: parentId, to: id, attrs: { kind: 'contains' } });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Variable statements: export const foo = ..., export const bar = () => ...
|
|
174
|
+
if (ts_morph_1.Node.isVariableStatement(node)) {
|
|
175
|
+
for (const decl of node.getDeclarations()) {
|
|
176
|
+
const name = decl.getName();
|
|
177
|
+
if (typeof name !== 'string')
|
|
178
|
+
continue;
|
|
179
|
+
const initializer = decl.getInitializer();
|
|
180
|
+
const isArrow = initializer?.getKind() === ts_morph_1.SyntaxKind.ArrowFunction ||
|
|
181
|
+
initializer?.getKind() === ts_morph_1.SyntaxKind.FunctionExpression;
|
|
182
|
+
const kind = isArrow ? 'function' : 'variable';
|
|
183
|
+
const id = makeId(fileId, name);
|
|
184
|
+
nodes.push({ id, attrs: buildAttrs(kind, fileId, name, node, mtime) });
|
|
185
|
+
edges.push({ from: parentId, to: id, attrs: { kind: 'contains' } });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Helpers
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
function makeId(fileId, ...parts) {
|
|
193
|
+
return [fileId, ...parts].join('::');
|
|
194
|
+
}
|
|
195
|
+
function buildAttrs(kind, fileId, name, node, mtime) {
|
|
196
|
+
const start = node.getStartLineNumber();
|
|
197
|
+
const end = node.getEndLineNumber();
|
|
198
|
+
const fullText = node.getFullText().trim();
|
|
199
|
+
// Signature = first line (up to opening brace or end of line)
|
|
200
|
+
const firstLine = fullText.split('\n')[0].trim();
|
|
201
|
+
const signature = firstLine.length > 200 ? firstLine.slice(0, 200) + '…' : firstLine;
|
|
202
|
+
// JSDoc = leading comment of the node
|
|
203
|
+
const jsDocs = 'getJsDocs' in node
|
|
204
|
+
? node.getJsDocs()
|
|
205
|
+
: [];
|
|
206
|
+
const docComment = jsDocs.map(d => d.getFullText().trim()).join('\n').trim();
|
|
207
|
+
// isExported
|
|
208
|
+
const isExported = 'isExported' in node
|
|
209
|
+
? node.isExported()
|
|
210
|
+
: false;
|
|
211
|
+
return {
|
|
212
|
+
kind,
|
|
213
|
+
fileId,
|
|
214
|
+
name,
|
|
215
|
+
signature,
|
|
216
|
+
docComment,
|
|
217
|
+
body: fullText,
|
|
218
|
+
startLine: start,
|
|
219
|
+
endLine: end,
|
|
220
|
+
isExported,
|
|
221
|
+
embedding: [],
|
|
222
|
+
fileEmbedding: [],
|
|
223
|
+
mtime,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function extractFileDocComment(sourceFile) {
|
|
227
|
+
// First statement's leading trivia that looks like a file-level JSDoc
|
|
228
|
+
const firstStatement = sourceFile.getStatements()[0];
|
|
229
|
+
if (!firstStatement)
|
|
230
|
+
return '';
|
|
231
|
+
const leadingComments = firstStatement.getLeadingCommentRanges();
|
|
232
|
+
return leadingComments
|
|
233
|
+
.map(r => r.getText())
|
|
234
|
+
.filter(t => t.startsWith('/**') || t.startsWith('//'))
|
|
235
|
+
.join('\n')
|
|
236
|
+
.trim();
|
|
237
|
+
}
|
|
238
|
+
function buildImportSummary(sourceFile) {
|
|
239
|
+
const imports = sourceFile.getImportDeclarations();
|
|
240
|
+
if (imports.length === 0)
|
|
241
|
+
return '';
|
|
242
|
+
return imports.map(i => i.getText().trim()).join('\n');
|
|
243
|
+
}
|
|
244
|
+
function isRelative(moduleSpecifier) {
|
|
245
|
+
return moduleSpecifier.startsWith('./') || moduleSpecifier.startsWith('../');
|
|
246
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractSymbols = extractSymbols;
|
|
4
|
+
const ts_morph_1 = require("ts-morph");
|
|
5
|
+
const TS_LANGUAGES = new Set([
|
|
6
|
+
'ts', 'typescript',
|
|
7
|
+
'js', 'javascript',
|
|
8
|
+
'tsx', 'jsx',
|
|
9
|
+
]);
|
|
10
|
+
/**
|
|
11
|
+
* Extract top-level symbol names from a code block using ts-morph.
|
|
12
|
+
* Returns [] for non-TS/JS languages or on parse failure.
|
|
13
|
+
*/
|
|
14
|
+
function extractSymbols(code, language) {
|
|
15
|
+
if (!TS_LANGUAGES.has(language.toLowerCase()))
|
|
16
|
+
return [];
|
|
17
|
+
try {
|
|
18
|
+
const project = new ts_morph_1.Project({
|
|
19
|
+
useInMemoryFileSystem: true,
|
|
20
|
+
compilerOptions: { allowJs: true, strict: false },
|
|
21
|
+
});
|
|
22
|
+
const ext = language.toLowerCase().startsWith('ts') || language.toLowerCase() === 'tsx'
|
|
23
|
+
? '.tsx'
|
|
24
|
+
: '.jsx';
|
|
25
|
+
const sourceFile = project.createSourceFile(`__snippet${ext}`, code);
|
|
26
|
+
const symbols = [];
|
|
27
|
+
for (const stmt of sourceFile.getStatements()) {
|
|
28
|
+
if (ts_morph_1.Node.isFunctionDeclaration(stmt)) {
|
|
29
|
+
const name = stmt.getName();
|
|
30
|
+
if (name)
|
|
31
|
+
symbols.push(name);
|
|
32
|
+
}
|
|
33
|
+
else if (ts_morph_1.Node.isClassDeclaration(stmt)) {
|
|
34
|
+
const name = stmt.getName();
|
|
35
|
+
if (name)
|
|
36
|
+
symbols.push(name);
|
|
37
|
+
}
|
|
38
|
+
else if (ts_morph_1.Node.isInterfaceDeclaration(stmt)) {
|
|
39
|
+
symbols.push(stmt.getName());
|
|
40
|
+
}
|
|
41
|
+
else if (ts_morph_1.Node.isTypeAliasDeclaration(stmt)) {
|
|
42
|
+
symbols.push(stmt.getName());
|
|
43
|
+
}
|
|
44
|
+
else if (ts_morph_1.Node.isEnumDeclaration(stmt)) {
|
|
45
|
+
symbols.push(stmt.getName());
|
|
46
|
+
}
|
|
47
|
+
else if (ts_morph_1.Node.isVariableStatement(stmt)) {
|
|
48
|
+
for (const decl of stmt.getDeclarations()) {
|
|
49
|
+
const name = decl.getName();
|
|
50
|
+
if (typeof name === 'string')
|
|
51
|
+
symbols.push(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (ts_morph_1.Node.isExportAssignment(stmt)) {
|
|
55
|
+
// export default ... — skip, no named symbol
|
|
56
|
+
}
|
|
57
|
+
else if (ts_morph_1.Node.isExpressionStatement(stmt)) {
|
|
58
|
+
// e.g. `app.use(...)` — skip
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return symbols;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|