@graphmemory/server 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.js +4 -4
- package/dist/api/rest/code.js +2 -1
- package/dist/api/rest/docs.js +2 -1
- package/dist/api/rest/embed.js +8 -1
- package/dist/api/rest/index.js +4 -3
- package/dist/api/rest/knowledge.js +4 -2
- package/dist/api/rest/skills.js +2 -1
- package/dist/api/rest/tasks.js +2 -1
- package/dist/api/rest/validation.js +41 -40
- package/dist/api/rest/websocket.js +24 -7
- package/dist/api/tools/knowledge/add-attachment.js +2 -1
- package/dist/api/tools/skills/add-attachment.js +2 -1
- package/dist/api/tools/tasks/add-attachment.js +2 -1
- package/dist/cli/index.js +5 -4
- package/dist/cli/indexer.js +2 -1
- package/dist/graphs/attachment-types.js +5 -0
- package/dist/graphs/code.js +34 -8
- package/dist/graphs/docs.js +5 -3
- package/dist/graphs/file-index.js +5 -3
- package/dist/graphs/knowledge.js +11 -4
- package/dist/graphs/skill.js +12 -5
- package/dist/graphs/task.js +12 -5
- package/dist/lib/defaults.js +78 -0
- package/dist/lib/embedder.js +11 -12
- package/dist/lib/embedding-codec.js +3 -5
- package/dist/lib/graph-persistence.js +68 -0
- package/dist/lib/mirror-watcher.js +4 -3
- package/dist/lib/parsers/docs.js +2 -1
- package/dist/lib/parsers/languages/typescript.js +34 -17
- package/dist/lib/project-manager.js +7 -1
- package/dist/lib/search/bm25.js +5 -4
- package/dist/lib/search/code.js +2 -1
- package/dist/lib/search/docs.js +2 -1
- package/dist/lib/search/file-index.js +2 -1
- package/dist/lib/search/files.js +3 -2
- package/dist/lib/search/knowledge.js +2 -1
- package/dist/lib/search/skills.js +2 -1
- package/dist/lib/search/tasks.js +2 -1
- package/package.json +5 -2
package/dist/api/index.js
CHANGED
|
@@ -47,6 +47,7 @@ const embedder_1 = require("../lib/embedder");
|
|
|
47
47
|
const index_1 = require("../api/rest/index");
|
|
48
48
|
const websocket_1 = require("../api/rest/websocket");
|
|
49
49
|
const access_1 = require("../lib/access");
|
|
50
|
+
const defaults_1 = require("../lib/defaults");
|
|
50
51
|
const multi_config_1 = require("../lib/multi-config");
|
|
51
52
|
const docs_1 = require("../graphs/docs");
|
|
52
53
|
const code_1 = require("../graphs/code");
|
|
@@ -314,13 +315,12 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
|
|
|
314
315
|
// ---------------------------------------------------------------------------
|
|
315
316
|
// HTTP transport (Streamable HTTP)
|
|
316
317
|
// ---------------------------------------------------------------------------
|
|
317
|
-
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
318
318
|
async function collectBody(req) {
|
|
319
319
|
const chunks = [];
|
|
320
320
|
let size = 0;
|
|
321
321
|
for await (const chunk of req) {
|
|
322
322
|
size += chunk.length;
|
|
323
|
-
if (size > MAX_BODY_SIZE)
|
|
323
|
+
if (size > defaults_1.MAX_BODY_SIZE)
|
|
324
324
|
throw new Error('Request body too large');
|
|
325
325
|
chunks.push(chunk);
|
|
326
326
|
}
|
|
@@ -338,7 +338,7 @@ async function startHttpServer(host, port, sessionTimeoutMs, docGraph, codeGraph
|
|
|
338
338
|
process.stderr.write(`[http] Session ${sid} timed out\n`);
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
|
-
},
|
|
341
|
+
}, defaults_1.SESSION_SWEEP_INTERVAL_MS);
|
|
342
342
|
sweepInterval.unref();
|
|
343
343
|
const httpServer = http_1.default.createServer(async (req, res) => {
|
|
344
344
|
try {
|
|
@@ -401,7 +401,7 @@ async function startMultiProjectHttpServer(host, port, sessionTimeoutMs, project
|
|
|
401
401
|
process.stderr.write(`[http] Session ${sid} (project: ${s.projectId}) timed out\n`);
|
|
402
402
|
}
|
|
403
403
|
}
|
|
404
|
-
},
|
|
404
|
+
}, defaults_1.SESSION_SWEEP_INTERVAL_MS);
|
|
405
405
|
sweepInterval.unref();
|
|
406
406
|
// Express app handles /api/* routes
|
|
407
407
|
const restApp = (0, index_1.createRestApp)(projectManager, restOptions);
|
package/dist/api/rest/code.js
CHANGED
|
@@ -50,7 +50,8 @@ function createCodeRouter() {
|
|
|
50
50
|
const symbol = p.codeManager.getSymbol(symbolId);
|
|
51
51
|
if (!symbol)
|
|
52
52
|
return res.status(404).json({ error: 'Symbol not found' });
|
|
53
|
-
|
|
53
|
+
const { embedding: _, fileEmbedding: _fe, ...rest } = symbol;
|
|
54
|
+
res.json(rest);
|
|
54
55
|
}
|
|
55
56
|
catch (err) {
|
|
56
57
|
next(err);
|
package/dist/api/rest/docs.js
CHANGED
|
@@ -52,7 +52,8 @@ function createDocsRouter() {
|
|
|
52
52
|
const node = p.docManager.getNode(nodeId);
|
|
53
53
|
if (!node)
|
|
54
54
|
return res.status(404).json({ error: 'Node not found' });
|
|
55
|
-
|
|
55
|
+
const { embedding: _, ...rest } = node;
|
|
56
|
+
res.json(rest);
|
|
56
57
|
}
|
|
57
58
|
catch (err) {
|
|
58
59
|
next(err);
|
package/dist/api/rest/embed.js
CHANGED
|
@@ -8,6 +8,7 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
8
8
|
const express_1 = require("express");
|
|
9
9
|
const zod_1 = require("zod");
|
|
10
10
|
const embedder_1 = require("../../lib/embedder");
|
|
11
|
+
const embedding_codec_1 = require("../../lib/embedding-codec");
|
|
11
12
|
/**
|
|
12
13
|
* Create an Express router for the embedding API.
|
|
13
14
|
* POST /api/embed — embed texts using the server's embedding model.
|
|
@@ -16,6 +17,7 @@ function createEmbedRouter(apiConfig, modelName) {
|
|
|
16
17
|
const router = (0, express_1.Router)();
|
|
17
18
|
const embedRequestSchema = zod_1.z.object({
|
|
18
19
|
texts: zod_1.z.array(zod_1.z.string().max(apiConfig.maxTextChars)).min(1).max(apiConfig.maxTexts),
|
|
20
|
+
format: zod_1.z.enum(['json', 'base64']).optional().default('json'),
|
|
19
21
|
});
|
|
20
22
|
router.post('/', async (req, res, next) => {
|
|
21
23
|
try {
|
|
@@ -34,7 +36,12 @@ function createEmbedRouter(apiConfig, modelName) {
|
|
|
34
36
|
const parsed = embedRequestSchema.parse(req.body);
|
|
35
37
|
const inputs = parsed.texts.map(text => ({ title: text, content: '' }));
|
|
36
38
|
const embeddings = await (0, embedder_1.embedBatch)(inputs, modelName);
|
|
37
|
-
|
|
39
|
+
if (parsed.format === 'base64') {
|
|
40
|
+
res.json({ embeddings: embeddings.map(e => (0, embedding_codec_1.float32ToBase64)(e)), format: 'base64' });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
res.json({ embeddings });
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
catch (err) {
|
|
40
47
|
if (err?.name === 'ZodError') {
|
package/dist/api/rest/index.js
CHANGED
|
@@ -24,6 +24,7 @@ const graph_1 = require("../../api/rest/graph");
|
|
|
24
24
|
const tools_1 = require("../../api/rest/tools");
|
|
25
25
|
const embed_1 = require("../../api/rest/embed");
|
|
26
26
|
const team_1 = require("../../lib/team");
|
|
27
|
+
const defaults_1 = require("../../lib/defaults");
|
|
27
28
|
/**
|
|
28
29
|
* Express middleware: reject if accessLevel (set by requireGraphAccess) is not 'rw'.
|
|
29
30
|
* Use on POST/PUT/DELETE routes inside domain routers.
|
|
@@ -59,10 +60,10 @@ function createRestApp(projectManager, options) {
|
|
|
59
60
|
const rl = serverConfig?.rateLimit;
|
|
60
61
|
const rateLimitMsg = { error: 'Too many requests, please try again later' };
|
|
61
62
|
if (rl && rl.global > 0) {
|
|
62
|
-
app.use('/api/', (0, express_rate_limit_1.default)({ windowMs:
|
|
63
|
+
app.use('/api/', (0, express_rate_limit_1.default)({ windowMs: defaults_1.RATE_LIMIT_WINDOW_MS, max: rl.global, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg }));
|
|
63
64
|
}
|
|
64
65
|
if (rl && rl.search > 0) {
|
|
65
|
-
const searchLimiter = (0, express_rate_limit_1.default)({ windowMs:
|
|
66
|
+
const searchLimiter = (0, express_rate_limit_1.default)({ windowMs: defaults_1.RATE_LIMIT_WINDOW_MS, max: rl.search, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg });
|
|
66
67
|
app.use('/api/projects/:projectId/knowledge/search', searchLimiter);
|
|
67
68
|
app.use('/api/projects/:projectId/tasks/search', searchLimiter);
|
|
68
69
|
app.use('/api/projects/:projectId/skills/search', searchLimiter);
|
|
@@ -72,7 +73,7 @@ function createRestApp(projectManager, options) {
|
|
|
72
73
|
app.use('/api/embed', searchLimiter);
|
|
73
74
|
}
|
|
74
75
|
if (rl && rl.auth > 0) {
|
|
75
|
-
app.use('/api/auth/login', (0, express_rate_limit_1.default)({ windowMs:
|
|
76
|
+
app.use('/api/auth/login', (0, express_rate_limit_1.default)({ windowMs: defaults_1.RATE_LIMIT_WINDOW_MS, max: rl.auth, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg }));
|
|
76
77
|
}
|
|
77
78
|
const jwtSecret = serverConfig?.jwtSecret;
|
|
78
79
|
const accessTokenTtl = serverConfig?.accessTokenTtl ?? '15m';
|
|
@@ -11,7 +11,8 @@ const multer_1 = __importDefault(require("multer"));
|
|
|
11
11
|
const validation_1 = require("../../api/rest/validation");
|
|
12
12
|
const index_1 = require("../../api/rest/index");
|
|
13
13
|
const manager_types_1 = require("../../graphs/manager-types");
|
|
14
|
-
const
|
|
14
|
+
const defaults_1 = require("../../lib/defaults");
|
|
15
|
+
const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: defaults_1.MAX_UPLOAD_SIZE } });
|
|
15
16
|
function createKnowledgeRouter() {
|
|
16
17
|
const router = (0, express_1.Router)({ mergeParams: true });
|
|
17
18
|
function getProject(req) {
|
|
@@ -52,7 +53,8 @@ function createKnowledgeRouter() {
|
|
|
52
53
|
const note = p.knowledgeManager.getNote(req.params.noteId);
|
|
53
54
|
if (!note)
|
|
54
55
|
return res.status(404).json({ error: 'Note not found' });
|
|
55
|
-
|
|
56
|
+
const { embedding: _, ...rest } = note;
|
|
57
|
+
res.json(rest);
|
|
56
58
|
}
|
|
57
59
|
catch (err) {
|
|
58
60
|
next(err);
|
package/dist/api/rest/skills.js
CHANGED
|
@@ -11,7 +11,8 @@ const multer_1 = __importDefault(require("multer"));
|
|
|
11
11
|
const validation_1 = require("../../api/rest/validation");
|
|
12
12
|
const index_1 = require("../../api/rest/index");
|
|
13
13
|
const manager_types_1 = require("../../graphs/manager-types");
|
|
14
|
-
const
|
|
14
|
+
const defaults_1 = require("../../lib/defaults");
|
|
15
|
+
const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: defaults_1.MAX_UPLOAD_SIZE } });
|
|
15
16
|
function createSkillsRouter() {
|
|
16
17
|
const router = (0, express_1.Router)({ mergeParams: true });
|
|
17
18
|
function getProject(req) {
|
package/dist/api/rest/tasks.js
CHANGED
|
@@ -11,7 +11,8 @@ const multer_1 = __importDefault(require("multer"));
|
|
|
11
11
|
const validation_1 = require("../../api/rest/validation");
|
|
12
12
|
const index_1 = require("../../api/rest/index");
|
|
13
13
|
const manager_types_1 = require("../../graphs/manager-types");
|
|
14
|
-
const
|
|
14
|
+
const defaults_1 = require("../../lib/defaults");
|
|
15
|
+
const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: defaults_1.MAX_UPLOAD_SIZE } });
|
|
15
16
|
function createTasksRouter() {
|
|
16
17
|
const router = (0, express_1.Router)({ mergeParams: true });
|
|
17
18
|
function getProject(req) {
|
|
@@ -4,6 +4,7 @@ exports.attachmentFilenameSchema = exports.linkedQuerySchema = exports.graphExpo
|
|
|
4
4
|
exports.validateBody = validateBody;
|
|
5
5
|
exports.validateQuery = validateQuery;
|
|
6
6
|
const zod_1 = require("zod");
|
|
7
|
+
const defaults_1 = require("../../lib/defaults");
|
|
7
8
|
function validateBody(schema) {
|
|
8
9
|
return (req, _res, next) => {
|
|
9
10
|
req.body = schema.parse(req.body);
|
|
@@ -20,14 +21,14 @@ function validateQuery(schema) {
|
|
|
20
21
|
// Knowledge schemas
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
exports.createNoteSchema = zod_1.z.object({
|
|
23
|
-
title: zod_1.z.string().min(1).max(
|
|
24
|
-
content: zod_1.z.string().max(
|
|
25
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
24
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN),
|
|
25
|
+
content: zod_1.z.string().max(defaults_1.MAX_NOTE_CONTENT_LEN),
|
|
26
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional().default([]),
|
|
26
27
|
});
|
|
27
28
|
exports.updateNoteSchema = zod_1.z.object({
|
|
28
|
-
title: zod_1.z.string().min(1).max(
|
|
29
|
-
content: zod_1.z.string().max(
|
|
30
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
29
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN).optional(),
|
|
30
|
+
content: zod_1.z.string().max(defaults_1.MAX_NOTE_CONTENT_LEN).optional(),
|
|
31
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional(),
|
|
31
32
|
version: zod_1.z.number().int().positive().optional(),
|
|
32
33
|
});
|
|
33
34
|
exports.createRelationSchema = zod_1.z.object({
|
|
@@ -38,8 +39,8 @@ exports.createRelationSchema = zod_1.z.object({
|
|
|
38
39
|
projectId: zod_1.z.string().min(1).optional(),
|
|
39
40
|
});
|
|
40
41
|
exports.noteSearchSchema = zod_1.z.object({
|
|
41
|
-
q: zod_1.z.string().min(1).max(
|
|
42
|
-
topK: zod_1.z.coerce.number().int().positive().max(
|
|
42
|
+
q: zod_1.z.string().min(1).max(defaults_1.MAX_SEARCH_QUERY_LEN),
|
|
43
|
+
topK: zod_1.z.coerce.number().int().positive().max(defaults_1.MAX_SEARCH_TOP_K).optional(),
|
|
43
44
|
minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
|
|
44
45
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
|
|
45
46
|
});
|
|
@@ -52,24 +53,24 @@ exports.noteListSchema = zod_1.z.object({
|
|
|
52
53
|
// Task schemas
|
|
53
54
|
// ---------------------------------------------------------------------------
|
|
54
55
|
exports.createTaskSchema = zod_1.z.object({
|
|
55
|
-
title: zod_1.z.string().min(1).max(
|
|
56
|
-
description: zod_1.z.string().max(
|
|
56
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN),
|
|
57
|
+
description: zod_1.z.string().max(defaults_1.MAX_DESCRIPTION_LEN).default(''),
|
|
57
58
|
status: zod_1.z.enum(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled']).default('todo'),
|
|
58
59
|
priority: zod_1.z.enum(['critical', 'high', 'medium', 'low']).default('medium'),
|
|
59
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
60
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional().default([]),
|
|
60
61
|
dueDate: zod_1.z.number().nullable().optional(),
|
|
61
62
|
estimate: zod_1.z.number().nullable().optional(),
|
|
62
|
-
assignee: zod_1.z.string().max(
|
|
63
|
+
assignee: zod_1.z.string().max(defaults_1.MAX_ASSIGNEE_LEN).nullable().optional(),
|
|
63
64
|
});
|
|
64
65
|
exports.updateTaskSchema = zod_1.z.object({
|
|
65
|
-
title: zod_1.z.string().min(1).max(
|
|
66
|
-
description: zod_1.z.string().max(
|
|
66
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN).optional(),
|
|
67
|
+
description: zod_1.z.string().max(defaults_1.MAX_DESCRIPTION_LEN).optional(),
|
|
67
68
|
status: zod_1.z.enum(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled']).optional(),
|
|
68
69
|
priority: zod_1.z.enum(['critical', 'high', 'medium', 'low']).optional(),
|
|
69
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
70
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional(),
|
|
70
71
|
dueDate: zod_1.z.number().nullable().optional(),
|
|
71
72
|
estimate: zod_1.z.number().nullable().optional(),
|
|
72
|
-
assignee: zod_1.z.string().max(
|
|
73
|
+
assignee: zod_1.z.string().max(defaults_1.MAX_ASSIGNEE_LEN).nullable().optional(),
|
|
73
74
|
version: zod_1.z.number().int().positive().optional(),
|
|
74
75
|
});
|
|
75
76
|
exports.moveTaskSchema = zod_1.z.object({
|
|
@@ -84,8 +85,8 @@ exports.createTaskLinkSchema = zod_1.z.object({
|
|
|
84
85
|
projectId: zod_1.z.string().min(1).optional(),
|
|
85
86
|
});
|
|
86
87
|
exports.taskSearchSchema = zod_1.z.object({
|
|
87
|
-
q: zod_1.z.string().min(1).max(
|
|
88
|
-
topK: zod_1.z.coerce.number().int().positive().max(
|
|
88
|
+
q: zod_1.z.string().min(1).max(defaults_1.MAX_SEARCH_QUERY_LEN),
|
|
89
|
+
topK: zod_1.z.coerce.number().int().positive().max(defaults_1.MAX_SEARCH_TOP_K).optional(),
|
|
89
90
|
minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
|
|
90
91
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
|
|
91
92
|
});
|
|
@@ -101,8 +102,8 @@ exports.taskListSchema = zod_1.z.object({
|
|
|
101
102
|
// Search schemas (docs, code, files)
|
|
102
103
|
// ---------------------------------------------------------------------------
|
|
103
104
|
exports.searchQuerySchema = zod_1.z.object({
|
|
104
|
-
q: zod_1.z.string().min(1).max(
|
|
105
|
-
topK: zod_1.z.coerce.number().int().positive().max(
|
|
105
|
+
q: zod_1.z.string().min(1).max(defaults_1.MAX_SEARCH_QUERY_LEN),
|
|
106
|
+
topK: zod_1.z.coerce.number().int().positive().max(defaults_1.MAX_SEARCH_TOP_K).optional(),
|
|
106
107
|
minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
|
|
107
108
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
|
|
108
109
|
});
|
|
@@ -127,24 +128,24 @@ exports.fileListSchema = zod_1.z.object({
|
|
|
127
128
|
// Skill schemas
|
|
128
129
|
// ---------------------------------------------------------------------------
|
|
129
130
|
exports.createSkillSchema = zod_1.z.object({
|
|
130
|
-
title: zod_1.z.string().min(1).max(
|
|
131
|
-
description: zod_1.z.string().max(
|
|
132
|
-
steps: zod_1.z.array(zod_1.z.string().max(
|
|
133
|
-
triggers: zod_1.z.array(zod_1.z.string().max(
|
|
134
|
-
inputHints: zod_1.z.array(zod_1.z.string().max(
|
|
135
|
-
filePatterns: zod_1.z.array(zod_1.z.string().max(
|
|
136
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
131
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN),
|
|
132
|
+
description: zod_1.z.string().max(defaults_1.MAX_DESCRIPTION_LEN).default(''),
|
|
133
|
+
steps: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_STEP_LEN)).max(defaults_1.MAX_SKILL_STEPS_COUNT).optional().default([]),
|
|
134
|
+
triggers: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional().default([]),
|
|
135
|
+
inputHints: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional().default([]),
|
|
136
|
+
filePatterns: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional().default([]),
|
|
137
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional().default([]),
|
|
137
138
|
source: zod_1.z.enum(['user', 'learned']).default('user'),
|
|
138
139
|
confidence: zod_1.z.number().min(0).max(1).default(1),
|
|
139
140
|
});
|
|
140
141
|
exports.updateSkillSchema = zod_1.z.object({
|
|
141
|
-
title: zod_1.z.string().min(1).max(
|
|
142
|
-
description: zod_1.z.string().max(
|
|
143
|
-
steps: zod_1.z.array(zod_1.z.string().max(
|
|
144
|
-
triggers: zod_1.z.array(zod_1.z.string().max(
|
|
145
|
-
inputHints: zod_1.z.array(zod_1.z.string().max(
|
|
146
|
-
filePatterns: zod_1.z.array(zod_1.z.string().max(
|
|
147
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
142
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN).optional(),
|
|
143
|
+
description: zod_1.z.string().max(defaults_1.MAX_DESCRIPTION_LEN).optional(),
|
|
144
|
+
steps: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_STEP_LEN)).max(defaults_1.MAX_SKILL_STEPS_COUNT).optional(),
|
|
145
|
+
triggers: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional(),
|
|
146
|
+
inputHints: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional(),
|
|
147
|
+
filePatterns: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional(),
|
|
148
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional(),
|
|
148
149
|
source: zod_1.z.enum(['user', 'learned']).optional(),
|
|
149
150
|
confidence: zod_1.z.number().min(0).max(1).optional(),
|
|
150
151
|
version: zod_1.z.number().int().positive().optional(),
|
|
@@ -157,8 +158,8 @@ exports.createSkillLinkSchema = zod_1.z.object({
|
|
|
157
158
|
projectId: zod_1.z.string().min(1).optional(),
|
|
158
159
|
});
|
|
159
160
|
exports.skillSearchSchema = zod_1.z.object({
|
|
160
|
-
q: zod_1.z.string().min(1).max(
|
|
161
|
-
topK: zod_1.z.coerce.number().int().positive().max(
|
|
161
|
+
q: zod_1.z.string().min(1).max(defaults_1.MAX_SEARCH_QUERY_LEN),
|
|
162
|
+
topK: zod_1.z.coerce.number().int().positive().max(defaults_1.MAX_SEARCH_TOP_K).optional(),
|
|
162
163
|
minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
|
|
163
164
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
|
|
164
165
|
});
|
|
@@ -179,9 +180,9 @@ exports.graphExportSchema = zod_1.z.object({
|
|
|
179
180
|
// ---------------------------------------------------------------------------
|
|
180
181
|
exports.linkedQuerySchema = zod_1.z.object({
|
|
181
182
|
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge', 'tasks', 'skills']),
|
|
182
|
-
targetNodeId: zod_1.z.string().min(1).max(
|
|
183
|
-
kind: zod_1.z.string().max(
|
|
184
|
-
projectId: zod_1.z.string().max(
|
|
183
|
+
targetNodeId: zod_1.z.string().min(1).max(defaults_1.MAX_TARGET_NODE_ID_LEN),
|
|
184
|
+
kind: zod_1.z.string().max(defaults_1.MAX_LINK_KIND_LEN).optional(),
|
|
185
|
+
projectId: zod_1.z.string().max(defaults_1.MAX_PROJECT_ID_LEN).optional(),
|
|
185
186
|
});
|
|
186
187
|
// ---------------------------------------------------------------------------
|
|
187
188
|
// Attachment schemas
|
|
@@ -189,7 +190,7 @@ exports.linkedQuerySchema = zod_1.z.object({
|
|
|
189
190
|
/** Validates an attachment filename (path param). No path separators, no .., no dangerous chars. */
|
|
190
191
|
exports.attachmentFilenameSchema = zod_1.z.string()
|
|
191
192
|
.min(1)
|
|
192
|
-
.max(
|
|
193
|
+
.max(defaults_1.MAX_ATTACHMENT_FILENAME_LEN)
|
|
193
194
|
.refine(s => !/[/\\]/.test(s), 'Filename must not contain path separators')
|
|
194
195
|
.refine(s => !s.includes('..'), 'Filename must not contain ..')
|
|
195
196
|
.refine(s => !s.includes('\0'), 'Filename must not contain null bytes')
|
|
@@ -3,10 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.attachWebSocket = attachWebSocket;
|
|
4
4
|
const ws_1 = require("ws");
|
|
5
5
|
const jwt_1 = require("../../lib/jwt");
|
|
6
|
+
const defaults_1 = require("../../lib/defaults");
|
|
6
7
|
/**
|
|
7
8
|
* Attach a WebSocket server to the HTTP server at /api/ws.
|
|
8
9
|
* Broadcasts all ProjectManager events to connected clients.
|
|
9
10
|
* Each event includes projectId — clients filter on their side.
|
|
11
|
+
* Returns a handle with a cleanup function to remove all listeners.
|
|
10
12
|
*/
|
|
11
13
|
function attachWebSocket(httpServer, projectManager, options) {
|
|
12
14
|
const wss = new ws_1.WebSocketServer({ noServer: true });
|
|
@@ -55,7 +57,8 @@ function attachWebSocket(httpServer, projectManager, options) {
|
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
|
-
// Subscribe to ProjectManager events
|
|
60
|
+
// Subscribe to ProjectManager events (track handlers for cleanup)
|
|
61
|
+
const listeners = [];
|
|
59
62
|
const events = [
|
|
60
63
|
'note:created', 'note:updated', 'note:deleted',
|
|
61
64
|
'note:attachment:added', 'note:attachment:deleted',
|
|
@@ -66,14 +69,16 @@ function attachWebSocket(httpServer, projectManager, options) {
|
|
|
66
69
|
'project:indexed',
|
|
67
70
|
];
|
|
68
71
|
for (const eventType of events) {
|
|
69
|
-
|
|
72
|
+
const handler = (data) => {
|
|
70
73
|
broadcast({ projectId: data.projectId, type: eventType, data });
|
|
71
|
-
}
|
|
74
|
+
};
|
|
75
|
+
projectManager.on(eventType, handler);
|
|
76
|
+
listeners.push([eventType, handler]);
|
|
72
77
|
}
|
|
73
78
|
// Debounced graph:updated from indexer
|
|
74
79
|
let graphUpdateTimer;
|
|
75
80
|
let pendingGraphUpdates = new Map();
|
|
76
|
-
|
|
81
|
+
const graphHandler = (data) => {
|
|
77
82
|
const key = data.projectId;
|
|
78
83
|
if (!pendingGraphUpdates.has(key))
|
|
79
84
|
pendingGraphUpdates.set(key, []);
|
|
@@ -85,8 +90,20 @@ function attachWebSocket(httpServer, projectManager, options) {
|
|
|
85
90
|
}
|
|
86
91
|
pendingGraphUpdates = new Map();
|
|
87
92
|
graphUpdateTimer = undefined;
|
|
88
|
-
},
|
|
93
|
+
}, defaults_1.WS_DEBOUNCE_MS);
|
|
89
94
|
}
|
|
90
|
-
}
|
|
91
|
-
|
|
95
|
+
};
|
|
96
|
+
projectManager.on('graph:updated', graphHandler);
|
|
97
|
+
listeners.push(['graph:updated', graphHandler]);
|
|
98
|
+
function cleanup() {
|
|
99
|
+
for (const [event, handler] of listeners) {
|
|
100
|
+
projectManager.removeListener(event, handler);
|
|
101
|
+
}
|
|
102
|
+
if (graphUpdateTimer) {
|
|
103
|
+
clearTimeout(graphUpdateTimer);
|
|
104
|
+
graphUpdateTimer = undefined;
|
|
105
|
+
}
|
|
106
|
+
pendingGraphUpdates.clear();
|
|
107
|
+
}
|
|
108
|
+
return { wss, cleanup };
|
|
92
109
|
}
|
|
@@ -7,6 +7,7 @@ exports.register = register;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const zod_1 = require("zod");
|
|
10
|
+
const defaults_1 = require("../../../lib/defaults");
|
|
10
11
|
function register(server, mgr) {
|
|
11
12
|
server.registerTool('add_note_attachment', {
|
|
12
13
|
description: 'Attach a file to a note. Provide the absolute path to a local file. ' +
|
|
@@ -28,7 +29,7 @@ function register(server, mgr) {
|
|
|
28
29
|
if (!stat.isFile()) {
|
|
29
30
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
|
|
30
31
|
}
|
|
31
|
-
if (stat.size >
|
|
32
|
+
if (stat.size > defaults_1.MAX_UPLOAD_SIZE) {
|
|
32
33
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
|
|
33
34
|
}
|
|
34
35
|
const data = fs_1.default.readFileSync(resolved);
|
|
@@ -7,6 +7,7 @@ exports.register = register;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const zod_1 = require("zod");
|
|
10
|
+
const defaults_1 = require("../../../lib/defaults");
|
|
10
11
|
function register(server, mgr) {
|
|
11
12
|
server.registerTool('add_skill_attachment', {
|
|
12
13
|
description: 'Attach a file to a skill. Provide the absolute path to a local file. ' +
|
|
@@ -28,7 +29,7 @@ function register(server, mgr) {
|
|
|
28
29
|
if (!stat.isFile()) {
|
|
29
30
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
|
|
30
31
|
}
|
|
31
|
-
if (stat.size >
|
|
32
|
+
if (stat.size > defaults_1.MAX_UPLOAD_SIZE) {
|
|
32
33
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
|
|
33
34
|
}
|
|
34
35
|
const data = fs_1.default.readFileSync(resolved);
|
|
@@ -7,6 +7,7 @@ exports.register = register;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const zod_1 = require("zod");
|
|
10
|
+
const defaults_1 = require("../../../lib/defaults");
|
|
10
11
|
function register(server, mgr) {
|
|
11
12
|
server.registerTool('add_task_attachment', {
|
|
12
13
|
description: 'Attach a file to a task. Provide the absolute path to a local file. ' +
|
|
@@ -28,7 +29,7 @@ function register(server, mgr) {
|
|
|
28
29
|
if (!stat.isFile()) {
|
|
29
30
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
|
|
30
31
|
}
|
|
31
|
-
if (stat.size >
|
|
32
|
+
if (stat.size > defaults_1.MAX_UPLOAD_SIZE) {
|
|
32
33
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
|
|
33
34
|
}
|
|
34
35
|
const data = fs_1.default.readFileSync(resolved);
|
package/dist/cli/index.js
CHANGED
|
@@ -14,11 +14,12 @@ const jwt_1 = require("../lib/jwt");
|
|
|
14
14
|
const project_manager_1 = require("../lib/project-manager");
|
|
15
15
|
const embedder_1 = require("../lib/embedder");
|
|
16
16
|
const index_1 = require("../api/index");
|
|
17
|
+
const defaults_1 = require("../lib/defaults");
|
|
17
18
|
const program = new commander_1.Command();
|
|
18
19
|
program
|
|
19
20
|
.name('graphmemory')
|
|
20
21
|
.description('MCP server for semantic graph memory from markdown docs and source code')
|
|
21
|
-
.version('1.3.
|
|
22
|
+
.version('1.3.1');
|
|
22
23
|
const parseIntArg = (v) => parseInt(v, 10);
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Helper: load config from file, or fall back to default (cwd as single project)
|
|
@@ -211,7 +212,7 @@ program
|
|
|
211
212
|
const forceTimer = setTimeout(() => {
|
|
212
213
|
process.stderr.write('[serve] Shutdown timeout, force exit\n');
|
|
213
214
|
process.exit(1);
|
|
214
|
-
},
|
|
215
|
+
}, defaults_1.GRACEFUL_SHUTDOWN_TIMEOUT_MS);
|
|
215
216
|
try {
|
|
216
217
|
httpServer.close();
|
|
217
218
|
// Destroy all open connections (including WebSocket) so the server can close
|
|
@@ -324,8 +325,8 @@ usersCmd
|
|
|
324
325
|
process.stderr.write('Invalid email format\n');
|
|
325
326
|
process.exit(1);
|
|
326
327
|
}
|
|
327
|
-
if (password.length >
|
|
328
|
-
process.stderr.write(
|
|
328
|
+
if (password.length > defaults_1.MAX_PASSWORD_LEN) {
|
|
329
|
+
process.stderr.write(`Password too long (max ${defaults_1.MAX_PASSWORD_LEN})\n`);
|
|
329
330
|
process.exit(1);
|
|
330
331
|
}
|
|
331
332
|
const pwHash = await (0, jwt_1.hashPassword)(password);
|
package/dist/cli/indexer.js
CHANGED
|
@@ -13,6 +13,7 @@ const docs_2 = require("../graphs/docs");
|
|
|
13
13
|
const code_1 = require("../lib/parsers/code");
|
|
14
14
|
const code_2 = require("../graphs/code");
|
|
15
15
|
const watcher_1 = require("../lib/watcher");
|
|
16
|
+
const defaults_1 = require("../lib/defaults");
|
|
16
17
|
const knowledge_1 = require("../graphs/knowledge");
|
|
17
18
|
const task_1 = require("../graphs/task");
|
|
18
19
|
const skill_1 = require("../graphs/skill");
|
|
@@ -103,7 +104,7 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
|
|
|
103
104
|
const rootChunk = chunks.find(c => c.level === 1);
|
|
104
105
|
const embedText = rootChunk?.title
|
|
105
106
|
? `${fileId} ${rootChunk.title}`
|
|
106
|
-
: `${fileId} ${rootChunk?.content.slice(0,
|
|
107
|
+
: `${fileId} ${rootChunk?.content.slice(0, defaults_1.INDEXER_PREVIEW_LEN) ?? ''}`;
|
|
107
108
|
batchInputs.push({ title: embedText, content: '' });
|
|
108
109
|
const embeddings = await (0, embedder_1.embedBatch)(batchInputs, config.docsModelName);
|
|
109
110
|
for (let i = 0; i < chunks.length; i++) {
|
|
@@ -36,10 +36,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.MAX_ATTACHMENTS_PER_ENTITY = exports.MAX_ATTACHMENT_SIZE = void 0;
|
|
39
40
|
exports.scanAttachments = scanAttachments;
|
|
40
41
|
const fs = __importStar(require("fs"));
|
|
41
42
|
const path = __importStar(require("path"));
|
|
42
43
|
const mime_1 = __importDefault(require("mime"));
|
|
44
|
+
/** Maximum size of a single attachment in bytes (10 MB). */
|
|
45
|
+
exports.MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
|
|
46
|
+
/** Maximum number of attachments per entity (note/task/skill). */
|
|
47
|
+
exports.MAX_ATTACHMENTS_PER_ENTITY = 20;
|
|
43
48
|
/**
|
|
44
49
|
* Scan the attachments/ subdirectory of an entity directory.
|
|
45
50
|
* Returns metadata for each file found.
|
package/dist/graphs/code.js
CHANGED
|
@@ -22,6 +22,8 @@ const code_1 = require("../lib/search/code");
|
|
|
22
22
|
const files_1 = require("../lib/search/files");
|
|
23
23
|
const bm25_1 = require("../lib/search/bm25");
|
|
24
24
|
const embedding_codec_1 = require("../lib/embedding-codec");
|
|
25
|
+
const graph_persistence_1 = require("../lib/graph-persistence");
|
|
26
|
+
const defaults_1 = require("../lib/defaults");
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
// CRUD
|
|
27
29
|
// ---------------------------------------------------------------------------
|
|
@@ -87,10 +89,10 @@ function resolvePendingImports(graph) {
|
|
|
87
89
|
}
|
|
88
90
|
/**
|
|
89
91
|
* Resolve pending extends/implements edges after all files have been indexed.
|
|
90
|
-
*
|
|
92
|
+
* When multiple candidates share the same name, prefers the one whose file
|
|
93
|
+
* is imported by the source file (falls back to first match).
|
|
91
94
|
*/
|
|
92
95
|
function resolvePendingEdges(graph) {
|
|
93
|
-
// Build name → nodeId index for fast lookup
|
|
94
96
|
const nameIndex = new Map();
|
|
95
97
|
graph.forEachNode((id, attrs) => {
|
|
96
98
|
if (attrs.kind === 'class' || attrs.kind === 'interface') {
|
|
@@ -99,6 +101,18 @@ function resolvePendingEdges(graph) {
|
|
|
99
101
|
nameIndex.set(attrs.name, list);
|
|
100
102
|
}
|
|
101
103
|
});
|
|
104
|
+
// Build file → imported file IDs index for disambiguation
|
|
105
|
+
const fileImports = new Map();
|
|
106
|
+
graph.forEachNode((id, attrs) => {
|
|
107
|
+
if (attrs.kind === 'file') {
|
|
108
|
+
const imported = new Set();
|
|
109
|
+
graph.forEachOutEdge(id, (_edge, edgeAttrs, _src, target) => {
|
|
110
|
+
if (edgeAttrs.kind === 'imports')
|
|
111
|
+
imported.add(target);
|
|
112
|
+
});
|
|
113
|
+
fileImports.set(id, imported);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
102
116
|
let created = 0;
|
|
103
117
|
graph.forEachNode((id, attrs) => {
|
|
104
118
|
if (!attrs.pendingEdges || attrs.pendingEdges.length === 0)
|
|
@@ -107,8 +121,20 @@ function resolvePendingEdges(graph) {
|
|
|
107
121
|
for (const edge of attrs.pendingEdges) {
|
|
108
122
|
const candidates = nameIndex.get(edge.toName);
|
|
109
123
|
if (candidates && candidates.length > 0 && graph.hasNode(edge.from)) {
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
let toId;
|
|
125
|
+
if (candidates.length === 1) {
|
|
126
|
+
toId = candidates[0];
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Disambiguate: prefer candidate whose file is imported by edge.from's file
|
|
130
|
+
const fromFileId = edge.from.split('::')[0];
|
|
131
|
+
const imports = fileImports.get(fromFileId);
|
|
132
|
+
const match = imports && candidates.find(c => {
|
|
133
|
+
const cFileId = c.split('::')[0];
|
|
134
|
+
return imports.has(cFileId);
|
|
135
|
+
});
|
|
136
|
+
toId = match ?? candidates[0];
|
|
137
|
+
}
|
|
112
138
|
if (toId !== edge.from && !graph.hasEdge(edge.from, toId)) {
|
|
113
139
|
graph.addEdgeWithKey(`${edge.from}→${toId}`, edge.from, toId, { kind: edge.kind });
|
|
114
140
|
created++;
|
|
@@ -142,7 +168,7 @@ function getCodeFileMtime(graph, fileId) {
|
|
|
142
168
|
return graph.getNodeAttribute(nodes[0], 'mtime');
|
|
143
169
|
}
|
|
144
170
|
/** List all indexed files with symbol counts. */
|
|
145
|
-
function listCodeFiles(graph, filter, limit =
|
|
171
|
+
function listCodeFiles(graph, filter, limit = defaults_1.LIST_LIMIT_SMALL) {
|
|
146
172
|
const files = new Map();
|
|
147
173
|
const lowerFilter = filter?.toLowerCase();
|
|
148
174
|
graph.forEachNode((_, attrs) => {
|
|
@@ -182,10 +208,10 @@ function loadCodeGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
|
182
208
|
if (fresh)
|
|
183
209
|
return graph;
|
|
184
210
|
const file = path_1.default.join(graphMemory, 'code.json');
|
|
185
|
-
|
|
211
|
+
const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
|
|
212
|
+
if (!data)
|
|
186
213
|
return graph;
|
|
187
214
|
try {
|
|
188
|
-
const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
189
215
|
const stored = data.embeddingModel;
|
|
190
216
|
if (embeddingFingerprint && stored !== embeddingFingerprint) {
|
|
191
217
|
process.stderr.write(`[code-graph] Embedding config changed, re-indexing code graph\n`);
|
|
@@ -207,7 +233,7 @@ class CodeGraphManager {
|
|
|
207
233
|
_graph;
|
|
208
234
|
embedFns;
|
|
209
235
|
ext;
|
|
210
|
-
_bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment} ${attrs.body}`);
|
|
236
|
+
_bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment} ${attrs.body.slice(0, defaults_1.BM25_BODY_MAX_CHARS)}`);
|
|
211
237
|
constructor(_graph, embedFns, ext = {}) {
|
|
212
238
|
this._graph = _graph;
|
|
213
239
|
this.embedFns = embedFns;
|