@graphmemory/server 1.2.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.
Files changed (111) hide show
  1. package/LICENSE +84 -12
  2. package/README.md +7 -1
  3. package/dist/api/index.js +151 -54
  4. package/dist/api/rest/code.js +2 -1
  5. package/dist/api/rest/docs.js +2 -1
  6. package/dist/api/rest/embed.js +8 -1
  7. package/dist/api/rest/index.js +39 -18
  8. package/dist/api/rest/knowledge.js +4 -2
  9. package/dist/api/rest/skills.js +2 -1
  10. package/dist/api/rest/tasks.js +2 -1
  11. package/dist/api/rest/tools.js +8 -1
  12. package/dist/api/rest/validation.js +41 -40
  13. package/dist/api/rest/websocket.js +24 -7
  14. package/dist/api/tools/code/search-code.js +12 -9
  15. package/dist/api/tools/code/search-files.js +1 -1
  16. package/dist/api/tools/docs/cross-references.js +3 -2
  17. package/dist/api/tools/docs/explain-symbol.js +2 -1
  18. package/dist/api/tools/docs/find-examples.js +2 -1
  19. package/dist/api/tools/docs/search-files.js +1 -1
  20. package/dist/api/tools/docs/search-snippets.js +1 -1
  21. package/dist/api/tools/docs/search.js +5 -4
  22. package/dist/api/tools/file-index/search-all-files.js +1 -1
  23. package/dist/api/tools/knowledge/add-attachment.js +15 -3
  24. package/dist/api/tools/knowledge/remove-attachment.js +5 -1
  25. package/dist/api/tools/knowledge/search-notes.js +5 -4
  26. package/dist/api/tools/skills/add-attachment.js +15 -3
  27. package/dist/api/tools/skills/recall-skills.js +1 -1
  28. package/dist/api/tools/skills/remove-attachment.js +5 -1
  29. package/dist/api/tools/skills/search-skills.js +6 -5
  30. package/dist/api/tools/tasks/add-attachment.js +15 -3
  31. package/dist/api/tools/tasks/remove-attachment.js +5 -1
  32. package/dist/api/tools/tasks/search-tasks.js +5 -4
  33. package/dist/cli/index.js +63 -52
  34. package/dist/cli/indexer.js +62 -29
  35. package/dist/graphs/attachment-types.js +5 -0
  36. package/dist/graphs/code.js +99 -10
  37. package/dist/graphs/docs.js +20 -5
  38. package/dist/graphs/file-index.js +22 -6
  39. package/dist/graphs/file-lang.js +1 -1
  40. package/dist/graphs/knowledge.js +31 -7
  41. package/dist/graphs/skill.js +35 -9
  42. package/dist/graphs/task.js +35 -9
  43. package/dist/lib/defaults.js +78 -0
  44. package/dist/lib/embedder.js +11 -12
  45. package/dist/lib/embedding-codec.js +63 -0
  46. package/dist/lib/graph-persistence.js +68 -0
  47. package/dist/lib/jwt.js +4 -4
  48. package/dist/lib/mirror-watcher.js +4 -3
  49. package/dist/lib/multi-config.js +6 -1
  50. package/dist/lib/parsers/code.js +158 -31
  51. package/dist/lib/parsers/codeblock.js +11 -6
  52. package/dist/lib/parsers/docs.js +60 -31
  53. package/dist/lib/parsers/languages/registry.js +2 -2
  54. package/dist/lib/parsers/languages/typescript.js +214 -46
  55. package/dist/lib/project-manager.js +21 -11
  56. package/dist/lib/search/bm25.js +23 -5
  57. package/dist/lib/search/code.js +13 -3
  58. package/dist/lib/search/docs.js +2 -1
  59. package/dist/lib/search/file-index.js +2 -1
  60. package/dist/lib/search/files.js +3 -2
  61. package/dist/lib/search/knowledge.js +2 -1
  62. package/dist/lib/search/skills.js +2 -1
  63. package/dist/lib/search/tasks.js +2 -1
  64. package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
  65. package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
  66. package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
  67. package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
  68. package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
  69. package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
  70. package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
  71. package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
  72. package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
  73. package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
  74. package/dist/ui/assets/api-BMnBjMMf.js +1 -0
  75. package/dist/ui/assets/api-BlFF6gX-.js +1 -0
  76. package/dist/ui/assets/api-CrGJOcaN.js +1 -0
  77. package/dist/ui/assets/api-DuX-0a_X.js +1 -0
  78. package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
  79. package/dist/ui/assets/client-Bq88u7gN.js +1 -0
  80. package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
  81. package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
  82. package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
  83. package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
  84. package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
  85. package/dist/ui/assets/files-0bPg6NH9.js +1 -0
  86. package/dist/ui/assets/graph-DXGud_wF.js +1 -0
  87. package/dist/ui/assets/help-CEMQqZUR.js +891 -0
  88. package/dist/ui/assets/help-DJ52_fxN.js +1 -0
  89. package/dist/ui/assets/index-BCZDAYZi.js +2 -0
  90. package/dist/ui/assets/index-D6zSNtzo.css +1 -0
  91. package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
  92. package/dist/ui/assets/new-CpD7hOBA.js +1 -0
  93. package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
  94. package/dist/ui/assets/new-s8c0M75X.js +1 -0
  95. package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
  96. package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
  97. package/dist/ui/assets/search-EpJhdP2a.js +1 -0
  98. package/dist/ui/assets/skill-y9pizyqE.js +1 -0
  99. package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
  100. package/dist/ui/assets/tasks-CobouTKV.js +1 -0
  101. package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
  102. package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
  103. package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
  104. package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
  105. package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
  106. package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
  107. package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
  108. package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
  109. package/dist/ui/index.html +11 -3
  110. package/package.json +6 -3
  111. package/dist/ui/assets/index-0hRezICt.js +0 -1702
@@ -4,9 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.parseFile = parseFile;
7
+ exports.clearWikiIndexCache = clearWikiIndexCache;
7
8
  const path_1 = __importDefault(require("path"));
8
9
  const fs_1 = __importDefault(require("fs"));
9
10
  const codeblock_1 = require("../../lib/parsers/codeblock");
11
+ const defaults_1 = require("../../lib/defaults");
10
12
  // Parse a markdown file into chunks split by headings
11
13
  async function parseFile(content, absolutePath, projectDir, chunkDepth) {
12
14
  const fileId = path_1.default.relative(projectDir, absolutePath);
@@ -119,8 +121,10 @@ function extractFileTitle(content, filePath) {
119
121
  function extractLinks(content, fromFile, projectDir) {
120
122
  const results = new Set();
121
123
  const fileDir = path_1.default.dirname(fromFile);
124
+ // Strip fenced code blocks before extracting links (links inside code are not real references)
125
+ const textOnly = content.replace(FENCE_RE, '');
122
126
  // [text](./path.md)
123
- const mdLinks = content.matchAll(/\[[^\]]*\]\(([^)#\s]+)/g);
127
+ const mdLinks = textOnly.matchAll(/\[[^\]]*\]\(([^)#\s]+)/g);
124
128
  for (const [, href] of mdLinks) {
125
129
  if (isExternal(href))
126
130
  continue;
@@ -130,7 +134,7 @@ function extractLinks(content, fromFile, projectDir) {
130
134
  results.add(fileId);
131
135
  }
132
136
  // [[wiki link]] or [[wiki link|alias]]
133
- const wikiLinks = content.matchAll(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g);
137
+ const wikiLinks = textOnly.matchAll(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g);
134
138
  for (const [, name] of wikiLinks) {
135
139
  const resolved = findWikiFile(name.trim(), projectDir);
136
140
  if (!resolved)
@@ -160,40 +164,65 @@ function toFileId(absolutePath, projectDir) {
160
164
  return path_1.default.relative(projectDir, withMd);
161
165
  return null;
162
166
  }
167
+ /**
168
+ * Cache: projectDir → Map<lowercased basename (without .md), absolute path>.
169
+ * Built once per project via a single recursive walk, then reused for all wiki link lookups.
170
+ */
171
+ const _wikiIndex = new Map();
172
+ /** Clear cached wiki link index (call when files change or between projects). */
173
+ function clearWikiIndexCache(projectDir) {
174
+ if (projectDir)
175
+ _wikiIndex.delete(projectDir);
176
+ else
177
+ _wikiIndex.clear();
178
+ }
179
+ function getWikiIndex(projectDir) {
180
+ if (_wikiIndex.has(projectDir))
181
+ return _wikiIndex.get(projectDir);
182
+ const index = new Map();
183
+ const MAX_DEPTH = defaults_1.WIKI_MAX_DEPTH;
184
+ function walk(dir, depth) {
185
+ if (depth >= MAX_DEPTH)
186
+ return;
187
+ let entries;
188
+ try {
189
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
190
+ }
191
+ catch {
192
+ return;
193
+ }
194
+ for (const entry of entries) {
195
+ if (entry.name.startsWith('.') || entry.isSymbolicLink())
196
+ continue;
197
+ const full = path_1.default.join(dir, entry.name);
198
+ if (entry.isDirectory()) {
199
+ walk(full, depth + 1);
200
+ }
201
+ else if (entry.isFile()) {
202
+ // Index by basename without extension (lowercased)
203
+ const key = path_1.default.basename(entry.name, path_1.default.extname(entry.name)).toLowerCase();
204
+ if (!index.has(key))
205
+ index.set(key, full);
206
+ // Also index by full filename (lowercased)
207
+ const fullKey = entry.name.toLowerCase();
208
+ if (!index.has(fullKey))
209
+ index.set(fullKey, full);
210
+ }
211
+ }
212
+ }
213
+ walk(projectDir, 0);
214
+ _wikiIndex.set(projectDir, index);
215
+ return index;
216
+ }
163
217
  function findWikiFile(name, projectDir) {
218
+ // Direct path check first (fast path)
164
219
  const direct = path_1.default.join(projectDir, name);
165
220
  if (fs_1.default.existsSync(direct))
166
221
  return direct;
167
222
  const withMd = direct + '.md';
168
223
  if (fs_1.default.existsSync(withMd))
169
224
  return withMd;
170
- return searchRecursive(name, projectDir);
171
- }
172
- const MAX_SEARCH_DEPTH = 10;
173
- function searchRecursive(name, dir, depth = 0) {
174
- if (depth >= MAX_SEARCH_DEPTH)
175
- return null;
176
- let entries;
177
- try {
178
- entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
179
- }
180
- catch {
181
- return null;
182
- }
183
- for (const entry of entries) {
184
- if (entry.name.startsWith('.'))
185
- continue;
186
- const full = path_1.default.join(dir, entry.name);
187
- if (entry.isDirectory()) {
188
- const found = searchRecursive(name, full, depth + 1);
189
- if (found)
190
- return found;
191
- }
192
- else if (entry.isFile()) {
193
- if (entry.name === name || entry.name === `${name}.md` || path_1.default.basename(entry.name, '.md') === name) {
194
- return full;
195
- }
196
- }
197
- }
198
- return null;
225
+ // Fall back to cached index lookup
226
+ const index = getWikiIndex(projectDir);
227
+ return index.get(name.toLowerCase()) ?? index.get(name.toLowerCase() + '.md') ?? null;
199
228
  }
@@ -48,7 +48,7 @@ function isLanguageSupported(languageName) {
48
48
  }
49
49
  /** Reusable parser per language (avoids WASM memory leak from creating Parser on every call). */
50
50
  const parsers = new Map();
51
- /** Parse source code with the appropriate language grammar. Returns root node or null. */
51
+ /** Parse source code with the appropriate language grammar. Returns tree (caller must call tree.delete() when done) or null. */
52
52
  async function parseSource(code, languageName) {
53
53
  const entry = languages.get(languageName);
54
54
  if (!entry)
@@ -62,7 +62,7 @@ async function parseSource(code, languageName) {
62
62
  parsers.set(languageName, parser);
63
63
  }
64
64
  const tree = parser.parse(code);
65
- return tree?.rootNode ?? null;
65
+ return tree ?? null;
66
66
  }
67
67
  /** Get the mapper for a language. Returns undefined for unsupported languages. */
68
68
  function getMapper(languageName) {
@@ -2,22 +2,74 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerTypescript = registerTypescript;
4
4
  const registry_1 = require("./registry");
5
+ const defaults_1 = require("../../../lib/defaults");
5
6
  /** Get the previous named sibling that is a JSDoc comment. */
6
7
  function getDocComment(node) {
7
- let prev = node.previousSibling;
8
+ let prev = node.previousNamedSibling;
8
9
  while (prev && prev.type === 'comment' && !prev.text.startsWith('/**')) {
9
- prev = prev.previousSibling;
10
+ prev = prev.previousNamedSibling;
10
11
  }
11
12
  if (prev && prev.type === 'comment' && prev.text.startsWith('/**')) {
12
13
  return prev.text.trim();
13
14
  }
14
15
  return '';
15
16
  }
16
- /** Build a signature: first line of the node text (truncated to 200 chars). */
17
- function buildSignature(node) {
18
- const text = node.text ?? '';
19
- const firstLine = text.split('\n')[0].trim();
20
- return firstLine.length > 200 ? firstLine.slice(0, 200) + '…' : firstLine;
17
+ /** Collapse whitespace and truncate. */
18
+ function truncate(text, maxLen = defaults_1.SIGNATURE_MAX_LEN) {
19
+ const collapsed = text.replace(/\s+/g, ' ').trim();
20
+ return collapsed.length > maxLen ? collapsed.slice(0, maxLen) + '' : collapsed;
21
+ }
22
+ /**
23
+ * Slice outerNode.text up to where bodyNode begins, using line-based
24
+ * slicing to avoid tree-sitter byte-offset vs JS char-offset mismatch
25
+ * (startIndex is UTF-8 bytes, String.slice uses UTF-16 code units).
26
+ */
27
+ function sliceBeforeBody(outerNode, bodyNode) {
28
+ const text = outerNode.text ?? '';
29
+ const outerStartRow = outerNode.startPosition.row;
30
+ const bodyStartRow = bodyNode.startPosition.row;
31
+ if (bodyStartRow > outerStartRow) {
32
+ const lines = text.split('\n');
33
+ const relativeRow = bodyStartRow - outerStartRow;
34
+ const beforeBody = lines.slice(0, relativeRow);
35
+ const bodyLine = lines[relativeRow] ?? '';
36
+ const braceIdx = bodyLine.indexOf('{');
37
+ if (braceIdx >= 0)
38
+ beforeBody.push(bodyLine.slice(0, braceIdx));
39
+ return beforeBody.join('\n');
40
+ }
41
+ const braceIdx = text.indexOf('{');
42
+ if (braceIdx > 0)
43
+ return text.slice(0, braceIdx);
44
+ return null;
45
+ }
46
+ /**
47
+ * Build signature by taking everything before the body node.
48
+ * Falls back to first line if no body found.
49
+ */
50
+ function buildSignature(outerNode, innerNode) {
51
+ const bodyNode = innerNode.childForFieldName('body');
52
+ const text = outerNode.text ?? '';
53
+ if (!bodyNode)
54
+ return truncate(text);
55
+ const header = sliceBeforeBody(outerNode, bodyNode);
56
+ return truncate(header ?? text.split('\n')[0]);
57
+ }
58
+ /**
59
+ * Build signature for variable declarations.
60
+ * If value is arrow/function, strip the function body.
61
+ */
62
+ function buildVariableSignature(outerNode, declarator) {
63
+ const value = declarator.childForFieldName('value');
64
+ if (value) {
65
+ const valueBody = value.childForFieldName('body');
66
+ if (valueBody) {
67
+ const header = sliceBeforeBody(outerNode, valueBody);
68
+ if (header)
69
+ return truncate(header);
70
+ }
71
+ }
72
+ return truncate(outerNode.text ?? '');
21
73
  }
22
74
  /** Build the full body text for a symbol. Includes JSDoc + node text. */
23
75
  function buildBody(node, docComment) {
@@ -26,10 +78,6 @@ function buildBody(node, docComment) {
26
78
  }
27
79
  return node.text ?? '';
28
80
  }
29
- /** Build a signature: always use the code declaration, not JSDoc. */
30
- function buildFullSignature(node, _docComment) {
31
- return buildSignature(node);
32
- }
33
81
  /** Check if a node is inside an export_statement. */
34
82
  function isExported(node) {
35
83
  const parent = node.parent;
@@ -49,76 +97,171 @@ function startLine(node) {
49
97
  function endLine(node) {
50
98
  return (node.endPosition?.row ?? 0) + 1;
51
99
  }
100
+ /**
101
+ * Extract the base type name from a type node, handling generic types.
102
+ * `Foo` → "Foo", `Foo<T>` (generic_type) → "Foo"
103
+ */
104
+ function extractTypeName(node) {
105
+ if (node.type === 'identifier' || node.type === 'type_identifier') {
106
+ return node.text;
107
+ }
108
+ if (node.type === 'generic_type') {
109
+ const name = node.childForFieldName('name') ?? node.namedChildren?.[0];
110
+ if (name && (name.type === 'identifier' || name.type === 'type_identifier')) {
111
+ return name.text;
112
+ }
113
+ }
114
+ return null;
115
+ }
52
116
  // ---------------------------------------------------------------------------
53
117
  // Symbol extraction
54
118
  // ---------------------------------------------------------------------------
55
- function extractClassSymbol(node) {
56
- const outer = getOuterNode(node);
57
- const doc = getDocComment(outer);
58
- const name = node.childForFieldName('name')?.text ?? '';
59
- const body = node.childForFieldName('body');
60
- // Extract methods
119
+ /** Extract class members: methods, fields, getters/setters. */
120
+ function extractClassMembers(body) {
61
121
  const children = [];
62
- if (body) {
63
- for (const member of body.namedChildren) {
64
- if (member.type === 'method_definition') {
122
+ if (!body)
123
+ return children;
124
+ for (const member of body.namedChildren) {
125
+ switch (member.type) {
126
+ case 'method_definition':
127
+ case 'abstract_method_definition':
128
+ case 'abstract_method_signature': {
65
129
  const methodName = member.childForFieldName('name')?.text ?? '';
66
130
  if (!methodName)
67
131
  continue;
68
132
  const methodDoc = getDocComment(member);
69
133
  children.push({
70
134
  name: methodName,
71
- kind: 'method',
72
- signature: buildSignature(member),
135
+ kind: methodName === 'constructor' ? 'constructor' : 'method',
136
+ signature: buildSignature(member, member),
73
137
  docComment: methodDoc,
74
138
  body: buildBody(member, methodDoc),
75
139
  startLine: startLine(member),
76
140
  endLine: endLine(member),
77
141
  isExported: false,
78
142
  });
143
+ break;
144
+ }
145
+ case 'public_field_definition':
146
+ case 'property_definition': {
147
+ const fieldName = member.childForFieldName('name')?.text ?? '';
148
+ if (!fieldName)
149
+ continue;
150
+ const fieldDoc = getDocComment(member);
151
+ children.push({
152
+ name: fieldName,
153
+ kind: 'variable',
154
+ signature: truncate(member.text ?? ''),
155
+ docComment: fieldDoc,
156
+ body: buildBody(member, fieldDoc),
157
+ startLine: startLine(member),
158
+ endLine: endLine(member),
159
+ isExported: false,
160
+ });
161
+ break;
79
162
  }
80
163
  }
81
164
  }
165
+ return children;
166
+ }
167
+ function extractClassSymbol(node) {
168
+ const outer = getOuterNode(node);
169
+ const doc = getDocComment(outer);
170
+ const name = node.childForFieldName('name')?.text ?? '';
171
+ const body = node.childForFieldName('body');
172
+ const children = extractClassMembers(body);
82
173
  return {
83
174
  name,
84
175
  kind: 'class',
85
- signature: buildFullSignature(outer, doc),
176
+ signature: buildSignature(outer, node),
86
177
  docComment: doc,
87
178
  body: buildBody(outer, doc),
88
179
  startLine: startLine(outer),
89
180
  endLine: endLine(outer),
90
181
  isExported: isExported(node),
91
- children,
182
+ children: children.length > 0 ? children : undefined,
92
183
  };
93
184
  }
185
+ /** Extract nested named function declarations from a function body (1 level deep). */
186
+ function extractNestedFunctions(body) {
187
+ const nested = [];
188
+ if (!body || body.type !== 'statement_block')
189
+ return nested;
190
+ for (const stmt of body.namedChildren) {
191
+ if (stmt.type === 'function_declaration') {
192
+ const childName = stmt.childForFieldName('name')?.text;
193
+ if (!childName)
194
+ continue;
195
+ const childDoc = getDocComment(stmt);
196
+ nested.push({
197
+ name: childName,
198
+ kind: 'function',
199
+ signature: buildSignature(stmt, stmt),
200
+ docComment: childDoc,
201
+ body: buildBody(stmt, childDoc),
202
+ startLine: startLine(stmt),
203
+ endLine: endLine(stmt),
204
+ isExported: false,
205
+ });
206
+ }
207
+ }
208
+ return nested;
209
+ }
94
210
  function extractFunctionSymbol(node) {
95
211
  const outer = getOuterNode(node);
96
212
  const doc = getDocComment(outer);
97
213
  const name = node.childForFieldName('name')?.text ?? '';
214
+ const body = node.childForFieldName('body');
215
+ const children = extractNestedFunctions(body);
98
216
  return {
99
217
  name,
100
218
  kind: 'function',
101
- signature: buildFullSignature(outer, doc),
219
+ signature: buildSignature(outer, node),
102
220
  docComment: doc,
103
221
  body: buildBody(outer, doc),
104
222
  startLine: startLine(outer),
105
223
  endLine: endLine(outer),
106
224
  isExported: isExported(node),
225
+ children: children.length > 0 ? children : undefined,
107
226
  };
108
227
  }
109
228
  function extractInterfaceSymbol(node) {
110
229
  const outer = getOuterNode(node);
111
230
  const doc = getDocComment(outer);
112
231
  const name = node.childForFieldName('name')?.text ?? '';
232
+ // Extract interface members
233
+ const children = [];
234
+ const body = node.childForFieldName('body');
235
+ if (body) {
236
+ for (const member of body.namedChildren) {
237
+ if (member.type === 'property_signature' || member.type === 'method_signature') {
238
+ const memberName = member.childForFieldName('name')?.text ?? '';
239
+ if (!memberName)
240
+ continue;
241
+ const memberDoc = getDocComment(member);
242
+ children.push({
243
+ name: memberName,
244
+ kind: member.type === 'method_signature' ? 'method' : 'variable',
245
+ signature: truncate(member.text ?? ''),
246
+ docComment: memberDoc,
247
+ body: buildBody(member, memberDoc),
248
+ startLine: startLine(member),
249
+ endLine: endLine(member),
250
+ isExported: false,
251
+ });
252
+ }
253
+ }
254
+ }
113
255
  return {
114
256
  name,
115
257
  kind: 'interface',
116
- signature: buildFullSignature(outer, doc),
258
+ signature: buildSignature(outer, node),
117
259
  docComment: doc,
118
260
  body: buildBody(outer, doc),
119
261
  startLine: startLine(outer),
120
262
  endLine: endLine(outer),
121
263
  isExported: isExported(node),
264
+ children: children.length > 0 ? children : undefined,
122
265
  };
123
266
  }
124
267
  function extractTypeAliasSymbol(node) {
@@ -128,7 +271,7 @@ function extractTypeAliasSymbol(node) {
128
271
  return {
129
272
  name,
130
273
  kind: 'type',
131
- signature: buildFullSignature(outer, doc),
274
+ signature: truncate(outer.text ?? ''),
132
275
  docComment: doc,
133
276
  body: buildBody(outer, doc),
134
277
  startLine: startLine(outer),
@@ -143,7 +286,7 @@ function extractEnumSymbol(node) {
143
286
  return {
144
287
  name,
145
288
  kind: 'enum',
146
- signature: buildFullSignature(outer, doc),
289
+ signature: buildSignature(outer, node),
147
290
  docComment: doc,
148
291
  body: buildBody(outer, doc),
149
292
  startLine: startLine(outer),
@@ -155,7 +298,6 @@ function extractVariableSymbols(node, exported) {
155
298
  const outer = exported ? node.parent : node;
156
299
  const doc = getDocComment(outer);
157
300
  const symbols = [];
158
- // Find all variable_declarator children
159
301
  for (const child of node.namedChildren) {
160
302
  if (child.type === 'variable_declarator') {
161
303
  const name = child.childForFieldName('name')?.text ?? '';
@@ -163,15 +305,24 @@ function extractVariableSymbols(node, exported) {
163
305
  continue;
164
306
  const value = child.childForFieldName('value');
165
307
  const isArrow = value?.type === 'arrow_function' || value?.type === 'function_expression';
308
+ // Extract nested named functions from arrow/function body
309
+ let children;
310
+ if (isArrow && value) {
311
+ const fnBody = value.childForFieldName('body');
312
+ const nested = extractNestedFunctions(fnBody);
313
+ if (nested.length > 0)
314
+ children = nested;
315
+ }
166
316
  symbols.push({
167
317
  name,
168
318
  kind: isArrow ? 'function' : 'variable',
169
- signature: buildFullSignature(outer, doc),
319
+ signature: buildVariableSignature(outer, child),
170
320
  docComment: doc,
171
321
  body: buildBody(outer, doc),
172
322
  startLine: startLine(outer),
173
323
  endLine: endLine(outer),
174
324
  isExported: exported,
325
+ children,
175
326
  });
176
327
  }
177
328
  }
@@ -182,13 +333,15 @@ function extractVariableSymbols(node, exported) {
182
333
  // ---------------------------------------------------------------------------
183
334
  function processTopLevel(node) {
184
335
  switch (node.type) {
185
- case 'function_declaration': {
336
+ case 'function_declaration':
337
+ case 'function_signature': {
186
338
  const name = node.childForFieldName('name')?.text;
187
339
  if (!name)
188
340
  return [];
189
341
  return [extractFunctionSymbol(node)];
190
342
  }
191
- case 'class_declaration': {
343
+ case 'class_declaration':
344
+ case 'abstract_class_declaration': {
192
345
  const name = node.childForFieldName('name')?.text;
193
346
  if (!name)
194
347
  return [];
@@ -218,6 +371,17 @@ function processTopLevel(node) {
218
371
  }
219
372
  return [];
220
373
  }
374
+ case 'ambient_declaration': {
375
+ // declare function/class/interface/etc — unwrap and process inner declaration
376
+ for (const child of node.namedChildren) {
377
+ if (child.type === 'comment')
378
+ continue;
379
+ const results = processTopLevel(child);
380
+ if (results.length > 0)
381
+ return results;
382
+ }
383
+ return [];
384
+ }
221
385
  default:
222
386
  return [];
223
387
  }
@@ -233,27 +397,27 @@ const typescriptMapper = {
233
397
  extractEdges(rootNode) {
234
398
  const edges = [];
235
399
  function findClasses(node) {
236
- if (node.type === 'class_declaration') {
400
+ if (node.type === 'class_declaration' || node.type === 'abstract_class_declaration') {
237
401
  const className = node.childForFieldName('name')?.text;
238
402
  if (!className)
239
403
  return;
240
- // Look for class_heritage
241
404
  for (const child of node.namedChildren) {
242
405
  if (child.type === 'class_heritage') {
243
406
  for (const clause of child.namedChildren) {
244
407
  if (clause.type === 'extends_clause') {
245
- // First named child is the base class
246
408
  for (const c of clause.namedChildren) {
247
- if (c.type === 'identifier' || c.type === 'type_identifier') {
248
- edges.push({ fromName: className, toName: c.text, kind: 'extends' });
249
- break;
409
+ const typeName = extractTypeName(c);
410
+ if (typeName) {
411
+ edges.push({ fromName: className, toName: typeName, kind: 'extends' });
412
+ break; // Only one base class
250
413
  }
251
414
  }
252
415
  }
253
416
  if (clause.type === 'implements_clause') {
254
417
  for (const c of clause.namedChildren) {
255
- if (c.type === 'type_identifier' || c.type === 'identifier') {
256
- edges.push({ fromName: className, toName: c.text, kind: 'implements' });
418
+ const typeName = extractTypeName(c);
419
+ if (typeName) {
420
+ edges.push({ fromName: className, toName: typeName, kind: 'implements' });
257
421
  }
258
422
  }
259
423
  }
@@ -271,14 +435,20 @@ const typescriptMapper = {
271
435
  extractImports(rootNode) {
272
436
  const imports = [];
273
437
  for (const child of rootNode.children) {
438
+ // import statements
274
439
  if (child.type === 'import_statement') {
275
440
  const source = child.childForFieldName('source');
276
441
  if (source) {
277
- // Remove quotes from string literal
278
442
  const specifier = source.text.replace(/^['"]|['"]$/g, '');
279
- if (specifier.startsWith('./') || specifier.startsWith('../')) {
280
- imports.push({ specifier });
281
- }
443
+ imports.push({ specifier });
444
+ }
445
+ }
446
+ // Re-exports: export { ... } from '...' and export * from '...'
447
+ if (child.type === 'export_statement') {
448
+ const source = child.childForFieldName('source');
449
+ if (source) {
450
+ const specifier = source.text.replace(/^['"]|['"]$/g, '');
451
+ imports.push({ specifier });
282
452
  }
283
453
  }
284
454
  }
@@ -293,10 +463,8 @@ function registerTypescript() {
293
463
  if (_registered)
294
464
  return;
295
465
  _registered = true;
296
- // TypeScript and TSX share the same mapper
297
466
  (0, registry_1.registerLanguage)('typescript', 'tree-sitter-typescript.wasm', typescriptMapper);
298
467
  (0, registry_1.registerLanguage)('tsx', 'tree-sitter-tsx.wasm', typescriptMapper);
299
- // JavaScript and JSX use the same mapper (TS is a superset)
300
468
  (0, registry_1.registerLanguage)('javascript', 'tree-sitter-javascript.wasm', typescriptMapper);
301
469
  (0, registry_1.registerLanguage)('jsx', 'tree-sitter-javascript.wasm', typescriptMapper);
302
470
  }
@@ -13,11 +13,14 @@ const file_index_1 = require("../graphs/file-index");
13
13
  const task_1 = require("../graphs/task");
14
14
  const skill_1 = require("../graphs/skill");
15
15
  const indexer_1 = require("../cli/indexer");
16
+ const code_2 = require("../lib/parsers/code");
17
+ const docs_2 = require("../lib/parsers/docs");
16
18
  const promise_queue_1 = require("../lib/promise-queue");
17
19
  const multi_config_1 = require("../lib/multi-config");
18
20
  const mirror_watcher_1 = require("../lib/mirror-watcher");
19
21
  const team_1 = require("../lib/team");
20
22
  const path_1 = __importDefault(require("path"));
23
+ const defaults_1 = require("../lib/defaults");
21
24
  // ---------------------------------------------------------------------------
22
25
  // ProjectManager
23
26
  // ---------------------------------------------------------------------------
@@ -264,6 +267,9 @@ class ProjectManager extends events_1.EventEmitter {
264
267
  const instance = this.projects.get(id);
265
268
  if (!instance)
266
269
  throw new Error(`Project "${id}" not found`);
270
+ // Clear parser caches to prevent cross-project leaks in multi-project mode
271
+ (0, code_2.clearPathMappingsCache)();
272
+ (0, docs_2.clearWikiIndexCache)();
267
273
  const gc = instance.config.graphConfigs;
268
274
  const indexer = (0, indexer_1.createProjectIndexer)(instance.docGraph, instance.codeGraph, {
269
275
  projectId: id,
@@ -287,17 +293,21 @@ class ProjectManager extends events_1.EventEmitter {
287
293
  this.saveProject(instance);
288
294
  instance.dirty = false;
289
295
  // Scan and watch .notes/ and .tasks/ for reverse import (skip for workspace projects — handled by workspace)
296
+ // Skip mirror entirely if knowledge or tasks graph is readonly (mirror requires both)
290
297
  if (instance.mirrorTracker && !instance.workspaceId && instance.knowledgeManager && instance.taskManager) {
291
- const mirrorConfig = {
292
- projectDir: instance.config.projectDir,
293
- knowledgeManager: instance.knowledgeManager,
294
- taskManager: instance.taskManager,
295
- skillManager: instance.skillManager,
296
- mutationQueue: instance.mutationQueue,
297
- tracker: instance.mirrorTracker,
298
- };
299
- await (0, mirror_watcher_1.scanMirrorDirs)(mirrorConfig);
300
- instance.mirrorWatcher = (0, mirror_watcher_1.startMirrorWatcher)(mirrorConfig);
298
+ const gc = instance.config.graphConfigs;
299
+ if (!gc.knowledge.readonly && !gc.tasks.readonly) {
300
+ const mirrorConfig = {
301
+ projectDir: instance.config.projectDir,
302
+ knowledgeManager: instance.knowledgeManager,
303
+ taskManager: instance.taskManager,
304
+ skillManager: gc.skills.readonly ? undefined : instance.skillManager,
305
+ mutationQueue: instance.mutationQueue,
306
+ tracker: instance.mirrorTracker,
307
+ };
308
+ await (0, mirror_watcher_1.scanMirrorDirs)(mirrorConfig);
309
+ instance.mirrorWatcher = (0, mirror_watcher_1.startMirrorWatcher)(mirrorConfig);
310
+ }
301
311
  }
302
312
  this.emit('project:indexed', { projectId: id });
303
313
  process.stderr.write(`[project-manager] Project "${id}" indexed\n`);
@@ -354,7 +364,7 @@ class ProjectManager extends events_1.EventEmitter {
354
364
  /**
355
365
  * Start auto-save interval (every intervalMs, save dirty projects).
356
366
  */
357
- startAutoSave(intervalMs = 30_000) {
367
+ startAutoSave(intervalMs = defaults_1.AUTO_SAVE_INTERVAL_MS) {
358
368
  this.autoSaveInterval = setInterval(() => {
359
369
  for (const instance of this.projects.values()) {
360
370
  if (instance.dirty) {