@graphmemory/server 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +216 -0
  3. package/dist/api/index.js +473 -0
  4. package/dist/api/rest/code.js +78 -0
  5. package/dist/api/rest/docs.js +80 -0
  6. package/dist/api/rest/embed.js +47 -0
  7. package/dist/api/rest/files.js +64 -0
  8. package/dist/api/rest/graph.js +71 -0
  9. package/dist/api/rest/index.js +371 -0
  10. package/dist/api/rest/knowledge.js +239 -0
  11. package/dist/api/rest/skills.js +285 -0
  12. package/dist/api/rest/tasks.js +273 -0
  13. package/dist/api/rest/tools.js +157 -0
  14. package/dist/api/rest/validation.js +196 -0
  15. package/dist/api/rest/websocket.js +71 -0
  16. package/dist/api/tools/code/get-file-symbols.js +30 -0
  17. package/dist/api/tools/code/get-symbol.js +22 -0
  18. package/dist/api/tools/code/list-files.js +18 -0
  19. package/dist/api/tools/code/search-code.js +27 -0
  20. package/dist/api/tools/code/search-files.js +22 -0
  21. package/dist/api/tools/context/get-context.js +19 -0
  22. package/dist/api/tools/docs/cross-references.js +76 -0
  23. package/dist/api/tools/docs/explain-symbol.js +55 -0
  24. package/dist/api/tools/docs/find-examples.js +52 -0
  25. package/dist/api/tools/docs/get-node.js +24 -0
  26. package/dist/api/tools/docs/get-toc.js +22 -0
  27. package/dist/api/tools/docs/list-snippets.js +46 -0
  28. package/dist/api/tools/docs/list-topics.js +18 -0
  29. package/dist/api/tools/docs/search-files.js +22 -0
  30. package/dist/api/tools/docs/search-snippets.js +43 -0
  31. package/dist/api/tools/docs/search.js +27 -0
  32. package/dist/api/tools/file-index/get-file-info.js +21 -0
  33. package/dist/api/tools/file-index/list-all-files.js +28 -0
  34. package/dist/api/tools/file-index/search-all-files.js +24 -0
  35. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  36. package/dist/api/tools/knowledge/create-note.js +20 -0
  37. package/dist/api/tools/knowledge/create-relation.js +29 -0
  38. package/dist/api/tools/knowledge/delete-note.js +19 -0
  39. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  40. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  41. package/dist/api/tools/knowledge/get-note.js +20 -0
  42. package/dist/api/tools/knowledge/list-notes.js +18 -0
  43. package/dist/api/tools/knowledge/list-relations.js +17 -0
  44. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  45. package/dist/api/tools/knowledge/search-notes.js +25 -0
  46. package/dist/api/tools/knowledge/update-note.js +34 -0
  47. package/dist/api/tools/skills/add-attachment.js +31 -0
  48. package/dist/api/tools/skills/bump-usage.js +19 -0
  49. package/dist/api/tools/skills/create-skill-link.js +25 -0
  50. package/dist/api/tools/skills/create-skill.js +26 -0
  51. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  52. package/dist/api/tools/skills/delete-skill.js +20 -0
  53. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  54. package/dist/api/tools/skills/get-skill.js +21 -0
  55. package/dist/api/tools/skills/link-skill.js +23 -0
  56. package/dist/api/tools/skills/list-skills.js +20 -0
  57. package/dist/api/tools/skills/recall-skills.js +18 -0
  58. package/dist/api/tools/skills/remove-attachment.js +19 -0
  59. package/dist/api/tools/skills/search-skills.js +25 -0
  60. package/dist/api/tools/skills/update-skill.js +58 -0
  61. package/dist/api/tools/tasks/add-attachment.js +31 -0
  62. package/dist/api/tools/tasks/create-task-link.js +25 -0
  63. package/dist/api/tools/tasks/create-task.js +26 -0
  64. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  65. package/dist/api/tools/tasks/delete-task.js +20 -0
  66. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  67. package/dist/api/tools/tasks/get-task.js +20 -0
  68. package/dist/api/tools/tasks/link-task.js +23 -0
  69. package/dist/api/tools/tasks/list-tasks.js +25 -0
  70. package/dist/api/tools/tasks/move-task.js +38 -0
  71. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  72. package/dist/api/tools/tasks/search-tasks.js +25 -0
  73. package/dist/api/tools/tasks/update-task.js +58 -0
  74. package/dist/cli/index.js +617 -0
  75. package/dist/cli/indexer.js +275 -0
  76. package/dist/graphs/attachment-types.js +74 -0
  77. package/dist/graphs/code-types.js +10 -0
  78. package/dist/graphs/code.js +204 -0
  79. package/dist/graphs/docs.js +231 -0
  80. package/dist/graphs/file-index-types.js +10 -0
  81. package/dist/graphs/file-index.js +310 -0
  82. package/dist/graphs/file-lang.js +119 -0
  83. package/dist/graphs/knowledge-types.js +32 -0
  84. package/dist/graphs/knowledge.js +768 -0
  85. package/dist/graphs/manager-types.js +87 -0
  86. package/dist/graphs/skill-types.js +10 -0
  87. package/dist/graphs/skill.js +1016 -0
  88. package/dist/graphs/task-types.js +17 -0
  89. package/dist/graphs/task.js +972 -0
  90. package/dist/lib/access.js +67 -0
  91. package/dist/lib/embedder.js +235 -0
  92. package/dist/lib/events-log.js +401 -0
  93. package/dist/lib/file-import.js +328 -0
  94. package/dist/lib/file-mirror.js +461 -0
  95. package/dist/lib/frontmatter.js +17 -0
  96. package/dist/lib/jwt.js +146 -0
  97. package/dist/lib/mirror-watcher.js +637 -0
  98. package/dist/lib/multi-config.js +393 -0
  99. package/dist/lib/parsers/code.js +214 -0
  100. package/dist/lib/parsers/codeblock.js +33 -0
  101. package/dist/lib/parsers/docs.js +199 -0
  102. package/dist/lib/parsers/languages/index.js +15 -0
  103. package/dist/lib/parsers/languages/registry.js +68 -0
  104. package/dist/lib/parsers/languages/types.js +2 -0
  105. package/dist/lib/parsers/languages/typescript.js +306 -0
  106. package/dist/lib/project-manager.js +458 -0
  107. package/dist/lib/promise-queue.js +22 -0
  108. package/dist/lib/search/bm25.js +167 -0
  109. package/dist/lib/search/code.js +103 -0
  110. package/dist/lib/search/docs.js +106 -0
  111. package/dist/lib/search/file-index.js +31 -0
  112. package/dist/lib/search/files.js +61 -0
  113. package/dist/lib/search/knowledge.js +101 -0
  114. package/dist/lib/search/skills.js +104 -0
  115. package/dist/lib/search/tasks.js +103 -0
  116. package/dist/lib/team.js +89 -0
  117. package/dist/lib/watcher.js +67 -0
  118. package/dist/ui/assets/index-D6oxrVF7.js +1759 -0
  119. package/dist/ui/assets/index-kKd4mVrh.css +1 -0
  120. package/dist/ui/favicon.svg +1 -0
  121. package/dist/ui/icons.svg +24 -0
  122. package/dist/ui/index.html +14 -0
  123. package/package.json +89 -0
