@graphmemory/server 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +84 -12
- package/README.md +7 -1
- package/dist/api/index.js +147 -50
- package/dist/api/rest/index.js +35 -15
- package/dist/api/rest/tools.js +8 -1
- package/dist/api/tools/code/search-code.js +12 -9
- package/dist/api/tools/code/search-files.js +1 -1
- package/dist/api/tools/docs/cross-references.js +3 -2
- package/dist/api/tools/docs/explain-symbol.js +2 -1
- package/dist/api/tools/docs/find-examples.js +2 -1
- package/dist/api/tools/docs/search-files.js +1 -1
- package/dist/api/tools/docs/search-snippets.js +1 -1
- package/dist/api/tools/docs/search.js +5 -4
- package/dist/api/tools/file-index/search-all-files.js +1 -1
- package/dist/api/tools/knowledge/add-attachment.js +14 -3
- package/dist/api/tools/knowledge/remove-attachment.js +5 -1
- package/dist/api/tools/knowledge/search-notes.js +5 -4
- package/dist/api/tools/skills/add-attachment.js +14 -3
- package/dist/api/tools/skills/recall-skills.js +1 -1
- package/dist/api/tools/skills/remove-attachment.js +5 -1
- package/dist/api/tools/skills/search-skills.js +6 -5
- package/dist/api/tools/tasks/add-attachment.js +14 -3
- package/dist/api/tools/tasks/remove-attachment.js +5 -1
- package/dist/api/tools/tasks/search-tasks.js +5 -4
- package/dist/cli/index.js +61 -51
- package/dist/cli/indexer.js +60 -28
- package/dist/graphs/code.js +70 -7
- package/dist/graphs/docs.js +15 -2
- package/dist/graphs/file-index.js +17 -3
- package/dist/graphs/file-lang.js +1 -1
- package/dist/graphs/knowledge.js +20 -3
- package/dist/graphs/skill.js +23 -4
- package/dist/graphs/task.js +23 -4
- package/dist/lib/embedding-codec.js +65 -0
- package/dist/lib/jwt.js +4 -4
- package/dist/lib/multi-config.js +6 -1
- package/dist/lib/parsers/code.js +158 -31
- package/dist/lib/parsers/codeblock.js +11 -6
- package/dist/lib/parsers/docs.js +59 -31
- package/dist/lib/parsers/languages/registry.js +2 -2
- package/dist/lib/parsers/languages/typescript.js +195 -44
- package/dist/lib/project-manager.js +14 -10
- package/dist/lib/search/bm25.js +18 -1
- package/dist/lib/search/code.js +12 -3
- package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
- package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
- package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
- package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
- package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
- package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
- package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
- package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
- package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
- package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
- package/dist/ui/assets/api-BMnBjMMf.js +1 -0
- package/dist/ui/assets/api-BlFF6gX-.js +1 -0
- package/dist/ui/assets/api-CrGJOcaN.js +1 -0
- package/dist/ui/assets/api-DuX-0a_X.js +1 -0
- package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
- package/dist/ui/assets/client-Bq88u7gN.js +1 -0
- package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
- package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
- package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
- package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
- package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
- package/dist/ui/assets/files-0bPg6NH9.js +1 -0
- package/dist/ui/assets/graph-DXGud_wF.js +1 -0
- package/dist/ui/assets/help-CEMQqZUR.js +891 -0
- package/dist/ui/assets/help-DJ52_fxN.js +1 -0
- package/dist/ui/assets/index-BCZDAYZi.js +2 -0
- package/dist/ui/assets/index-D6zSNtzo.css +1 -0
- package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
- package/dist/ui/assets/new-CpD7hOBA.js +1 -0
- package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
- package/dist/ui/assets/new-s8c0M75X.js +1 -0
- package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
- package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
- package/dist/ui/assets/search-EpJhdP2a.js +1 -0
- package/dist/ui/assets/skill-y9pizyqE.js +1 -0
- package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
- package/dist/ui/assets/tasks-CobouTKV.js +1 -0
- package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
- package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
- package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
- package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
- package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
- package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
- package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
- package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
- package/dist/ui/index.html +11 -3
- package/package.json +2 -2
- package/dist/ui/assets/index-0hRezICt.js +0 -1702
|
@@ -17,8 +17,9 @@ function register(server, docMgr, codeMgr) {
|
|
|
17
17
|
}, async ({ symbol }) => {
|
|
18
18
|
// 1. Search CodeGraph for definitions
|
|
19
19
|
const definitions = [];
|
|
20
|
+
const symbolLower = symbol.toLowerCase();
|
|
20
21
|
codeGraph.forEachNode((id, attrs) => {
|
|
21
|
-
if (attrs.name === symbol) {
|
|
22
|
+
if (attrs.name === symbol || attrs.name.toLowerCase() === symbolLower) {
|
|
22
23
|
definitions.push({
|
|
23
24
|
id,
|
|
24
25
|
fileId: attrs.fileId,
|
|
@@ -38,7 +39,7 @@ function register(server, docMgr, codeMgr) {
|
|
|
38
39
|
docGraph.forEachNode((id, attrs) => {
|
|
39
40
|
if (attrs.symbols.length === 0)
|
|
40
41
|
return;
|
|
41
|
-
if (!attrs.symbols.
|
|
42
|
+
if (!attrs.symbols.some(s => s === symbol || s.toLowerCase() === symbolLower))
|
|
42
43
|
return;
|
|
43
44
|
examples.push({
|
|
44
45
|
id,
|
|
@@ -14,13 +14,14 @@ function register(server, mgr) {
|
|
|
14
14
|
limit: zod_1.z.number().optional().describe('Max results to return (default 10)'),
|
|
15
15
|
},
|
|
16
16
|
}, async ({ symbol, limit = 10 }) => {
|
|
17
|
+
const symbolLower = symbol.toLowerCase();
|
|
17
18
|
const results = [];
|
|
18
19
|
graph.forEachNode((id, attrs) => {
|
|
19
20
|
if (results.length >= limit)
|
|
20
21
|
return;
|
|
21
22
|
if (attrs.symbols.length === 0)
|
|
22
23
|
return;
|
|
23
|
-
if (!attrs.symbols.
|
|
24
|
+
if (!attrs.symbols.some(s => s === symbol || s.toLowerCase() === symbolLower))
|
|
24
25
|
return;
|
|
25
26
|
// Find the parent text section
|
|
26
27
|
const parent = findParentTextSection(graph, id, attrs);
|
|
@@ -14,13 +14,14 @@ function register(server, mgr) {
|
|
|
14
14
|
limit: zod_1.z.number().optional().describe('Max results to return (default 20)'),
|
|
15
15
|
},
|
|
16
16
|
}, async ({ symbol, limit = 20 }) => {
|
|
17
|
+
const symbolLower = symbol.toLowerCase();
|
|
17
18
|
const results = [];
|
|
18
19
|
graph.forEachNode((id, attrs) => {
|
|
19
20
|
if (results.length >= limit)
|
|
20
21
|
return;
|
|
21
22
|
if (attrs.symbols.length === 0)
|
|
22
23
|
return;
|
|
23
|
-
if (!attrs.symbols.
|
|
24
|
+
if (!attrs.symbols.some(s => s === symbol || s.toLowerCase() === symbolLower))
|
|
24
25
|
return;
|
|
25
26
|
// Find parent text section (previous node with lower level and no language)
|
|
26
27
|
const parentId = findParentSection(graph, id, attrs);
|
|
@@ -12,7 +12,7 @@ function register(server, mgr) {
|
|
|
12
12
|
'Use this to discover which doc files are relevant before diving into content with search or get_toc.',
|
|
13
13
|
inputSchema: {
|
|
14
14
|
query: zod_1.z.string().describe('Natural language search query, e.g. "authentication setup" or "API endpoints"'),
|
|
15
|
-
topK: zod_1.z.number().optional().describe('Maximum number of results to return (default 10)'),
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 10)'),
|
|
16
16
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.3)'),
|
|
17
17
|
},
|
|
18
18
|
}, async ({ query, topK, minScore }) => {
|
|
@@ -12,7 +12,7 @@ function register(server, mgr) {
|
|
|
12
12
|
'Returns code block nodes sorted by relevance score.',
|
|
13
13
|
inputSchema: {
|
|
14
14
|
query: zod_1.z.string().describe('Natural language search query'),
|
|
15
|
-
topK: zod_1.z.number().optional().describe('Max results to return (default 10)'),
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('Max results to return (default 10)'),
|
|
16
16
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.3)'),
|
|
17
17
|
language: zod_1.z.string().optional().describe('Filter by language, e.g. "typescript", "python"'),
|
|
18
18
|
},
|
|
@@ -5,7 +5,8 @@ const zod_1 = require("zod");
|
|
|
5
5
|
function register(server, mgr) {
|
|
6
6
|
server.registerTool('search', {
|
|
7
7
|
description: 'Semantic search over the indexed documentation. ' +
|
|
8
|
-
'
|
|
8
|
+
'Supports three modes: hybrid (default, BM25 + vector), vector, keyword. ' +
|
|
9
|
+
'Finds the most relevant sections, then expands results ' +
|
|
9
10
|
'by traversing links between documents (graph walk). ' +
|
|
10
11
|
'Returns an array of chunks sorted by relevance score (0–1), each with: ' +
|
|
11
12
|
'id, fileId, title, content, level, score. ' +
|
|
@@ -13,9 +14,9 @@ function register(server, mgr) {
|
|
|
13
14
|
'Prefer this tool when looking for information without knowing which file contains it.',
|
|
14
15
|
inputSchema: {
|
|
15
16
|
query: zod_1.z.string().describe('Natural language search query'),
|
|
16
|
-
topK: zod_1.z.number().optional().describe('How many top similar sections to use as seeds for graph expansion (default 5)'),
|
|
17
|
-
bfsDepth: zod_1.z.number().optional().describe('How many hops to follow cross-document links from each seed (default 1; 0 = no expansion)'),
|
|
18
|
-
maxResults: zod_1.z.number().optional().describe('Maximum number of results to return (default 20)'),
|
|
17
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar sections to use as seeds for graph expansion (default 5)'),
|
|
18
|
+
bfsDepth: zod_1.z.number().min(0).max(10).optional().describe('How many hops to follow cross-document links from each seed (default 1; 0 = no expansion)'),
|
|
19
|
+
maxResults: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 20)'),
|
|
19
20
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score threshold 0–1; lower values return more results (default 0.5)'),
|
|
20
21
|
bfsDecay: zod_1.z.number().min(0).max(1).optional().describe('Score multiplier applied per graph hop; controls how quickly relevance fades with distance (default 0.8)'),
|
|
21
22
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional().describe('Search mode: hybrid (default, BM25 + vector), vector (embedding only), keyword (BM25 only)'),
|
|
@@ -12,7 +12,7 @@ function register(server, mgr) {
|
|
|
12
12
|
'Use this to discover which project files are relevant to a topic.',
|
|
13
13
|
inputSchema: {
|
|
14
14
|
query: zod_1.z.string().describe('Search query'),
|
|
15
|
-
topK: zod_1.z.number().optional().default(10)
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().default(10)
|
|
16
16
|
.describe('Max results (default 10)'),
|
|
17
17
|
minScore: zod_1.z.number().optional().default(0.3)
|
|
18
18
|
.describe('Minimum cosine similarity score (default 0.3)'),
|
|
@@ -17,11 +17,22 @@ function register(server, mgr) {
|
|
|
17
17
|
filePath: zod_1.z.string().describe('Absolute path to the file on disk'),
|
|
18
18
|
},
|
|
19
19
|
}, async ({ noteId, filePath }) => {
|
|
20
|
-
|
|
20
|
+
const resolved = path_1.default.resolve(filePath);
|
|
21
|
+
let stat;
|
|
22
|
+
try {
|
|
23
|
+
stat = fs_1.default.statSync(resolved);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
21
26
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File not found' }) }], isError: true };
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
if (!stat.isFile()) {
|
|
29
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
32
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
const data = fs_1.default.readFileSync(resolved);
|
|
35
|
+
const filename = path_1.default.basename(resolved);
|
|
25
36
|
const meta = mgr.addAttachment(noteId, filename, data);
|
|
26
37
|
if (!meta) {
|
|
27
38
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Note not found or no project dir' }) }], isError: true };
|
|
@@ -7,7 +7,11 @@ function register(server, mgr) {
|
|
|
7
7
|
description: 'Remove an attachment from a note. The file is deleted from disk.',
|
|
8
8
|
inputSchema: {
|
|
9
9
|
noteId: zod_1.z.string().describe('ID of the note'),
|
|
10
|
-
filename: zod_1.z.string().
|
|
10
|
+
filename: zod_1.z.string().min(1).max(255)
|
|
11
|
+
.refine(s => !/[/\\]/.test(s), 'Filename must not contain path separators')
|
|
12
|
+
.refine(s => !s.includes('..'), 'Filename must not contain ..')
|
|
13
|
+
.refine(s => !s.includes('\0'), 'Filename must not contain null bytes')
|
|
14
|
+
.describe('Filename of the attachment to remove'),
|
|
11
15
|
},
|
|
12
16
|
}, async ({ noteId, filename }) => {
|
|
13
17
|
const ok = mgr.removeAttachment(noteId, filename);
|
|
@@ -5,15 +5,16 @@ const zod_1 = require("zod");
|
|
|
5
5
|
function register(server, mgr) {
|
|
6
6
|
server.registerTool('search_notes', {
|
|
7
7
|
description: 'Semantic search over the knowledge graph (facts and notes). ' +
|
|
8
|
-
'
|
|
8
|
+
'Supports three modes: hybrid (default, BM25 + vector), vector, keyword. ' +
|
|
9
|
+
'Finds the most relevant notes, then expands results ' +
|
|
9
10
|
'by traversing relations between notes (graph walk). ' +
|
|
10
11
|
'Returns an array sorted by relevance score (0–1), each with: ' +
|
|
11
12
|
'id, title, content, tags, score.',
|
|
12
13
|
inputSchema: {
|
|
13
14
|
query: zod_1.z.string().describe('Natural language search query'),
|
|
14
|
-
topK: zod_1.z.number().optional().describe('How many top similar notes to use as seeds (default 5)'),
|
|
15
|
-
bfsDepth: zod_1.z.number().optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
|
|
16
|
-
maxResults: zod_1.z.number().optional().describe('Maximum number of results to return (default 20)'),
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar notes to use as seeds (default 5)'),
|
|
16
|
+
bfsDepth: zod_1.z.number().min(0).max(10).optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
|
|
17
|
+
maxResults: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 20)'),
|
|
17
18
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.5)'),
|
|
18
19
|
bfsDecay: zod_1.z.number().min(0).max(1).optional().describe('Score multiplier per hop (default 0.8)'),
|
|
19
20
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional().describe('Search mode: hybrid (default, BM25 + vector), vector (embedding only), keyword (BM25 only)'),
|
|
@@ -17,11 +17,22 @@ function register(server, mgr) {
|
|
|
17
17
|
filePath: zod_1.z.string().describe('Absolute path to the file on disk'),
|
|
18
18
|
},
|
|
19
19
|
}, async ({ skillId, filePath }) => {
|
|
20
|
-
|
|
20
|
+
const resolved = path_1.default.resolve(filePath);
|
|
21
|
+
let stat;
|
|
22
|
+
try {
|
|
23
|
+
stat = fs_1.default.statSync(resolved);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
21
26
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File not found' }) }], isError: true };
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
if (!stat.isFile()) {
|
|
29
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
32
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
const data = fs_1.default.readFileSync(resolved);
|
|
35
|
+
const filename = path_1.default.basename(resolved);
|
|
25
36
|
const meta = mgr.addAttachment(skillId, filename, data);
|
|
26
37
|
if (!meta) {
|
|
27
38
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Skill not found or no project dir' }) }], isError: true };
|
|
@@ -8,7 +8,7 @@ function register(server, mgr) {
|
|
|
8
8
|
'minScore default (0.3) for higher recall. Use at the start of a task to find applicable recipes.',
|
|
9
9
|
inputSchema: {
|
|
10
10
|
context: zod_1.z.string().describe('Description of the current task or context to match skills against'),
|
|
11
|
-
topK: zod_1.z.number().optional().describe('How many top similar skills to use as seeds (default 5)'),
|
|
11
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar skills to use as seeds (default 5)'),
|
|
12
12
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.3)'),
|
|
13
13
|
},
|
|
14
14
|
}, async ({ context, topK, minScore }) => {
|
|
@@ -7,7 +7,11 @@ function register(server, mgr) {
|
|
|
7
7
|
description: 'Remove an attachment from a skill. The file is deleted from disk.',
|
|
8
8
|
inputSchema: {
|
|
9
9
|
skillId: zod_1.z.string().describe('ID of the skill'),
|
|
10
|
-
filename: zod_1.z.string().
|
|
10
|
+
filename: zod_1.z.string().min(1).max(255)
|
|
11
|
+
.refine(s => !/[/\\]/.test(s), 'Filename must not contain path separators')
|
|
12
|
+
.refine(s => !s.includes('..'), 'Filename must not contain ..')
|
|
13
|
+
.refine(s => !s.includes('\0'), 'Filename must not contain null bytes')
|
|
14
|
+
.describe('Filename of the attachment to remove'),
|
|
11
15
|
},
|
|
12
16
|
}, async ({ skillId, filename }) => {
|
|
13
17
|
const ok = mgr.removeAttachment(skillId, filename);
|
|
@@ -5,15 +5,16 @@ const zod_1 = require("zod");
|
|
|
5
5
|
function register(server, mgr) {
|
|
6
6
|
server.registerTool('search_skills', {
|
|
7
7
|
description: 'Semantic search over the skill graph. ' +
|
|
8
|
-
'
|
|
8
|
+
'Supports three modes: hybrid (default, BM25 + vector), vector, keyword. ' +
|
|
9
|
+
'Finds the most relevant skills, then expands results ' +
|
|
9
10
|
'by traversing relations between skills (graph walk). ' +
|
|
10
11
|
'Returns an array sorted by relevance score (0–1), each with: ' +
|
|
11
|
-
'id, title, description, tags, source, confidence, score.',
|
|
12
|
+
'id, title, description, tags, source, confidence, usageCount, score.',
|
|
12
13
|
inputSchema: {
|
|
13
14
|
query: zod_1.z.string().describe('Natural language search query'),
|
|
14
|
-
topK: zod_1.z.number().optional().describe('How many top similar skills to use as seeds (default 5)'),
|
|
15
|
-
bfsDepth: zod_1.z.number().optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
|
|
16
|
-
maxResults: zod_1.z.number().optional().describe('Maximum number of results to return (default 20)'),
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar skills to use as seeds (default 5)'),
|
|
16
|
+
bfsDepth: zod_1.z.number().min(0).max(10).optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
|
|
17
|
+
maxResults: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 20)'),
|
|
17
18
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.5)'),
|
|
18
19
|
bfsDecay: zod_1.z.number().min(0).max(1).optional().describe('Score multiplier per hop (default 0.8)'),
|
|
19
20
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional().describe('Search mode: hybrid (default, BM25 + vector), vector (embedding only), keyword (BM25 only)'),
|
|
@@ -17,11 +17,22 @@ function register(server, mgr) {
|
|
|
17
17
|
filePath: zod_1.z.string().describe('Absolute path to the file on disk'),
|
|
18
18
|
},
|
|
19
19
|
}, async ({ taskId, filePath }) => {
|
|
20
|
-
|
|
20
|
+
const resolved = path_1.default.resolve(filePath);
|
|
21
|
+
let stat;
|
|
22
|
+
try {
|
|
23
|
+
stat = fs_1.default.statSync(resolved);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
21
26
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File not found' }) }], isError: true };
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
if (!stat.isFile()) {
|
|
29
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
32
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
const data = fs_1.default.readFileSync(resolved);
|
|
35
|
+
const filename = path_1.default.basename(resolved);
|
|
25
36
|
const meta = mgr.addAttachment(taskId, filename, data);
|
|
26
37
|
if (!meta) {
|
|
27
38
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Task not found or no project dir' }) }], isError: true };
|
|
@@ -7,7 +7,11 @@ function register(server, mgr) {
|
|
|
7
7
|
description: 'Remove an attachment from a task. The file is deleted from disk.',
|
|
8
8
|
inputSchema: {
|
|
9
9
|
taskId: zod_1.z.string().describe('ID of the task'),
|
|
10
|
-
filename: zod_1.z.string().
|
|
10
|
+
filename: zod_1.z.string().min(1).max(255)
|
|
11
|
+
.refine(s => !/[/\\]/.test(s), 'Filename must not contain path separators')
|
|
12
|
+
.refine(s => !s.includes('..'), 'Filename must not contain ..')
|
|
13
|
+
.refine(s => !s.includes('\0'), 'Filename must not contain null bytes')
|
|
14
|
+
.describe('Filename of the attachment to remove'),
|
|
11
15
|
},
|
|
12
16
|
}, async ({ taskId, filename }) => {
|
|
13
17
|
const ok = mgr.removeAttachment(taskId, filename);
|
|
@@ -5,15 +5,16 @@ const zod_1 = require("zod");
|
|
|
5
5
|
function register(server, mgr) {
|
|
6
6
|
server.registerTool('search_tasks', {
|
|
7
7
|
description: 'Semantic search over the task graph. ' +
|
|
8
|
-
'
|
|
8
|
+
'Supports three modes: hybrid (default, BM25 + vector), vector, keyword. ' +
|
|
9
|
+
'Finds the most relevant tasks, then expands results ' +
|
|
9
10
|
'by traversing relations between tasks (graph walk). ' +
|
|
10
11
|
'Returns an array sorted by relevance score (0–1), each with: ' +
|
|
11
12
|
'id, title, description, status, priority, tags, score.',
|
|
12
13
|
inputSchema: {
|
|
13
14
|
query: zod_1.z.string().describe('Natural language search query'),
|
|
14
|
-
topK: zod_1.z.number().optional().describe('How many top similar tasks to use as seeds (default 5)'),
|
|
15
|
-
bfsDepth: zod_1.z.number().optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
|
|
16
|
-
maxResults: zod_1.z.number().optional().describe('Maximum number of results to return (default 20)'),
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar tasks to use as seeds (default 5)'),
|
|
16
|
+
bfsDepth: zod_1.z.number().min(0).max(10).optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
|
|
17
|
+
maxResults: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 20)'),
|
|
17
18
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.5)'),
|
|
18
19
|
bfsDecay: zod_1.z.number().min(0).max(1).optional().describe('Score multiplier per hop (default 0.8)'),
|
|
19
20
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional().describe('Search mode: hybrid (default, BM25 + vector), vector (embedding only), keyword (BM25 only)'),
|
package/dist/cli/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const program = new commander_1.Command();
|
|
|
18
18
|
program
|
|
19
19
|
.name('graphmemory')
|
|
20
20
|
.description('MCP server for semantic graph memory from markdown docs and source code')
|
|
21
|
-
.version('1.
|
|
21
|
+
.version('1.3.0');
|
|
22
22
|
const parseIntArg = (v) => parseInt(v, 10);
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
// Helper: load config from file, or fall back to default (cwd as single project)
|
|
@@ -146,7 +146,48 @@ program
|
|
|
146
146
|
}
|
|
147
147
|
// Embedding API model name (loaded in background with other models)
|
|
148
148
|
const embeddingApiModelName = mc.server.embeddingApi?.enabled ? '__server__' : undefined;
|
|
149
|
-
//
|
|
149
|
+
// Load models and index all projects before starting HTTP
|
|
150
|
+
// Load embedding API model if enabled
|
|
151
|
+
if (embeddingApiModelName) {
|
|
152
|
+
try {
|
|
153
|
+
await (0, embedder_1.loadModel)(mc.server.model, mc.server.embedding, mc.server.modelsDir, embeddingApiModelName);
|
|
154
|
+
process.stderr.write(`[serve] Embedding API model ready\n`);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
process.stderr.write(`[serve] Failed to load embedding API model: ${err}\n`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Load workspace models
|
|
161
|
+
for (const wsId of manager.listWorkspaces()) {
|
|
162
|
+
try {
|
|
163
|
+
await manager.loadWorkspaceModels(wsId);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
process.stderr.write(`[serve] Failed to load workspace "${wsId}" models: ${err}\n`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Load project models and start indexing
|
|
170
|
+
for (const id of manager.listProjects()) {
|
|
171
|
+
try {
|
|
172
|
+
await manager.loadModels(id);
|
|
173
|
+
await manager.startIndexing(id);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
process.stderr.write(`[serve] Failed to initialize project "${id}": ${err}\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Start workspace mirror watchers (after all projects are indexed)
|
|
180
|
+
for (const wsId of manager.listWorkspaces()) {
|
|
181
|
+
try {
|
|
182
|
+
await manager.startWorkspaceMirror(wsId);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
process.stderr.write(`[serve] Failed to start workspace "${wsId}" mirror: ${err}\n`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Start auto-save
|
|
189
|
+
manager.startAutoSave();
|
|
190
|
+
// Start HTTP server (all models loaded, all projects indexed)
|
|
150
191
|
const httpServer = await (0, index_1.startMultiProjectHttpServer)(host, port, sessionTimeoutMs, manager, {
|
|
151
192
|
serverConfig: mc.server,
|
|
152
193
|
users: mc.users,
|
|
@@ -158,52 +199,6 @@ program
|
|
|
158
199
|
openSockets.add(socket);
|
|
159
200
|
socket.on('close', () => openSockets.delete(socket));
|
|
160
201
|
});
|
|
161
|
-
// Start auto-save
|
|
162
|
-
manager.startAutoSave();
|
|
163
|
-
// Load models and start indexing in background (workspaces first, then projects)
|
|
164
|
-
async function initProjects() {
|
|
165
|
-
// Load embedding API model if enabled
|
|
166
|
-
if (embeddingApiModelName) {
|
|
167
|
-
try {
|
|
168
|
-
await (0, embedder_1.loadModel)(mc.server.model, mc.server.embedding, mc.server.modelsDir, embeddingApiModelName);
|
|
169
|
-
process.stderr.write(`[serve] Embedding API model ready\n`);
|
|
170
|
-
}
|
|
171
|
-
catch (err) {
|
|
172
|
-
process.stderr.write(`[serve] Failed to load embedding API model: ${err}\n`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
// Load workspace models
|
|
176
|
-
for (const wsId of manager.listWorkspaces()) {
|
|
177
|
-
try {
|
|
178
|
-
await manager.loadWorkspaceModels(wsId);
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
process.stderr.write(`[serve] Failed to load workspace "${wsId}" models: ${err}\n`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
// Load project models and start indexing
|
|
185
|
-
for (const id of manager.listProjects()) {
|
|
186
|
-
try {
|
|
187
|
-
await manager.loadModels(id);
|
|
188
|
-
await manager.startIndexing(id);
|
|
189
|
-
}
|
|
190
|
-
catch (err) {
|
|
191
|
-
process.stderr.write(`[serve] Failed to initialize project "${id}": ${err}\n`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// Start workspace mirror watchers (after all projects are indexed)
|
|
195
|
-
for (const wsId of manager.listWorkspaces()) {
|
|
196
|
-
try {
|
|
197
|
-
await manager.startWorkspaceMirror(wsId);
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
process.stderr.write(`[serve] Failed to start workspace "${wsId}" mirror: ${err}\n`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
initProjects().catch((err) => {
|
|
205
|
-
process.stderr.write(`[serve] Init error: ${err}\n`);
|
|
206
|
-
});
|
|
207
202
|
let shuttingDown = false;
|
|
208
203
|
async function shutdown() {
|
|
209
204
|
if (shuttingDown) {
|
|
@@ -320,13 +315,28 @@ usersCmd
|
|
|
320
315
|
process.stderr.write('Passwords do not match\n');
|
|
321
316
|
process.exit(1);
|
|
322
317
|
}
|
|
318
|
+
// Validate inputs
|
|
319
|
+
if (/[\x00-\x1f\x7f]/.test(name)) {
|
|
320
|
+
process.stderr.write('Name contains invalid characters\n');
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
324
|
+
process.stderr.write('Invalid email format\n');
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
if (password.length > 256) {
|
|
328
|
+
process.stderr.write('Password too long (max 256)\n');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
323
331
|
const pwHash = await (0, jwt_1.hashPassword)(password);
|
|
324
332
|
const apiKey = `mgm-${crypto_1.default.randomBytes(24).toString('base64url')}`;
|
|
325
|
-
// Build YAML block for the new user
|
|
333
|
+
// Build YAML block for the new user — escape quotes to prevent YAML injection
|
|
334
|
+
const safeName = name.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
335
|
+
const safeEmail = email.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
326
336
|
const userBlock = [
|
|
327
337
|
` ${id}:`,
|
|
328
|
-
` name: "${
|
|
329
|
-
` email: "${
|
|
338
|
+
` name: "${safeName}"`,
|
|
339
|
+
` email: "${safeEmail}"`,
|
|
330
340
|
` apiKey: "${apiKey}"`,
|
|
331
341
|
` passwordHash: "${pwHash}"`,
|
|
332
342
|
].join('\n');
|
package/dist/cli/indexer.js
CHANGED
|
@@ -19,31 +19,51 @@ const skill_1 = require("../graphs/skill");
|
|
|
19
19
|
const file_index_1 = require("../graphs/file-index");
|
|
20
20
|
function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph) {
|
|
21
21
|
// Three independent serial queues — docs, code, and file index.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
22
|
+
// Array-based to avoid promise chain memory accumulation during scan.
|
|
23
|
+
function createSerialQueue(label) {
|
|
24
|
+
const pending = [];
|
|
25
|
+
let running = false;
|
|
26
|
+
let errors = 0;
|
|
27
|
+
let idleResolve = null;
|
|
28
|
+
let idlePromise = Promise.resolve();
|
|
29
|
+
async function pump() {
|
|
30
|
+
running = true;
|
|
31
|
+
while (pending.length > 0) {
|
|
32
|
+
const fn = pending.shift();
|
|
33
|
+
try {
|
|
34
|
+
await fn();
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
errors++;
|
|
38
|
+
process.stderr.write(`[indexer] ${label} error: ${err}\n`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
running = false;
|
|
42
|
+
if (idleResolve) {
|
|
43
|
+
idleResolve();
|
|
44
|
+
idleResolve = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
enqueue(fn) {
|
|
49
|
+
pending.push(fn);
|
|
50
|
+
if (!running) {
|
|
51
|
+
idlePromise = new Promise(r => { idleResolve = r; });
|
|
52
|
+
void pump();
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
waitIdle() {
|
|
56
|
+
return (pending.length === 0 && !running) ? Promise.resolve() : idlePromise;
|
|
57
|
+
},
|
|
58
|
+
get errors() { return errors; },
|
|
59
|
+
};
|
|
46
60
|
}
|
|
61
|
+
const docsQueue = createSerialQueue('Doc');
|
|
62
|
+
const codeQueue = createSerialQueue('Code');
|
|
63
|
+
const fileQueue = createSerialQueue('File index');
|
|
64
|
+
function enqueueDoc(fn) { docsQueue.enqueue(fn); }
|
|
65
|
+
function enqueueCode(fn) { codeQueue.enqueue(fn); }
|
|
66
|
+
function enqueueFile(fn) { fileQueue.enqueue(fn); }
|
|
47
67
|
// ---------------------------------------------------------------------------
|
|
48
68
|
// Per-file indexing
|
|
49
69
|
// ---------------------------------------------------------------------------
|
|
@@ -126,7 +146,16 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
|
|
|
126
146
|
const parsed = await (0, code_1.parseCodeFile)(absolutePath, config.projectDir, mtime);
|
|
127
147
|
// Batch-embed all symbols + file-level in one forward pass
|
|
128
148
|
const batchInputs = parsed.nodes.map(({ attrs }) => ({ title: attrs.signature, content: attrs.docComment }));
|
|
129
|
-
|
|
149
|
+
// File-level embedding: path + exported symbol names + import summary
|
|
150
|
+
const fileNode = parsed.nodes.find(n => n.attrs.kind === 'file');
|
|
151
|
+
const exportedNames = parsed.nodes
|
|
152
|
+
.filter(n => n.attrs.isExported && n.attrs.kind !== 'file')
|
|
153
|
+
.map(n => n.attrs.name);
|
|
154
|
+
const fileEmbedTitle = exportedNames.length > 0
|
|
155
|
+
? `${fileId} ${exportedNames.join(' ')}`
|
|
156
|
+
: fileId;
|
|
157
|
+
const fileEmbedContent = fileNode?.attrs.body ?? ''; // body = importSummary for file nodes
|
|
158
|
+
batchInputs.push({ title: fileEmbedTitle, content: fileEmbedContent });
|
|
130
159
|
const embeddings = await (0, embedder_1.embedBatch)(batchInputs, config.codeModelName);
|
|
131
160
|
for (let i = 0; i < parsed.nodes.length; i++) {
|
|
132
161
|
parsed.nodes[i].attrs.embedding = embeddings[i];
|
|
@@ -252,7 +281,7 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
|
|
|
252
281
|
}, '**/*', allExcludePatterns.length > 0 ? allExcludePatterns : undefined);
|
|
253
282
|
}
|
|
254
283
|
async function drain() {
|
|
255
|
-
await Promise.all([docsQueue, codeQueue, fileQueue]);
|
|
284
|
+
await Promise.all([docsQueue.waitIdle(), codeQueue.waitIdle(), fileQueue.waitIdle()]);
|
|
256
285
|
if (fileIndexGraph)
|
|
257
286
|
(0, file_index_1.rebuildDirectoryStats)(fileIndexGraph);
|
|
258
287
|
// Resolve cross-file edges that were deferred during indexing
|
|
@@ -265,10 +294,13 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
|
|
|
265
294
|
const codeImports = (0, code_2.resolvePendingImports)(codeGraph);
|
|
266
295
|
if (codeImports > 0)
|
|
267
296
|
process.stderr.write(`[indexer] Resolved ${codeImports} deferred code import edge(s)\n`);
|
|
297
|
+
const codeEdges = (0, code_2.resolvePendingEdges)(codeGraph);
|
|
298
|
+
if (codeEdges > 0)
|
|
299
|
+
process.stderr.write(`[indexer] Resolved ${codeEdges} deferred code extends/implements edge(s)\n`);
|
|
268
300
|
}
|
|
269
|
-
const totalErrors =
|
|
301
|
+
const totalErrors = docsQueue.errors + codeQueue.errors + fileQueue.errors;
|
|
270
302
|
if (totalErrors > 0) {
|
|
271
|
-
process.stderr.write(`[indexer] Completed with ${totalErrors} error(s): docs=${
|
|
303
|
+
process.stderr.write(`[indexer] Completed with ${totalErrors} error(s): docs=${docsQueue.errors}, code=${codeQueue.errors}, files=${fileQueue.errors}\n`);
|
|
272
304
|
}
|
|
273
305
|
}
|
|
274
306
|
return { scan, watch, drain };
|