@danielblomma/cortex-mcp 0.6.4 → 1.0.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/README.md CHANGED
@@ -131,6 +131,59 @@ Codex (`~/.config/codex/mcp-config.json`):
131
131
  }
132
132
  ```
133
133
 
134
+ ## WSL Mode (Windows)
135
+
136
+ If you run Node.js inside WSL but use Claude Desktop or another MCP client on Windows:
137
+
138
+ 1. Install Cortex inside WSL:
139
+
140
+ ```bash
141
+ # In a WSL terminal
142
+ npm i -g @danielblomma/cortex-mcp
143
+ cd /mnt/c/Users/yourname/your-project
144
+ cortex init --bootstrap
145
+ ```
146
+
147
+ 2. Configure Claude Desktop (`%APPDATA%\Claude\claude_desktop_config.json`):
148
+
149
+ ```json
150
+ {
151
+ "mcpServers": {
152
+ "cortex": {
153
+ "command": "wsl.exe",
154
+ "args": ["--distribution", "Ubuntu", "--exec", "cortex", "mcp"],
155
+ "env": {
156
+ "CORTEX_PROJECT_ROOT": "C:\\Users\\yourname\\your-project",
157
+ "CORTEX_AUTO_BOOTSTRAP_ON_MCP": "1"
158
+ }
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ Cortex automatically converts Windows paths (e.g. `C:\Users\...`) to WSL paths (`/mnt/c/Users/...`).
165
+
166
+ For projects on the WSL filesystem (e.g. `~/projects/myapp`), use the WSL path directly:
167
+
168
+ ```json
169
+ {
170
+ "mcpServers": {
171
+ "cortex": {
172
+ "command": "wsl.exe",
173
+ "args": ["--distribution", "Ubuntu", "--exec", "cortex", "mcp"],
174
+ "env": {
175
+ "CORTEX_PROJECT_ROOT": "/home/yourname/projects/myapp",
176
+ "CORTEX_AUTO_BOOTSTRAP_ON_MCP": "1"
177
+ }
178
+ }
179
+ }
180
+ }
181
+ ```
182
+
183
+ **Notes:**
184
+ - File watching on `/mnt/` paths (Windows filesystem) automatically uses poll mode since `inotify` is unreliable across filesystem boundaries.
185
+ - For best performance, keep projects on the WSL filesystem (`~/...`) rather than `/mnt/c/...`.
186
+
134
187
  ## MCP Tools
135
188
 
136
189
  ### `context.search`
@@ -154,6 +207,40 @@ Input:
154
207
  - `depth` (int, 1-3, default `1`)
155
208
  - `include_edges` (bool, default `true`)
156
209
 
210
+ ### `context.find_callers`
211
+
212
+ Return chunk callers for a chunk or file entity using the indexed call graph.
213
+
214
+ Input:
215
+
216
+ - `entity_id` (string, required)
217
+ - `depth` (int, 1-4, default `1`)
218
+ - `include_edges` (bool, default `true`)
219
+
220
+ ### `context.trace_calls`
221
+
222
+ Trace call graph neighbors from a chunk or file entity in the requested direction.
223
+
224
+ Input:
225
+
226
+ - `entity_id` (string, required)
227
+ - `depth` (int, 1-4, default `2`)
228
+ - `direction` (`"outgoing"` | `"incoming"` | `"both"`, default `"outgoing"`)
229
+ - `include_edges` (bool, default `true`)
230
+
231
+ ### `context.impact_analysis`
232
+
233
+ Analyze likely impacted call-graph entities starting from an entity id or search query.
234
+
235
+ Input:
236
+
237
+ - `entity_id` (string, optional) — either `entity_id` or `query` is required
238
+ - `query` (string, optional)
239
+ - `depth` (int, 1-4, default `2`)
240
+ - `top_k` (int, 1-20, default `8`)
241
+ - `direction` (`"incoming"` | `"outgoing"` | `"both"`, default `"incoming"`)
242
+ - `include_edges` (bool, default `true`)
243
+
157
244
  ### `context.get_rules`
158
245
 
159
246
  List indexed rules and optionally include inactive rules.
package/bin/cortex.mjs CHANGED
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { spawn } from "node:child_process";
6
+ import { normalizeProjectRoot } from "./wsl.mjs";
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
@@ -599,9 +600,9 @@ async function run() {
599
600
  }
600
601
 
601
602
  if (command === "mcp") {
602
- const target = process.env.CORTEX_PROJECT_ROOT
603
- ? path.resolve(process.env.CORTEX_PROJECT_ROOT)
604
- : process.cwd();
603
+ const rawTarget = process.env.CORTEX_PROJECT_ROOT || process.cwd();
604
+ const target = path.resolve(normalizeProjectRoot(rawTarget));
605
+ process.env.CORTEX_PROJECT_ROOT = target;
605
606
  await ensureProjectInitializedForMcp(target);
606
607
  ensureProjectInitialized(target);
607
608
  const serverEntry = path.join(target, "mcp", "dist", "server.js");
package/bin/wsl.mjs ADDED
@@ -0,0 +1,30 @@
1
+ import fs from "node:fs";
2
+
3
+ let _isWSL = null;
4
+
5
+ export function isWSL() {
6
+ if (_isWSL !== null) return _isWSL;
7
+ try {
8
+ const version = fs.readFileSync("/proc/version", "utf8");
9
+ _isWSL = /microsoft|wsl/i.test(version);
10
+ } catch {
11
+ _isWSL = false;
12
+ }
13
+ return _isWSL;
14
+ }
15
+
16
+ export function windowsToWslPath(winPath) {
17
+ const match = winPath.match(/^([A-Za-z]):[/\\](.*)/);
18
+ if (!match) return winPath;
19
+ const drive = match[1].toLowerCase();
20
+ const rest = match[2].replace(/\\/g, "/").replace(/\/+$/, "");
21
+ return `/mnt/${drive}/${rest}`;
22
+ }
23
+
24
+ export function normalizeProjectRoot(rawPath) {
25
+ if (!isWSL()) return rawPath;
26
+ if (/^[A-Za-z]:[/\\]/.test(rawPath)) {
27
+ return windowsToWslPath(rawPath);
28
+ }
29
+ return rawPath;
30
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@danielblomma/cortex-mcp",
3
3
  "mcpName": "io.github.DanielBlomma/cortex",
4
- "version": "0.6.4",
4
+ "version": "1.0.0",
5
5
  "description": "Local, repo-scoped context platform for coding assistants. Semantic search, graph relationships, and architectural rule context.",
6
6
  "type": "module",
7
7
  "author": "Daniel Blomma",
@@ -20,7 +20,8 @@
20
20
  "overrides": {
21
21
  "cmake-js": "^8.0.0",
22
22
  "express-rate-limit": "^8.3.1",
23
- "hono": "^4.12.7",
23
+ "hono": "^4.12.12",
24
+ "@hono/node-server": "^1.19.13",
24
25
  "tar": "^7.5.11"
25
26
  },
26
27
  "devDependencies": {
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import type { RankingWeights } from "./types.js";
@@ -5,9 +6,23 @@ import type { RankingWeights } from "./types.js";
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = path.dirname(__filename);
7
8
 
9
+ function normalizeForWsl(rawPath: string): string {
10
+ const winMatch = rawPath.match(/^([A-Za-z]):[/\\](.*)/);
11
+ if (!winMatch) return rawPath;
12
+ try {
13
+ const version = fs.readFileSync("/proc/version", "utf8");
14
+ if (!/microsoft|wsl/i.test(version)) return rawPath;
15
+ } catch {
16
+ return rawPath;
17
+ }
18
+ const drive = winMatch[1].toLowerCase();
19
+ const rest = winMatch[2].replace(/\\/g, "/").replace(/\/+$/, "");
20
+ return `/mnt/${drive}/${rest}`;
21
+ }
22
+
8
23
  const PROJECT_ROOT_OVERRIDE = process.env.CORTEX_PROJECT_ROOT?.trim();
9
24
  export const REPO_ROOT = PROJECT_ROOT_OVERRIDE
10
- ? path.resolve(PROJECT_ROOT_OVERRIDE)
25
+ ? path.resolve(normalizeForWsl(PROJECT_ROOT_OVERRIDE))
11
26
  : path.resolve(__dirname, "../..");
12
27
  export const CONTEXT_DIR = path.join(REPO_ROOT, ".context");
13
28
  export const CACHE_DIR = path.join(CONTEXT_DIR, "cache");
@@ -16,6 +16,7 @@ import {
16
16
  import { parseCode as parseConfigCode } from "./parsers/config.mjs";
17
17
  import { parseCode as parseResourcesCode } from "./parsers/resources.mjs";
18
18
  import { parseCode as parseSqlCode } from "./parsers/sql.mjs";
19
+ import { parseCode as parseRustCode } from "./parsers/rust.mjs";
19
20
 
20
21
  const __filename = fileURLToPath(import.meta.url);
21
22
  const __dirname = path.dirname(__filename);
@@ -273,6 +274,13 @@ const CHUNK_PARSERS = new Map([
273
274
  parse: parseCppCode,
274
275
  isAvailable: isCppParserAvailable
275
276
  }
277
+ ],
278
+ [
279
+ ".rs",
280
+ {
281
+ language: "rust",
282
+ parse: parseRustCode
283
+ }
276
284
  ]
277
285
  ]);
278
286
 
@@ -6,8 +6,17 @@ import { parseFrontmatter, parseStringList } from "../mcp/dist/frontmatter.js";
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
+
10
+ function normalizeForWsl(rawPath) {
11
+ const m = rawPath.match(/^([A-Za-z]):[/\\](.*)/);
12
+ if (!m) return rawPath;
13
+ try { if (!/microsoft|wsl/i.test(fs.readFileSync("/proc/version", "utf8"))) return rawPath; }
14
+ catch { return rawPath; }
15
+ return `/mnt/${m[1].toLowerCase()}/${m[2].replace(/\\/g, "/").replace(/\/+$/, "")}`;
16
+ }
17
+
9
18
  const REPO_ROOT = process.env.CORTEX_PROJECT_ROOT
10
- ? path.resolve(process.env.CORTEX_PROJECT_ROOT)
19
+ ? path.resolve(normalizeForWsl(process.env.CORTEX_PROJECT_ROOT))
11
20
  : path.resolve(__dirname, "..");
12
21
  const CONTEXT_DIR = path.join(REPO_ROOT, ".context");
13
22
  const MEMORY_DIR = path.join(CONTEXT_DIR, "memory");
@@ -6,8 +6,17 @@ import { parseFrontmatter, parseStringList } from "../mcp/dist/frontmatter.js";
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
+
10
+ function normalizeForWsl(rawPath) {
11
+ const m = rawPath.match(/^([A-Za-z]):[/\\](.*)/);
12
+ if (!m) return rawPath;
13
+ try { if (!/microsoft|wsl/i.test(fs.readFileSync("/proc/version", "utf8"))) return rawPath; }
14
+ catch { return rawPath; }
15
+ return `/mnt/${m[1].toLowerCase()}/${m[2].replace(/\\/g, "/").replace(/\/+$/, "")}`;
16
+ }
17
+
9
18
  const REPO_ROOT = process.env.CORTEX_PROJECT_ROOT
10
- ? path.resolve(process.env.CORTEX_PROJECT_ROOT)
19
+ ? path.resolve(normalizeForWsl(process.env.CORTEX_PROJECT_ROOT))
11
20
  : path.resolve(__dirname, "..");
12
21
  const CONTEXT_DIR = path.join(REPO_ROOT, ".context");
13
22
  const MEMORY_DIR = path.join(CONTEXT_DIR, "memory");
@@ -0,0 +1,515 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Regex-based Rust parser for Cortex.
4
+ *
5
+ * Extracts semantic chunks from Rust source files: functions, structs, enums,
6
+ * traits, impl blocks (with methods), inline modules, macro_rules! definitions,
7
+ * use imports, and call relationships.
8
+ *
9
+ * No external dependencies — pure regex, always available.
10
+ */
11
+
12
+ const CALL_KEYWORDS = new Set([
13
+ "if", "for", "while", "loop", "match", "return",
14
+ "Some", "None", "Ok", "Err", "Box", "Vec", "String",
15
+ "println", "eprintln", "format", "write", "writeln",
16
+ "panic", "todo", "unimplemented", "unreachable",
17
+ "assert", "assert_eq", "assert_ne", "debug_assert",
18
+ "debug_assert_eq", "debug_assert_ne",
19
+ "cfg", "derive", "allow", "warn", "deny"
20
+ ]);
21
+
22
+ const VIS_PREFIX = /(?:pub(?:\s*\([^)]*\))?\s+)?/;
23
+ const VIS_PREFIX_SRC = VIS_PREFIX.source;
24
+ const LINE_START = "^[^\\S\\n]*";
25
+
26
+ const FN_PATTERN = new RegExp(
27
+ `${LINE_START}${VIS_PREFIX_SRC}(?:default\\s+)?(?:async\\s+)?(?:unsafe\\s+)?(?:const\\s+)?(?:extern\\s+"[^"]*"\\s+)?fn\\s+([A-Za-z_]\\w*)`,
28
+ "gm"
29
+ );
30
+
31
+ const STRUCT_PATTERN = new RegExp(
32
+ `${LINE_START}${VIS_PREFIX_SRC}struct\\s+([A-Za-z_]\\w*)`,
33
+ "gm"
34
+ );
35
+
36
+ const ENUM_PATTERN = new RegExp(
37
+ `${LINE_START}${VIS_PREFIX_SRC}enum\\s+([A-Za-z_]\\w*)`,
38
+ "gm"
39
+ );
40
+
41
+ const TRAIT_PATTERN = new RegExp(
42
+ `${LINE_START}${VIS_PREFIX_SRC}(?:unsafe\\s+)?trait\\s+([A-Za-z_]\\w*)`,
43
+ "gm"
44
+ );
45
+
46
+ const IMPL_PATTERN = /^[^\S\n]*(?:unsafe\s+)?impl(?:<[^>]*>)?\s+(?:([A-Za-z_]\w*(?:<[^>]*>)?)\s+for\s+)?([A-Za-z_]\w*)/gm;
47
+
48
+ const MOD_PATTERN = new RegExp(
49
+ `${LINE_START}${VIS_PREFIX_SRC}mod\\s+([A-Za-z_]\\w*)`,
50
+ "gm"
51
+ );
52
+
53
+ const MACRO_PATTERN = new RegExp(
54
+ `${LINE_START}${VIS_PREFIX_SRC}macro_rules!\\s+([A-Za-z_]\\w*)`,
55
+ "gm"
56
+ );
57
+
58
+ const USE_PATTERN = new RegExp(
59
+ `^\\s*${VIS_PREFIX_SRC}use\\s+(.+?)\\s*;`,
60
+ "gm"
61
+ );
62
+
63
+ function countLinesBefore(text, index) {
64
+ let line = 1;
65
+ for (let i = 0; i < index; i += 1) {
66
+ if (text[i] === "\n") {
67
+ line += 1;
68
+ }
69
+ }
70
+ return line;
71
+ }
72
+
73
+ function normalizeWhitespace(value) {
74
+ return value.replace(/\s+/g, " ").trim();
75
+ }
76
+
77
+ function findMatchingBrace(text, openBraceIndex) {
78
+ if (openBraceIndex < 0 || text[openBraceIndex] !== "{") {
79
+ return -1;
80
+ }
81
+
82
+ let depth = 0;
83
+ let inSingleLineComment = false;
84
+ let inBlockComment = false;
85
+ let inString = false;
86
+ let stringChar = "";
87
+ let inRawString = false;
88
+ let rawHashCount = 0;
89
+
90
+ for (let index = openBraceIndex; index < text.length; index += 1) {
91
+ const current = text[index];
92
+ const next = text[index + 1];
93
+
94
+ if (inSingleLineComment) {
95
+ if (current === "\n") {
96
+ inSingleLineComment = false;
97
+ }
98
+ continue;
99
+ }
100
+
101
+ if (inBlockComment) {
102
+ if (current === "*" && next === "/") {
103
+ inBlockComment = false;
104
+ index += 1;
105
+ }
106
+ continue;
107
+ }
108
+
109
+ if (inRawString) {
110
+ if (current === '"') {
111
+ let hashes = 0;
112
+ while (hashes < rawHashCount && text[index + 1 + hashes] === "#") {
113
+ hashes += 1;
114
+ }
115
+ if (hashes === rawHashCount) {
116
+ inRawString = false;
117
+ index += hashes;
118
+ }
119
+ }
120
+ continue;
121
+ }
122
+
123
+ if (inString) {
124
+ if (current === "\\" && next) {
125
+ index += 1;
126
+ continue;
127
+ }
128
+ if (current === stringChar) {
129
+ inString = false;
130
+ stringChar = "";
131
+ }
132
+ continue;
133
+ }
134
+
135
+ if (current === "/" && next === "/") {
136
+ inSingleLineComment = true;
137
+ index += 1;
138
+ continue;
139
+ }
140
+
141
+ if (current === "/" && next === "*") {
142
+ inBlockComment = true;
143
+ index += 1;
144
+ continue;
145
+ }
146
+
147
+ // Rust raw strings: r#"..."#, r##"..."##, etc.
148
+ if (current === "r" && (next === '"' || next === "#")) {
149
+ let hashes = 0;
150
+ let pos = index + 1;
151
+ while (text[pos] === "#") {
152
+ hashes += 1;
153
+ pos += 1;
154
+ }
155
+ if (text[pos] === '"') {
156
+ inRawString = true;
157
+ rawHashCount = hashes;
158
+ index = pos;
159
+ continue;
160
+ }
161
+ }
162
+
163
+ if (current === '"' || current === "'") {
164
+ // Rust lifetime annotations ('a) should not trigger string mode
165
+ if (current === "'" && next && /[a-zA-Z_]/.test(next)) {
166
+ // Check if this is a lifetime like 'a or a char like 'x'
167
+ const afterIdent = text.indexOf("'", index + 2);
168
+ const nextNewline = text.indexOf("\n", index + 1);
169
+ if (afterIdent === -1 || (nextNewline !== -1 && afterIdent > nextNewline) || afterIdent > index + 4) {
170
+ // Lifetime — skip the tick and identifier
171
+ continue;
172
+ }
173
+ }
174
+ inString = true;
175
+ stringChar = current;
176
+ continue;
177
+ }
178
+
179
+ if (current === "{") {
180
+ depth += 1;
181
+ continue;
182
+ }
183
+
184
+ if (current === "}") {
185
+ depth -= 1;
186
+ if (depth === 0) {
187
+ return index;
188
+ }
189
+ }
190
+ }
191
+
192
+ return -1;
193
+ }
194
+
195
+ function findOpenBraceAfterMatch(code, matchEnd) {
196
+ let inLineComment = false;
197
+ let inBlockComment = false;
198
+ let inString = false;
199
+ let stringChar = "";
200
+
201
+ for (let i = matchEnd; i < code.length; i += 1) {
202
+ const ch = code[i];
203
+ const next = code[i + 1];
204
+
205
+ if (inLineComment) {
206
+ if (ch === "\n") inLineComment = false;
207
+ continue;
208
+ }
209
+ if (inBlockComment) {
210
+ if (ch === "*" && next === "/") { inBlockComment = false; i += 1; }
211
+ continue;
212
+ }
213
+ if (inString) {
214
+ if (ch === "\\" && next) { i += 1; continue; }
215
+ if (ch === stringChar) { inString = false; stringChar = ""; }
216
+ continue;
217
+ }
218
+ if (ch === "/" && next === "/") { inLineComment = true; i += 1; continue; }
219
+ if (ch === "/" && next === "*") { inBlockComment = true; i += 1; continue; }
220
+ if (ch === '"' || ch === "'") { inString = true; stringChar = ch; continue; }
221
+
222
+ if (ch === "{") return i;
223
+ if (ch === ";") return -1; // Declaration without body
224
+ }
225
+ return -1;
226
+ }
227
+
228
+ function buildSignature(source) {
229
+ const snippet = normalizeWhitespace(source);
230
+ const braceIndex = snippet.indexOf("{");
231
+ return (braceIndex === -1 ? snippet : snippet.slice(0, braceIndex)).trim();
232
+ }
233
+
234
+ function extractUseImports(code) {
235
+ const imports = [];
236
+ let match;
237
+ USE_PATTERN.lastIndex = 0;
238
+ while ((match = USE_PATTERN.exec(code)) !== null) {
239
+ imports.push(match[1].trim());
240
+ }
241
+ return [...new Set(imports)];
242
+ }
243
+
244
+ function collectCallNames(body, chunkName) {
245
+ const refs = new Set();
246
+ const ownTailName = chunkName.split("::").pop() || chunkName;
247
+ const pattern = /\b([A-Za-z_]\w*(?:::\w+)*)\s*[!(]\s*/g;
248
+ let match;
249
+ while ((match = pattern.exec(body)) !== null) {
250
+ let name = match[1];
251
+ const tailName = name.split("::").pop() || name;
252
+ if (CALL_KEYWORDS.has(tailName) || tailName === ownTailName) {
253
+ continue;
254
+ }
255
+ // Skip if it matched a macro invocation keyword
256
+ if (CALL_KEYWORDS.has(name)) {
257
+ continue;
258
+ }
259
+ refs.add(tailName);
260
+ }
261
+ return [...refs];
262
+ }
263
+
264
+ function extractBlockChunks(code, pattern, kind, language) {
265
+ const chunks = [];
266
+ pattern.lastIndex = 0;
267
+ let match;
268
+ while ((match = pattern.exec(code)) !== null) {
269
+ const name = match[1];
270
+ const openBraceIndex = findOpenBraceAfterMatch(code, match.index + match[0].length);
271
+ if (openBraceIndex === -1) {
272
+ // Could be a unit struct like `struct Foo;` — extract as single-line chunk
273
+ if (kind === "struct") {
274
+ const lineEnd = code.indexOf("\n", match.index);
275
+ const endIdx = lineEnd === -1 ? code.length : lineEnd;
276
+ const body = code.slice(match.index, endIdx).trimEnd();
277
+ if (body.includes(";")) {
278
+ const startLine = countLinesBefore(code, match.index);
279
+ chunks.push({
280
+ name,
281
+ kind,
282
+ signature: normalizeWhitespace(body),
283
+ body,
284
+ startLine,
285
+ endLine: startLine,
286
+ language,
287
+ calls: [],
288
+ imports: []
289
+ });
290
+ }
291
+ }
292
+ continue;
293
+ }
294
+ const closeBraceIndex = findMatchingBrace(code, openBraceIndex);
295
+ if (closeBraceIndex === -1) continue;
296
+
297
+ const bodyEndIndex = closeBraceIndex + 1;
298
+ const body = code.slice(match.index, bodyEndIndex);
299
+ const startLine = countLinesBefore(code, match.index);
300
+ const endLine = countLinesBefore(code, Math.max(match.index, bodyEndIndex - 1));
301
+
302
+ chunks.push({
303
+ name,
304
+ kind,
305
+ signature: buildSignature(body),
306
+ body,
307
+ startLine,
308
+ endLine,
309
+ language,
310
+ calls: kind === "function" ? collectCallNames(body, name) : [],
311
+ imports: []
312
+ });
313
+ }
314
+ return chunks;
315
+ }
316
+
317
+ function extractImplBlocks(code, language, imports) {
318
+ const chunks = [];
319
+ IMPL_PATTERN.lastIndex = 0;
320
+ let match;
321
+ while ((match = IMPL_PATTERN.exec(code)) !== null) {
322
+ const traitName = match[1] || null;
323
+ const typeName = match[2];
324
+ const openBraceIndex = findOpenBraceAfterMatch(code, match.index + match[0].length);
325
+ if (openBraceIndex === -1) continue;
326
+ const closeBraceIndex = findMatchingBrace(code, openBraceIndex);
327
+ if (closeBraceIndex === -1) continue;
328
+
329
+ const implBody = code.slice(match.index, closeBraceIndex + 1);
330
+ const implStartLine = countLinesBefore(code, match.index);
331
+ const implEndLine = countLinesBefore(code, closeBraceIndex);
332
+ const implName = traitName ? `${traitName} for ${typeName}` : typeName;
333
+
334
+ // Add the impl block itself
335
+ chunks.push({
336
+ name: implName,
337
+ kind: "impl",
338
+ signature: buildSignature(implBody),
339
+ body: implBody,
340
+ startLine: implStartLine,
341
+ endLine: implEndLine,
342
+ language,
343
+ calls: [],
344
+ imports: []
345
+ });
346
+
347
+ // Extract methods within the impl block
348
+ const innerCode = code.slice(openBraceIndex + 1, closeBraceIndex);
349
+ const innerOffset = openBraceIndex + 1;
350
+ FN_PATTERN.lastIndex = 0;
351
+ let fnMatch;
352
+ while ((fnMatch = FN_PATTERN.exec(innerCode)) !== null) {
353
+ const fnName = fnMatch[1];
354
+ const qualifiedName = `${typeName}::${fnName}`;
355
+ const fnOpenBrace = findOpenBraceAfterMatch(innerCode, fnMatch.index + fnMatch[0].length);
356
+ if (fnOpenBrace === -1) continue;
357
+ const fnCloseBrace = findMatchingBrace(innerCode, fnOpenBrace);
358
+ if (fnCloseBrace === -1) continue;
359
+
360
+ const fnBodyEndIndex = fnCloseBrace + 1;
361
+ const fnBody = innerCode.slice(fnMatch.index, fnBodyEndIndex);
362
+ const fnStartLine = countLinesBefore(code, innerOffset + fnMatch.index);
363
+ const fnEndLine = countLinesBefore(code, innerOffset + Math.max(fnMatch.index, fnBodyEndIndex - 1));
364
+
365
+ chunks.push({
366
+ name: qualifiedName,
367
+ kind: "method",
368
+ signature: buildSignature(fnBody),
369
+ body: fnBody,
370
+ startLine: fnStartLine,
371
+ endLine: fnEndLine,
372
+ language,
373
+ calls: collectCallNames(fnBody, qualifiedName),
374
+ imports
375
+ });
376
+ }
377
+ }
378
+ return chunks;
379
+ }
380
+
381
+ function extractMacroChunks(code, language) {
382
+ const chunks = [];
383
+ MACRO_PATTERN.lastIndex = 0;
384
+ let match;
385
+ while ((match = MACRO_PATTERN.exec(code)) !== null) {
386
+ const name = match[1];
387
+ // macro_rules! uses { } or ( ) or [ ] as delimiters
388
+ const afterMatch = code.slice(match.index + match[0].length).trimStart();
389
+ let openChar, closeChar;
390
+ if (afterMatch[0] === "{") {
391
+ openChar = "{";
392
+ } else if (afterMatch[0] === "(") {
393
+ openChar = "(";
394
+ } else if (afterMatch[0] === "[") {
395
+ openChar = "[";
396
+ } else {
397
+ continue;
398
+ }
399
+
400
+ // For braces, use findMatchingBrace; for parens/brackets, do simple depth counting
401
+ let endIndex;
402
+ if (openChar === "{") {
403
+ const openBraceIndex = code.indexOf("{", match.index + match[0].length);
404
+ const closeBraceIndex = findMatchingBrace(code, openBraceIndex);
405
+ if (closeBraceIndex === -1) continue;
406
+ endIndex = closeBraceIndex + 1;
407
+ } else {
408
+ closeChar = openChar === "(" ? ")" : "]";
409
+ const startSearch = match.index + match[0].length + afterMatch.indexOf(openChar);
410
+ let depth = 0;
411
+ endIndex = -1;
412
+ for (let i = startSearch; i < code.length; i += 1) {
413
+ if (code[i] === openChar) depth += 1;
414
+ else if (code[i] === closeChar) {
415
+ depth -= 1;
416
+ if (depth === 0) {
417
+ endIndex = i + 1;
418
+ break;
419
+ }
420
+ }
421
+ }
422
+ if (endIndex === -1) continue;
423
+ }
424
+
425
+ const body = code.slice(match.index, endIndex);
426
+ const startLine = countLinesBefore(code, match.index);
427
+ const endLine = countLinesBefore(code, Math.max(match.index, endIndex - 1));
428
+
429
+ chunks.push({
430
+ name,
431
+ kind: "macro",
432
+ signature: `macro_rules! ${name}`,
433
+ body,
434
+ startLine,
435
+ endLine,
436
+ language,
437
+ calls: [],
438
+ imports: []
439
+ });
440
+ }
441
+ return chunks;
442
+ }
443
+
444
+ function extractTopLevelFunctions(code, language, implChunks, imports) {
445
+ const chunks = [];
446
+ FN_PATTERN.lastIndex = 0;
447
+ let match;
448
+ while ((match = FN_PATTERN.exec(code)) !== null) {
449
+ const name = match[1];
450
+ const openBraceIndex = findOpenBraceAfterMatch(code, match.index + match[0].length);
451
+ if (openBraceIndex === -1) continue;
452
+ const closeBraceIndex = findMatchingBrace(code, openBraceIndex);
453
+ if (closeBraceIndex === -1) continue;
454
+
455
+ const startLine = countLinesBefore(code, match.index);
456
+ const endLine = countLinesBefore(code, closeBraceIndex);
457
+
458
+ // Skip functions that are inside impl blocks (already extracted as methods)
459
+ const insideImpl = implChunks.some(
460
+ (impl) => impl.kind === "impl" && startLine >= impl.startLine && endLine <= impl.endLine
461
+ );
462
+ if (insideImpl) continue;
463
+
464
+ const bodyEndIndex = closeBraceIndex + 1;
465
+ const body = code.slice(match.index, bodyEndIndex);
466
+
467
+ chunks.push({
468
+ name,
469
+ kind: "function",
470
+ signature: buildSignature(body),
471
+ body,
472
+ startLine,
473
+ endLine,
474
+ language,
475
+ calls: collectCallNames(body, name),
476
+ imports
477
+ });
478
+ }
479
+ return chunks;
480
+ }
481
+
482
+ export function parseCode(code, filePath, language = "rust") {
483
+ const imports = extractUseImports(code);
484
+ const implChunks = extractImplBlocks(code, language, imports);
485
+ const structChunks = extractBlockChunks(code, STRUCT_PATTERN, "struct", language);
486
+ const enumChunks = extractBlockChunks(code, ENUM_PATTERN, "enum", language);
487
+ const traitChunks = extractBlockChunks(code, TRAIT_PATTERN, "trait", language);
488
+ const modChunks = extractBlockChunks(code, MOD_PATTERN, "module", language);
489
+ const macroChunks = extractMacroChunks(code, language);
490
+ const fnChunks = extractTopLevelFunctions(code, language, implChunks, imports);
491
+
492
+ const seen = new Set();
493
+ const chunks = [...structChunks, ...enumChunks, ...traitChunks, ...implChunks, ...modChunks, ...macroChunks, ...fnChunks].filter((chunk) => {
494
+ const key = `${chunk.kind}|${chunk.name}|${chunk.startLine}|${chunk.endLine}`;
495
+ if (seen.has(key)) return false;
496
+ seen.add(key);
497
+ return true;
498
+ });
499
+
500
+ return { chunks, errors: [] };
501
+ }
502
+
503
+ if (import.meta.url === `file://${process.argv[1]}`) {
504
+ const fs = await import("node:fs");
505
+ const filePath = process.argv[2];
506
+
507
+ if (!filePath) {
508
+ console.error("Usage: rust.mjs <file.rs>");
509
+ process.exit(1);
510
+ }
511
+
512
+ const code = fs.readFileSync(filePath, "utf8");
513
+ const result = parseCode(code, filePath, "rust");
514
+ console.log(JSON.stringify(result, null, 2));
515
+ }
@@ -84,10 +84,18 @@ detect_event_backend() {
84
84
  return 1
85
85
  }
86
86
 
87
+ is_wsl_mnt_path() {
88
+ grep -qi microsoft /proc/version 2>/dev/null && [[ "$REPO_ROOT" == /mnt/* ]]
89
+ }
90
+
87
91
  resolve_mode() {
88
92
  case "$WATCH_MODE" in
89
93
  auto)
90
- if EVENT_BACKEND="$(detect_event_backend)"; then
94
+ if is_wsl_mnt_path; then
95
+ echo "[watch] WSL detected with /mnt/ path, using poll mode (inotify unreliable on Windows mounts)"
96
+ WATCH_MODE="poll"
97
+ EVENT_BACKEND=""
98
+ elif EVENT_BACKEND="$(detect_event_backend)"; then
91
99
  WATCH_MODE="event"
92
100
  else
93
101
  WATCH_MODE="poll"