@@ -0,0 +1,393 @@
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
+ // Model config: taken as a whole from the first level that defines it (no field merge)
24
+ const modelConfigSchema = zod_1.z.object({
25
+ name: zod_1.z.string(),
26
+ pooling: zod_1.z.enum(['mean', 'cls']).optional(),
27
+ normalize: zod_1.z.boolean().optional(),
28
+ dtype: zod_1.z.string().optional(),
29
+ queryPrefix: zod_1.z.string().optional(),
30
+ documentPrefix: zod_1.z.string().optional(),
31
+ });
32
+ // Embedding config: each field individually inherits up the chain
33
+ const embeddingConfigSchema = zod_1.z.object({
34
+ batchSize: zod_1.z.number().int().positive().optional(),
35
+ maxChars: zod_1.z.number().int().positive().optional(),
36
+ cacheSize: zod_1.z.number().int().min(0).optional(),
37
+ remote: zod_1.z.string().optional(), // Remote embedding API URL
38
+ remoteApiKey: zod_1.z.string().optional(), // API key for remote embedding
39
+ });
40
+ const accessLevelSchema = zod_1.z.enum(['deny', 'r', 'rw']);
41
+ const accessMapSchema = zod_1.z.record(zod_1.z.string(), accessLevelSchema).optional();
42
+ // Exclude accepts string ("a,b") or array (["a", "b"]) in YAML
43
+ const excludeSchema = zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional();
44
+ const userSchema = zod_1.z.object({
45
+ name: zod_1.z.string(),
46
+ email: zod_1.z.string(),
47
+ apiKey: zod_1.z.string(),
48
+ passwordHash: zod_1.z.string().optional(),
49
+ });
50
+ const graphConfigSchema = zod_1.z.object({
51
+ enabled: zod_1.z.boolean().optional(),
52
+ include: zod_1.z.string().optional(),
53
+ exclude: excludeSchema,
54
+ model: modelConfigSchema.optional(),
55
+ embedding: embeddingConfigSchema.optional(),
56
+ access: accessMapSchema,
57
+ });
58
+ const graphsConfigSchema = zod_1.z.object({
59
+ docs: graphConfigSchema.optional(),
60
+ code: graphConfigSchema.optional(),
61
+ knowledge: graphConfigSchema.optional(),
62
+ tasks: graphConfigSchema.optional(),
63
+ files: graphConfigSchema.optional(),
64
+ skills: graphConfigSchema.optional(),
65
+ });
66
+ const projectSchema = zod_1.z.object({
67
+ projectDir: zod_1.z.string(),
68
+ graphMemory: zod_1.z.string().optional(),
69
+ exclude: excludeSchema,
70
+ chunkDepth: zod_1.z.number().int().positive().optional(),
71
+ maxFileSize: zod_1.z.number().int().positive().optional(),
72
+ model: modelConfigSchema.optional(),
73
+ embedding: embeddingConfigSchema.optional(),
74
+ graphs: graphsConfigSchema.optional(),
75
+ author: authorSchema.optional(),
76
+ access: accessMapSchema,
77
+ });
78
+ const embeddingApiSchema = zod_1.z.object({
79
+ enabled: zod_1.z.boolean().optional(),
80
+ apiKey: zod_1.z.string().optional(),
81
+ maxTexts: zod_1.z.number().int().positive().optional(), // max texts per request
82
+ maxTextChars: zod_1.z.number().int().positive().optional(), // max chars per text
83
+ });
84
+ const rateLimitSchema = zod_1.z.object({
85
+ global: zod_1.z.number().int().min(0).optional(), // req/min per IP (0 = disabled)
86
+ search: zod_1.z.number().int().min(0).optional(), // req/min per IP for search/embed
87
+ auth: zod_1.z.number().int().min(0).optional(), // req/min per IP for login
88
+ });
89
+ const serverSchema = zod_1.z.object({
90
+ host: zod_1.z.string().optional(),
91
+ port: zod_1.z.number().int().positive().optional(),
92
+ sessionTimeout: zod_1.z.number().int().positive().optional(),
93
+ modelsDir: zod_1.z.string().optional(),
94
+ corsOrigins: zod_1.z.array(zod_1.z.string()).optional(),
95
+ model: modelConfigSchema.optional(),
96
+ embedding: embeddingConfigSchema.optional(),
97
+ embeddingApi: embeddingApiSchema.optional(),
98
+ defaultAccess: accessLevelSchema.optional(),
99
+ access: accessMapSchema,
100
+ jwtSecret: zod_1.z.string().optional(),
101
+ accessTokenTtl: zod_1.z.string().optional(),
102
+ refreshTokenTtl: zod_1.z.string().optional(),
103
+ rateLimit: rateLimitSchema.optional(),
104
+ maxFileSize: zod_1.z.number().int().positive().optional(),
105
+ exclude: excludeSchema,
106
+ });
107
+ const wsGraphConfigSchema = zod_1.z.object({
108
+ enabled: zod_1.z.boolean().optional(),
109
+ exclude: excludeSchema,
110
+ model: modelConfigSchema.optional(),
111
+ embedding: embeddingConfigSchema.optional(),
112
+ access: accessMapSchema,
113
+ });
114
+ const wsGraphsConfigSchema = zod_1.z.object({
115
+ knowledge: wsGraphConfigSchema.optional(),
116
+ tasks: wsGraphConfigSchema.optional(),
117
+ skills: wsGraphConfigSchema.optional(),
118
+ });
119
+ const workspaceSchema = zod_1.z.object({
120
+ projects: zod_1.z.array(zod_1.z.string()),
121
+ graphMemory: zod_1.z.string().optional(),
122
+ mirrorDir: zod_1.z.string().optional(),
123
+ model: modelConfigSchema.optional(),
124
+ embedding: embeddingConfigSchema.optional(),
125
+ graphs: wsGraphsConfigSchema.optional(),
126
+ author: authorSchema.optional(),
127
+ access: accessMapSchema,
128
+ maxFileSize: zod_1.z.number().int().positive().optional(),
129
+ exclude: excludeSchema,
130
+ });
131
+ const configFileSchema = zod_1.z.object({
132
+ author: authorSchema.optional(),
133
+ server: serverSchema.optional(),
134
+ users: zod_1.z.record(zod_1.z.string(), userSchema).optional(),
135
+ projects: zod_1.z.record(zod_1.z.string(), projectSchema),
136
+ workspaces: zod_1.z.record(zod_1.z.string(), workspaceSchema).optional(),
137
+ });
138
+ exports.GRAPH_NAMES = ['docs', 'code', 'knowledge', 'tasks', 'files', 'skills'];
139
+ /**
140
+ * Build a stable fingerprint string from model config fields that affect stored vectors.
141
+ * queryPrefix excluded: only affects query-time, not stored document vectors.
142
+ */
143
+ function embeddingFingerprint(model) {
144
+ return `${model.name}|${model.pooling}|${model.normalize}|${model.dtype ?? ''}|${model.documentPrefix}`;
145
+ }
146
+ // ---------------------------------------------------------------------------
147
+ // Helpers
148
+ // ---------------------------------------------------------------------------
149
+ const AUTHOR_DEFAULT = { name: '', email: '' };
150
+ /**
151
+ * Format an author as a git-style string: "Name <email>".
152
+ * Returns empty string if name is not set.
153
+ */
154
+ function formatAuthor(author) {
155
+ if (!author.name)
156
+ return '';
157
+ return `${author.name} <${author.email}>`;
158
+ }
159
+ // ---------------------------------------------------------------------------
160
+ // Defaults
161
+ // ---------------------------------------------------------------------------
162
+ const MODEL_DEFAULTS = {
163
+ name: 'Xenova/bge-m3',
164
+ pooling: 'cls',
165
+ normalize: true,
166
+ queryPrefix: '',
167
+ documentPrefix: '',
168
+ };
169
+ const EMBEDDING_DEFAULTS = {
170
+ batchSize: 1,
171
+ maxChars: 8000,
172
+ cacheSize: 10_000,
173
+ };
174
+ const RATE_LIMIT_DEFAULTS = {
175
+ global: 600,
176
+ search: 120,
177
+ auth: 10,
178
+ };
179
+ const SERVER_DEFAULTS = {
180
+ host: '127.0.0.1',
181
+ port: 3000,
182
+ sessionTimeout: 1800,
183
+ modelsDir: path_1.default.join(HOME, '.graph-memory/models'),
184
+ model: MODEL_DEFAULTS,
185
+ embedding: EMBEDDING_DEFAULTS,
186
+ defaultAccess: 'rw',
187
+ accessTokenTtl: '15m',
188
+ refreshTokenTtl: '7d',
189
+ rateLimit: RATE_LIMIT_DEFAULTS,
190
+ maxFileSize: 1 * 1024 * 1024, // 1 MB
191
+ exclude: ['**/node_modules/**', '**/dist/**'],
192
+ };
193
+ const PROJECT_DEFAULTS = {
194
+ docsInclude: '**/*.md',
195
+ codeInclude: '**/*.{js,ts,jsx,tsx}',
196
+ chunkDepth: 4,
197
+ };
198
+ /** Parse comma-separated exclude string into array of patterns. */
199
+ function parseExclude(raw) {
200
+ if (!raw)
201
+ return [];
202
+ if (Array.isArray(raw))
203
+ return raw.map(p => p.trim()).filter(Boolean);
204
+ return raw.split(',').map(p => p.trim()).filter(Boolean);
205
+ }
206
+ // ---------------------------------------------------------------------------
207
+ // Resolution
208
+ // ---------------------------------------------------------------------------
209
+ /**
210
+ * Resolve ModelConfig: take the whole object from the first level that defines it.
211
+ * If undefined, returns parent (next level up the chain).
212
+ */
213
+ function resolveModel(raw, parent) {
214
+ if (!raw)
215
+ return parent;
216
+ return {
217
+ name: raw.name,
218
+ pooling: raw.pooling ?? MODEL_DEFAULTS.pooling,
219
+ normalize: raw.normalize ?? MODEL_DEFAULTS.normalize,
220
+ dtype: raw.dtype,
221
+ queryPrefix: raw.queryPrefix ?? MODEL_DEFAULTS.queryPrefix,
222
+ documentPrefix: raw.documentPrefix ?? MODEL_DEFAULTS.documentPrefix,
223
+ };
224
+ }
225
+ /**
226
+ * Resolve EmbeddingConfig: each field individually inherits up the chain.
227
+ */
228
+ function resolveEmbedding(raw, parent) {
229
+ if (!raw)
230
+ return parent;
231
+ return {
232
+ batchSize: raw.batchSize ?? parent.batchSize,
233
+ maxChars: raw.maxChars ?? parent.maxChars,
234
+ cacheSize: raw.cacheSize ?? parent.cacheSize,
235
+ remote: raw.remote ?? parent.remote,
236
+ remoteApiKey: raw.remoteApiKey ?? parent.remoteApiKey,
237
+ };
238
+ }
239
+ // ---------------------------------------------------------------------------
240
+ // Load
241
+ // ---------------------------------------------------------------------------
242
+ /**
243
+ * Load and validate a `graph-memory.yaml` config file.
244
+ * Resolves all paths to absolute, applies defaults.
245
+ */
246
+ function loadMultiConfig(yamlPath) {
247
+ const raw = fs_1.default.readFileSync(yamlPath, 'utf-8');
248
+ const parsed = (0, yaml_1.parse)(raw);
249
+ const validated = configFileSchema.parse(parsed);
250
+ const srv = validated.server ?? {};
251
+ const globalAuthor = validated.author ?? AUTHOR_DEFAULT;
252
+ // Server-level: model + embedding
253
+ const serverModel = resolveModel(srv.model, MODEL_DEFAULTS);
254
+ const serverEmbedding = resolveEmbedding(srv.embedding, EMBEDDING_DEFAULTS);
255
+ const server = {
256
+ host: srv.host ?? SERVER_DEFAULTS.host,
257
+ port: srv.port ?? SERVER_DEFAULTS.port,
258
+ sessionTimeout: srv.sessionTimeout ?? SERVER_DEFAULTS.sessionTimeout,
259
+ modelsDir: path_1.default.resolve(srv.modelsDir ?? SERVER_DEFAULTS.modelsDir),
260
+ corsOrigins: srv.corsOrigins,
261
+ model: serverModel,
262
+ embedding: serverEmbedding,
263
+ embeddingApi: srv.embeddingApi ? { enabled: !!srv.embeddingApi.enabled, apiKey: srv.embeddingApi.apiKey, maxTexts: srv.embeddingApi.maxTexts ?? 100, maxTextChars: srv.embeddingApi.maxTextChars ?? 10_000 } : undefined,
264
+ defaultAccess: srv.defaultAccess ?? SERVER_DEFAULTS.defaultAccess,
265
+ access: srv.access ?? undefined,
266
+ jwtSecret: srv.jwtSecret,
267
+ accessTokenTtl: srv.accessTokenTtl ?? SERVER_DEFAULTS.accessTokenTtl,
268
+ refreshTokenTtl: srv.refreshTokenTtl ?? SERVER_DEFAULTS.refreshTokenTtl,
269
+ rateLimit: {
270
+ global: srv.rateLimit?.global ?? RATE_LIMIT_DEFAULTS.global,
271
+ search: srv.rateLimit?.search ?? RATE_LIMIT_DEFAULTS.search,
272
+ auth: srv.rateLimit?.auth ?? RATE_LIMIT_DEFAULTS.auth,
273
+ },
274
+ maxFileSize: srv.maxFileSize ?? SERVER_DEFAULTS.maxFileSize,
275
+ exclude: [...SERVER_DEFAULTS.exclude, ...parseExclude(srv.exclude)],
276
+ };
277
+ // Users
278
+ const users = {};
279
+ if (validated.users) {
280
+ for (const [id, raw] of Object.entries(validated.users)) {
281
+ users[id] = { name: raw.name, email: raw.email, apiKey: raw.apiKey, passwordHash: raw.passwordHash };
282
+ }
283
+ }
284
+ const projects = new Map();
285
+ for (const [id, raw] of Object.entries(validated.projects)) {
286
+ const projectDir = path_1.default.resolve(raw.projectDir);
287
+ const graphMemory = raw.graphMemory
288
+ ? path_1.default.resolve(projectDir, raw.graphMemory)
289
+ : path_1.default.join(projectDir, '.graph-memory');
290
+ const projectModel = resolveModel(raw.model, serverModel);
291
+ const projectEmbedding = resolveEmbedding(raw.embedding, serverEmbedding);
292
+ // Exclude accumulates: server + project
293
+ const projectExclude = [...server.exclude, ...parseExclude(raw.exclude)];
294
+ const rawGraphs = raw.graphs ?? {};
295
+ const graphConfigs = {};
296
+ for (const gn of exports.GRAPH_NAMES) {
297
+ const gc = rawGraphs[gn];
298
+ // Exclude accumulates: server + project + graph
299
+ const graphExclude = [...projectExclude, ...parseExclude(gc?.exclude)];
300
+ graphConfigs[gn] = {
301
+ enabled: gc?.enabled ?? true,
302
+ include: gc?.include ?? (gn === 'docs' ? PROJECT_DEFAULTS.docsInclude : gn === 'code' ? PROJECT_DEFAULTS.codeInclude : undefined),
303
+ exclude: graphExclude,
304
+ model: resolveModel(gc?.model, projectModel),
305
+ embedding: resolveEmbedding(gc?.embedding, projectEmbedding),
306
+ access: gc?.access ?? undefined,
307
+ };
308
+ }
309
+ projects.set(id, {
310
+ projectDir,
311
+ graphMemory,
312
+ exclude: projectExclude,
313
+ chunkDepth: raw.chunkDepth ?? PROJECT_DEFAULTS.chunkDepth,
314
+ maxFileSize: raw.maxFileSize ?? -1,
315
+ model: projectModel,
316
+ embedding: projectEmbedding,
317
+ graphConfigs,
318
+ author: raw.author ?? globalAuthor,
319
+ access: raw.access ?? undefined,
320
+ });
321
+ }
322
+ // --- Workspaces ---
323
+ const workspaces = new Map();
324
+ if (validated.workspaces) {
325
+ for (const [wsId, raw] of Object.entries(validated.workspaces)) {
326
+ for (const projId of raw.projects) {
327
+ if (!projects.has(projId)) {
328
+ throw new Error(`Workspace "${wsId}" references unknown project "${projId}"`);
329
+ }
330
+ }
331
+ const firstProject = projects.get(raw.projects[0]);
332
+ const graphMemory = raw.graphMemory
333
+ ? path_1.default.resolve(raw.graphMemory)
334
+ : path_1.default.join(firstProject.projectDir, '.graph-memory', 'workspace');
335
+ const mirrorDir = raw.mirrorDir
336
+ ? path_1.default.resolve(raw.mirrorDir)
337
+ : graphMemory;
338
+ const wsModel = resolveModel(raw.model, serverModel);
339
+ const wsEmbedding = resolveEmbedding(raw.embedding, serverEmbedding);
340
+ // Exclude accumulates: server + workspace
341
+ const wsExclude = [...server.exclude, ...parseExclude(raw.exclude)];
342
+ const rawGraphs = raw.graphs ?? {};
343
+ const WS_GRAPH_NAMES = ['knowledge', 'tasks', 'skills'];
344
+ const graphConfigs = {};
345
+ for (const gn of WS_GRAPH_NAMES) {
346
+ const gc = rawGraphs[gn];
347
+ graphConfigs[gn] = {
348
+ enabled: gc?.enabled ?? true,
349
+ exclude: [...wsExclude, ...parseExclude(gc?.exclude)],
350
+ model: resolveModel(gc?.model, wsModel),
351
+ embedding: resolveEmbedding(gc?.embedding, wsEmbedding),
352
+ access: gc?.access ?? undefined,
353
+ };
354
+ }
355
+ workspaces.set(wsId, {
356
+ projects: raw.projects,
357
+ graphMemory,
358
+ mirrorDir,
359
+ model: wsModel,
360
+ embedding: wsEmbedding,
361
+ graphConfigs,
362
+ author: raw.author ?? globalAuthor,
363
+ access: raw.access ?? undefined,
364
+ maxFileSize: raw.maxFileSize,
365
+ exclude: wsExclude,
366
+ });
367
+ }
368
+ }
369
+ // --- Post-parse: inject workspace-level settings into member projects ---
370
+ const wsForProject = new Map();
371
+ for (const ws of workspaces.values()) {
372
+ for (const pid of ws.projects)
373
+ wsForProject.set(pid, ws);
374
+ }
375
+ for (const [pid, proj] of projects) {
376
+ const ws = wsForProject.get(pid);
377
+ // maxFileSize: project → workspace → server
378
+ if (proj.maxFileSize === -1) {
379
+ proj.maxFileSize = ws?.maxFileSize ?? server.maxFileSize;
380
+ }
381
+ // Inject workspace excludes into project + graph excludes
382
+ if (ws) {
383
+ const wsExtra = ws.exclude.filter(e => !server.exclude.includes(e)); // ws-only patterns
384
+ if (wsExtra.length > 0) {
385
+ proj.exclude = [...proj.exclude, ...wsExtra];
386
+ for (const gn of exports.GRAPH_NAMES) {
387
+ proj.graphConfigs[gn].exclude = [...proj.graphConfigs[gn].exclude, ...wsExtra];
388
+ }
389
+ }
390
+ }
391
+ }
392
+ return { author: globalAuthor, server, users, projects, workspaces };
393
+ }
@@ -0,0 +1,214 @@
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.parseCodeFile = parseCodeFile;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const languages_1 = require("../../lib/parsers/languages");
10
+ const file_lang_1 = require("../../graphs/file-lang");
11
+ // ---------------------------------------------------------------------------
12
+ // Import resolution — replaces ts-morph's getModuleSpecifierSourceFile()
13
+ // ---------------------------------------------------------------------------
14
+ const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs'];
15
+ /** Resolve a relative import specifier to an absolute file path, or null. */
16
+ function resolveRelativeImport(fromFile, specifier) {
17
+ const dir = path_1.default.dirname(fromFile);
18
+ const base = path_1.default.resolve(dir, specifier);
19
+ // Exact match (e.g. './foo.ts')
20
+ if (hasFile(base))
21
+ return base;
22
+ // Try adding extensions (e.g. './foo' → './foo.ts')
23
+ for (const ext of RESOLVE_EXTENSIONS) {
24
+ const candidate = base + ext;
25
+ if (hasFile(candidate))
26
+ return candidate;
27
+ }
28
+ // Try index files (e.g. './foo' → './foo/index.ts')
29
+ for (const ext of RESOLVE_EXTENSIONS) {
30
+ const candidate = path_1.default.join(base, 'index' + ext);
31
+ if (hasFile(candidate))
32
+ return candidate;
33
+ }
34
+ return null;
35
+ }
36
+ function hasFile(p) {
37
+ try {
38
+ return fs_1.default.statSync(p).isFile();
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ // ---------------------------------------------------------------------------
45
+ // Main parser
46
+ // ---------------------------------------------------------------------------
47
+ async function parseCodeFile(absolutePath, codeDir, mtime) {
48
+ const fileId = path_1.default.relative(codeDir, absolutePath);
49
+ // Determine language from file extension
50
+ const ext = path_1.default.extname(absolutePath);
51
+ const language = (0, file_lang_1.getLanguage)(ext);
52
+ if (!language || !(0, languages_1.isLanguageSupported)(language)) {
53
+ // Unsupported language — return file-only node, no symbols
54
+ return {
55
+ fileId,
56
+ mtime,
57
+ nodes: [{
58
+ id: fileId,
59
+ attrs: makeFileAttrs(fileId, '', '', 1, mtime),
60
+ }],
61
+ edges: [],
62
+ };
63
+ }
64
+ const source = fs_1.default.readFileSync(absolutePath, 'utf-8');
65
+ const rootNode = await (0, languages_1.parseSource)(source, language);
66
+ if (!rootNode) {
67
+ return {
68
+ fileId,
69
+ mtime,
70
+ nodes: [{
71
+ id: fileId,
72
+ attrs: makeFileAttrs(fileId, '', '', 1, mtime),
73
+ }],
74
+ edges: [],
75
+ };
76
+ }
77
+ const mapper = (0, languages_1.getMapper)(language);
78
+ const symbols = mapper.extractSymbols(rootNode);
79
+ const edgeInfos = mapper.extractEdges(rootNode);
80
+ const imports = mapper.extractImports(rootNode);
81
+ const nodes = [];
82
+ const edges = [];
83
+ 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
+ nodes.push({
89
+ id: fileNodeId,
90
+ attrs: makeFileAttrs(fileId, fileDocComment, importSummary, lastLine, mtime),
91
+ });
92
+ // --- Symbols ---
93
+ for (const sym of symbols) {
94
+ if (!sym.name)
95
+ continue;
96
+ const symbolId = makeId(fileId, sym.name);
97
+ nodes.push({
98
+ id: symbolId,
99
+ attrs: {
100
+ kind: sym.kind,
101
+ fileId,
102
+ name: sym.name,
103
+ signature: sym.signature,
104
+ docComment: sym.docComment,
105
+ body: sym.body,
106
+ startLine: sym.startLine,
107
+ endLine: sym.endLine,
108
+ isExported: sym.isExported,
109
+ embedding: [],
110
+ fileEmbedding: [],
111
+ mtime,
112
+ },
113
+ });
114
+ edges.push({ from: fileNodeId, to: symbolId, attrs: { kind: 'contains' } });
115
+ // Child symbols (e.g. methods)
116
+ if (sym.children) {
117
+ for (const child of sym.children) {
118
+ if (!child.name)
119
+ continue;
120
+ const childId = makeId(fileId, sym.name, child.name);
121
+ nodes.push({
122
+ id: childId,
123
+ attrs: {
124
+ kind: child.kind,
125
+ fileId,
126
+ name: child.name,
127
+ signature: child.signature,
128
+ docComment: child.docComment,
129
+ body: child.body,
130
+ startLine: child.startLine,
131
+ endLine: child.endLine,
132
+ isExported: child.isExported,
133
+ embedding: [],
134
+ fileEmbedding: [],
135
+ mtime,
136
+ },
137
+ });
138
+ edges.push({ from: symbolId, to: childId, attrs: { kind: 'contains' } });
139
+ }
140
+ }
141
+ }
142
+ // --- Extends / implements edges ---
143
+ for (const edge of edgeInfos) {
144
+ const fromId = makeId(fileId, edge.fromName);
145
+ const toId = makeId(fileId, edge.toName);
146
+ edges.push({ from: fromId, to: toId, attrs: { kind: edge.kind } });
147
+ }
148
+ // --- Import edges: file → imported file ---
149
+ for (const imp of imports) {
150
+ const targetAbsolute = resolveRelativeImport(absolutePath, imp.specifier);
151
+ if (!targetAbsolute)
152
+ continue;
153
+ const targetRel = path_1.default.relative(codeDir, targetAbsolute);
154
+ if (targetRel.startsWith('..') || path_1.default.isAbsolute(targetRel))
155
+ continue;
156
+ const targetFileId = path_1.default.relative(codeDir, targetAbsolute);
157
+ if (targetFileId !== fileNodeId) {
158
+ edges.push({
159
+ from: fileNodeId,
160
+ to: targetFileId,
161
+ attrs: { kind: 'imports' },
162
+ });
163
+ }
164
+ }
165
+ return { fileId, mtime, nodes, edges };
166
+ }
167
+ // ---------------------------------------------------------------------------
168
+ // Helpers
169
+ // ---------------------------------------------------------------------------
170
+ function makeId(fileId, ...parts) {
171
+ return [fileId, ...parts].join('::');
172
+ }
173
+ function makeFileAttrs(fileId, docComment, importSummary, lastLine, mtime) {
174
+ return {
175
+ kind: 'file',
176
+ fileId,
177
+ name: path_1.default.basename(fileId),
178
+ signature: fileId,
179
+ docComment,
180
+ body: importSummary,
181
+ startLine: 1,
182
+ endLine: lastLine,
183
+ isExported: false,
184
+ embedding: [],
185
+ fileEmbedding: [],
186
+ mtime,
187
+ };
188
+ }
189
+ /**
190
+ * Extract the file-level doc comment (first JSDoc comment before any declaration).
191
+ */
192
+ function extractFileDocComment(rootNode) {
193
+ for (const child of rootNode.children) {
194
+ if (child.type === 'comment' && child.text.startsWith('/**')) {
195
+ return child.text.trim();
196
+ }
197
+ // Stop at first non-comment node
198
+ if (child.type !== 'comment')
199
+ break;
200
+ }
201
+ return '';
202
+ }
203
+ /**
204
+ * Build a summary of import statements.
205
+ */
206
+ function buildImportSummary(rootNode) {
207
+ const imports = [];
208
+ for (const child of rootNode.children) {
209
+ if (child.type === 'import_statement') {
210
+ imports.push(child.text.trim());
211
+ }
212
+ }
213
+ return imports.join('\n');
214
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractSymbols = extractSymbols;
4
+ const languages_1 = require("../../lib/parsers/languages");
5
+ /** Map of common code fence language tags to language names used by the registry. */
6
+ const TAG_TO_LANGUAGE = {
7
+ ts: 'typescript',
8
+ typescript: 'typescript',
9
+ tsx: 'typescript',
10
+ js: 'javascript',
11
+ javascript: 'javascript',
12
+ jsx: 'javascript',
13
+ };
14
+ /**
15
+ * Extract top-level symbol names from a code block using tree-sitter.
16
+ * Returns [] for unsupported languages or on parse failure.
17
+ */
18
+ async function extractSymbols(code, language) {
19
+ const lang = TAG_TO_LANGUAGE[language.toLowerCase()];
20
+ if (!lang || !(0, languages_1.isLanguageSupported)(lang))
21
+ return [];
22
+ try {
23
+ const rootNode = await (0, languages_1.parseSource)(code, lang);
24
+ if (!rootNode)
25
+ return [];
26
+ const mapper = (0, languages_1.getMapper)(lang);
27
+ const symbols = mapper.extractSymbols(rootNode);
28
+ return symbols.map(s => s.name).filter(Boolean);
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }