@akiojin/unity-mcp-server 2.41.8 → 2.42.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 +93 -10
- package/package.json +5 -6
- package/scripts/ensure-better-sqlite3.mjs +1 -1
- package/src/core/codeIndex.js +68 -41
- package/src/core/indexBuildWorkerPool.js +186 -0
- package/src/core/indexWatcher.js +58 -36
- package/src/core/transports/HybridStdioServerTransport.js +16 -4
- package/src/core/unityConnection.js +78 -51
- package/src/core/workers/indexBuildWorker.js +362 -0
- package/src/handlers/script/CodeIndexBuildToolHandler.js +5 -0
- package/src/handlers/script/CodeIndexStatusToolHandler.js +21 -59
- package/src/handlers/script/ScriptRefsFindToolHandler.js +52 -1
- package/src/handlers/script/ScriptSearchToolHandler.js +1 -1
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +64 -65
- package/src/lsp/LspRpcClient.js +7 -0
- package/src/lsp/LspRpcClientSingleton.js +8 -0
- package/src/core/codeIndexDb.js +0 -112
- package/src/core/sqliteFallback.js +0 -82
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Thread script for code index builds.
|
|
3
|
+
*
|
|
4
|
+
* This script runs in a separate thread and performs the heavy lifting of
|
|
5
|
+
* index builds: file scanning, LSP document symbol requests, and SQLite
|
|
6
|
+
* database operations. By running in a Worker Thread, these synchronous
|
|
7
|
+
* operations don't block the main event loop.
|
|
8
|
+
*
|
|
9
|
+
* Communication with main thread:
|
|
10
|
+
* - Receives: workerData with build options
|
|
11
|
+
* - Sends: progress updates, completion, errors, logs
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
|
|
22
|
+
// Helper to send messages to main thread
|
|
23
|
+
function sendMessage(type, data) {
|
|
24
|
+
if (parentPort) {
|
|
25
|
+
parentPort.postMessage({ type, ...data });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function log(level, message) {
|
|
30
|
+
sendMessage('log', { level, message });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sendProgress(processed, total, rate) {
|
|
34
|
+
sendMessage('progress', { data: { processed, total, rate } });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sendComplete(result) {
|
|
38
|
+
sendMessage('complete', { data: result });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sendError(error) {
|
|
42
|
+
sendMessage('error', { error: error.message || String(error) });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Walk directory tree and collect .cs files
|
|
47
|
+
*/
|
|
48
|
+
function walkCs(root, files, seen) {
|
|
49
|
+
try {
|
|
50
|
+
if (!fs.existsSync(root)) return;
|
|
51
|
+
const st = fs.statSync(root);
|
|
52
|
+
if (st.isFile()) {
|
|
53
|
+
if (root.endsWith('.cs') && !seen.has(root)) {
|
|
54
|
+
files.push(root);
|
|
55
|
+
seen.add(root);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
60
|
+
for (const e of entries) {
|
|
61
|
+
if (e.name === 'obj' || e.name === 'bin' || e.name.startsWith('.')) continue;
|
|
62
|
+
walkCs(path.join(root, e.name), files, seen);
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Ignore errors
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert absolute path to relative path
|
|
71
|
+
*/
|
|
72
|
+
function toRel(full, projectRoot) {
|
|
73
|
+
const normFull = String(full).replace(/\\/g, '/');
|
|
74
|
+
const normRoot = String(projectRoot).replace(/\\/g, '/').replace(/\/$/, '');
|
|
75
|
+
return normFull.startsWith(normRoot) ? normFull.substring(normRoot.length + 1) : normFull;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create file signature for change detection
|
|
80
|
+
*/
|
|
81
|
+
function makeSig(abs) {
|
|
82
|
+
try {
|
|
83
|
+
const st = fs.statSync(abs);
|
|
84
|
+
return `${st.size}-${Math.floor(st.mtimeMs)}`;
|
|
85
|
+
} catch {
|
|
86
|
+
return '0-0';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Convert LSP symbol kind to string
|
|
92
|
+
*/
|
|
93
|
+
function kindFromLsp(k) {
|
|
94
|
+
switch (k) {
|
|
95
|
+
case 5:
|
|
96
|
+
return 'class';
|
|
97
|
+
case 23:
|
|
98
|
+
return 'struct';
|
|
99
|
+
case 11:
|
|
100
|
+
return 'interface';
|
|
101
|
+
case 10:
|
|
102
|
+
return 'enum';
|
|
103
|
+
case 6:
|
|
104
|
+
return 'method';
|
|
105
|
+
case 7:
|
|
106
|
+
return 'property';
|
|
107
|
+
case 8:
|
|
108
|
+
return 'field';
|
|
109
|
+
case 3:
|
|
110
|
+
return 'namespace';
|
|
111
|
+
default:
|
|
112
|
+
return 'unknown';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Convert LSP document symbols to database rows
|
|
118
|
+
*/
|
|
119
|
+
function toRows(uri, symbols, projectRoot) {
|
|
120
|
+
const rel = toRel(uri.replace('file://', ''), projectRoot);
|
|
121
|
+
const rows = [];
|
|
122
|
+
|
|
123
|
+
const visit = (s, container) => {
|
|
124
|
+
const kind = kindFromLsp(s.kind);
|
|
125
|
+
const name = s.name || '';
|
|
126
|
+
const start = s.range?.start || s.selectionRange?.start || {};
|
|
127
|
+
rows.push({
|
|
128
|
+
path: rel,
|
|
129
|
+
name,
|
|
130
|
+
kind,
|
|
131
|
+
container: container || null,
|
|
132
|
+
ns: null,
|
|
133
|
+
line: (start.line ?? 0) + 1,
|
|
134
|
+
column: (start.character ?? 0) + 1
|
|
135
|
+
});
|
|
136
|
+
if (Array.isArray(s.children)) {
|
|
137
|
+
for (const c of s.children) visit(c, name || container);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (Array.isArray(symbols)) {
|
|
142
|
+
for (const s of symbols) visit(s, null);
|
|
143
|
+
}
|
|
144
|
+
return rows;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Main build function
|
|
149
|
+
*/
|
|
150
|
+
async function runBuild() {
|
|
151
|
+
const {
|
|
152
|
+
projectRoot,
|
|
153
|
+
dbPath,
|
|
154
|
+
concurrency: _concurrency = 1, // Reserved for future parallel processing
|
|
155
|
+
throttleMs = 0,
|
|
156
|
+
retry = 2,
|
|
157
|
+
reportPercentage = 10
|
|
158
|
+
} = workerData;
|
|
159
|
+
void _concurrency; // Explicitly mark as intentionally unused
|
|
160
|
+
|
|
161
|
+
log('info', `[worker] Starting build: projectRoot=${projectRoot}, dbPath=${dbPath}`);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// Dynamic import better-sqlite3 in worker thread
|
|
165
|
+
let Database;
|
|
166
|
+
try {
|
|
167
|
+
const mod = await import('better-sqlite3');
|
|
168
|
+
Database = mod.default || mod;
|
|
169
|
+
} catch (e) {
|
|
170
|
+
throw new Error(`better-sqlite3 unavailable in worker: ${e.message}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Open database
|
|
174
|
+
const db = new Database(dbPath);
|
|
175
|
+
db.exec('PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;');
|
|
176
|
+
|
|
177
|
+
// Initialize schema if needed
|
|
178
|
+
db.exec(`
|
|
179
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
180
|
+
key TEXT PRIMARY KEY,
|
|
181
|
+
value TEXT
|
|
182
|
+
);
|
|
183
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
184
|
+
path TEXT PRIMARY KEY,
|
|
185
|
+
sig TEXT,
|
|
186
|
+
updatedAt TEXT
|
|
187
|
+
);
|
|
188
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
189
|
+
path TEXT NOT NULL,
|
|
190
|
+
name TEXT NOT NULL,
|
|
191
|
+
kind TEXT NOT NULL,
|
|
192
|
+
container TEXT,
|
|
193
|
+
namespace TEXT,
|
|
194
|
+
line INTEGER,
|
|
195
|
+
column INTEGER
|
|
196
|
+
);
|
|
197
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path);
|
|
200
|
+
`);
|
|
201
|
+
|
|
202
|
+
// Scan for C# files
|
|
203
|
+
const roots = [
|
|
204
|
+
path.resolve(projectRoot, 'Assets'),
|
|
205
|
+
path.resolve(projectRoot, 'Packages'),
|
|
206
|
+
path.resolve(projectRoot, 'Library/PackageCache')
|
|
207
|
+
];
|
|
208
|
+
const files = [];
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
for (const r of roots) walkCs(r, files, seen);
|
|
211
|
+
|
|
212
|
+
log('info', `[worker] Found ${files.length} C# files to process`);
|
|
213
|
+
|
|
214
|
+
// Get current indexed files
|
|
215
|
+
const currentRows = db.prepare('SELECT path, sig FROM files').all();
|
|
216
|
+
const current = new Map(currentRows.map(r => [r.path, r.sig]));
|
|
217
|
+
|
|
218
|
+
// Determine changes
|
|
219
|
+
const wanted = new Map(files.map(abs => [toRel(abs, projectRoot), makeSig(abs)]));
|
|
220
|
+
const changed = [];
|
|
221
|
+
const removed = [];
|
|
222
|
+
|
|
223
|
+
for (const [rel, sig] of wanted) {
|
|
224
|
+
if (current.get(rel) !== sig) changed.push(rel);
|
|
225
|
+
}
|
|
226
|
+
for (const [rel] of current) {
|
|
227
|
+
if (!wanted.has(rel)) removed.push(rel);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
log('info', `[worker] Changes: ${changed.length} to update, ${removed.length} to remove`);
|
|
231
|
+
|
|
232
|
+
// Remove vanished files
|
|
233
|
+
const deleteSymbols = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
234
|
+
const deleteFile = db.prepare('DELETE FROM files WHERE path = ?');
|
|
235
|
+
for (const rel of removed) {
|
|
236
|
+
deleteSymbols.run(rel);
|
|
237
|
+
deleteFile.run(rel);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Prepare for updates
|
|
241
|
+
const absList = changed.map(rel => path.resolve(projectRoot, rel));
|
|
242
|
+
const startAt = Date.now();
|
|
243
|
+
let processed = 0;
|
|
244
|
+
let updated = 0;
|
|
245
|
+
let lastReportedPercentage = 0;
|
|
246
|
+
|
|
247
|
+
// Initialize LSP connection
|
|
248
|
+
// Note: LspRpcClientSingleton is in src/lsp/, not src/core/lsp/
|
|
249
|
+
let lsp = null;
|
|
250
|
+
try {
|
|
251
|
+
const lspModulePath = path.join(__dirname, '..', '..', 'lsp', 'LspRpcClientSingleton.js');
|
|
252
|
+
const { LspRpcClientSingleton } = await import(`file://${lspModulePath}`);
|
|
253
|
+
lsp = await LspRpcClientSingleton.getInstance(projectRoot);
|
|
254
|
+
log('info', `[worker] LSP initialized`);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
throw new Error(`LSP initialization failed: ${e.message}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// LSP request with retry
|
|
260
|
+
const requestWithRetry = async (uri, maxRetries = retry) => {
|
|
261
|
+
let lastErr = null;
|
|
262
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
263
|
+
try {
|
|
264
|
+
const res = await lsp.request('textDocument/documentSymbol', { textDocument: { uri } });
|
|
265
|
+
return res?.result ?? res;
|
|
266
|
+
} catch (err) {
|
|
267
|
+
lastErr = err;
|
|
268
|
+
await new Promise(r => setTimeout(r, 200 * (attempt + 1)));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
throw lastErr || new Error('documentSymbol failed');
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Prepared statements for updates
|
|
275
|
+
const insertSymbol = db.prepare(
|
|
276
|
+
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
277
|
+
);
|
|
278
|
+
const deleteSymbolsForPath = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
279
|
+
const upsertFile = db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)');
|
|
280
|
+
const updateMeta = db.prepare("REPLACE INTO meta(key,value) VALUES ('lastIndexedAt',?)");
|
|
281
|
+
|
|
282
|
+
// Process files sequentially (concurrency=1 for non-blocking)
|
|
283
|
+
for (let i = 0; i < absList.length; i++) {
|
|
284
|
+
const abs = absList[i];
|
|
285
|
+
const rel = toRel(abs, projectRoot);
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const uri = 'file://' + abs.replace(/\\/g, '/');
|
|
289
|
+
const docSymbols = await requestWithRetry(uri);
|
|
290
|
+
const rows = toRows(uri, docSymbols, projectRoot);
|
|
291
|
+
|
|
292
|
+
// Update database in transaction
|
|
293
|
+
db.transaction(() => {
|
|
294
|
+
deleteSymbolsForPath.run(rel);
|
|
295
|
+
for (const r of rows) {
|
|
296
|
+
insertSymbol.run(r.path, r.name, r.kind, r.container, r.ns, r.line, r.column);
|
|
297
|
+
}
|
|
298
|
+
upsertFile.run(rel, wanted.get(rel), new Date().toISOString());
|
|
299
|
+
updateMeta.run(new Date().toISOString());
|
|
300
|
+
})();
|
|
301
|
+
|
|
302
|
+
updated++;
|
|
303
|
+
} catch (err) {
|
|
304
|
+
// Log occasionally to avoid spam
|
|
305
|
+
if (processed % 50 === 0) {
|
|
306
|
+
log('warn', `[worker] Skipped file: ${rel} - ${err.message}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
processed++;
|
|
311
|
+
|
|
312
|
+
// Send progress update
|
|
313
|
+
const elapsed = Math.max(1, Date.now() - startAt);
|
|
314
|
+
const rate = parseFloat(((processed * 1000) / elapsed).toFixed(1));
|
|
315
|
+
const currentPercentage = Math.floor((processed / absList.length) * 100);
|
|
316
|
+
|
|
317
|
+
if (
|
|
318
|
+
currentPercentage >= lastReportedPercentage + reportPercentage ||
|
|
319
|
+
processed === absList.length
|
|
320
|
+
) {
|
|
321
|
+
sendProgress(processed, absList.length, rate);
|
|
322
|
+
log(
|
|
323
|
+
'info',
|
|
324
|
+
`[worker] progress ${currentPercentage}% (${processed}/${absList.length}) rate:${rate} f/s`
|
|
325
|
+
);
|
|
326
|
+
lastReportedPercentage = currentPercentage;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Throttle if requested (for testing)
|
|
330
|
+
if (throttleMs > 0) {
|
|
331
|
+
await new Promise(r => setTimeout(r, throttleMs));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Get final stats
|
|
336
|
+
const total = db.prepare('SELECT COUNT(*) AS c FROM symbols').get().c || 0;
|
|
337
|
+
const lastIndexedAt =
|
|
338
|
+
db.prepare("SELECT value AS v FROM meta WHERE key = 'lastIndexedAt'").get()?.v || null;
|
|
339
|
+
|
|
340
|
+
db.close();
|
|
341
|
+
|
|
342
|
+
const result = {
|
|
343
|
+
updatedFiles: updated,
|
|
344
|
+
removedFiles: removed.length,
|
|
345
|
+
totalIndexedSymbols: total,
|
|
346
|
+
lastIndexedAt
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
log(
|
|
350
|
+
'info',
|
|
351
|
+
`[worker] Build completed: updated=${result.updatedFiles}, removed=${result.removedFiles}, total=${result.totalIndexedSymbols}`
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
sendComplete(result);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
log('error', `[worker] Build failed: ${error.message}`);
|
|
357
|
+
sendError(error);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Run the build
|
|
362
|
+
runBuild();
|
|
@@ -234,6 +234,11 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
|
|
|
234
234
|
lastReportedPercentage = currentPercentage;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// Yield to event loop after each file to allow MCP requests to be processed
|
|
238
|
+
// This prevents the index build from blocking other MCP tool calls (US-8.1)
|
|
239
|
+
// Use setTimeout(1) instead of setImmediate to ensure MCP I/O callbacks get processed
|
|
240
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
241
|
+
|
|
237
242
|
if (throttleMs > 0) {
|
|
238
243
|
await new Promise(resolve => setTimeout(resolve, throttleMs));
|
|
239
244
|
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
4
|
-
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
5
2
|
import { JobManager } from '../../core/jobManager.js';
|
|
6
3
|
import { CodeIndex } from '../../core/codeIndex.js';
|
|
7
4
|
|
|
@@ -17,7 +14,6 @@ export class CodeIndexStatusToolHandler extends BaseToolHandler {
|
|
|
17
14
|
}
|
|
18
15
|
);
|
|
19
16
|
this.unityConnection = unityConnection;
|
|
20
|
-
this.projectInfo = new ProjectInfoProvider(unityConnection);
|
|
21
17
|
this.jobManager = JobManager.getInstance();
|
|
22
18
|
this.codeIndex = new CodeIndex(unityConnection);
|
|
23
19
|
}
|
|
@@ -34,13 +30,25 @@ export class CodeIndexStatusToolHandler extends BaseToolHandler {
|
|
|
34
30
|
const ready = await this.codeIndex.isReady();
|
|
35
31
|
if (this.codeIndex.disabled) {
|
|
36
32
|
return {
|
|
37
|
-
success:
|
|
38
|
-
|
|
33
|
+
success: true,
|
|
34
|
+
status: 'degraded',
|
|
35
|
+
disabled: true,
|
|
36
|
+
ready: false,
|
|
37
|
+
totalFiles: 0,
|
|
38
|
+
indexedFiles: 0,
|
|
39
|
+
coverage: 0,
|
|
39
40
|
message:
|
|
40
41
|
this.codeIndex.disableReason ||
|
|
41
42
|
'Code index is disabled because the SQLite driver could not be loaded. The server will continue without the symbol index.',
|
|
42
43
|
remediation:
|
|
43
|
-
'Install native build tools (python3, make, g++)
|
|
44
|
+
'Install native build tools (python3, make, g++) and run "npm rebuild better-sqlite3 --build-from-source", or set UNITY_MCP_SKIP_NATIVE_BUILD=0 to allow native rebuild. After installing, restart unity-mcp-server.',
|
|
45
|
+
index: {
|
|
46
|
+
ready: false,
|
|
47
|
+
disabled: true,
|
|
48
|
+
reason:
|
|
49
|
+
this.codeIndex.disableReason ||
|
|
50
|
+
'better-sqlite3 native binding unavailable; code index is disabled'
|
|
51
|
+
}
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
54
|
const buildInProgress = latestBuildJob?.status === 'running';
|
|
@@ -52,58 +60,13 @@ export class CodeIndexStatusToolHandler extends BaseToolHandler {
|
|
|
52
60
|
};
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
projectInfo = await this.projectInfo.get();
|
|
58
|
-
} catch (error) {
|
|
59
|
-
return {
|
|
60
|
-
success: false,
|
|
61
|
-
error: 'project_info_unavailable',
|
|
62
|
-
message: error.message || 'Unable to resolve Unity project root.'
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const projectRoot = projectInfo.projectRoot.replace(/\\/g, '/');
|
|
67
|
-
const roots = [
|
|
68
|
-
path.resolve(projectRoot, 'Assets'),
|
|
69
|
-
path.resolve(projectRoot, 'Packages'),
|
|
70
|
-
path.resolve(projectRoot, 'Library/PackageCache')
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
let totalFiles = 0;
|
|
74
|
-
const breakdown = { assets: 0, packages: 0, packageCache: 0, other: 0 };
|
|
75
|
-
|
|
76
|
-
const visit = targetPath => {
|
|
77
|
-
try {
|
|
78
|
-
if (!fs.existsSync(targetPath)) return;
|
|
79
|
-
const stats = fs.statSync(targetPath);
|
|
80
|
-
if (stats.isFile()) {
|
|
81
|
-
if (!targetPath.endsWith('.cs')) return;
|
|
82
|
-
totalFiles += 1;
|
|
83
|
-
const normalized = targetPath.replace(/\\/g, '/');
|
|
84
|
-
const relative = normalized.replace(projectRoot, '').replace(/^\//, '');
|
|
85
|
-
if (relative.startsWith('Assets/')) breakdown.assets += 1;
|
|
86
|
-
else if (relative.startsWith('Packages/')) breakdown.packages += 1;
|
|
87
|
-
else if (relative.includes('Library/PackageCache/')) breakdown.packageCache += 1;
|
|
88
|
-
else breakdown.other += 1;
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (stats.isDirectory()) {
|
|
93
|
-
for (const child of fs.readdirSync(targetPath)) {
|
|
94
|
-
if (child === 'obj' || child === 'bin' || child.startsWith('.')) continue;
|
|
95
|
-
visit(path.join(targetPath, child));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} catch (error) {
|
|
99
|
-
// Ignore permission errors while traversing; status reporting should not fail because of a single file.
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
for (const root of roots) visit(root);
|
|
104
|
-
|
|
63
|
+
// Use DB stats directly for fast status check - avoid expensive filesystem traversal
|
|
105
64
|
const stats = await this.codeIndex.getStats();
|
|
106
|
-
|
|
65
|
+
|
|
66
|
+
// Estimate total files from DB metadata if available, otherwise use indexed count
|
|
67
|
+
// The actual file count scan is too slow (6+ minutes for 7000+ files)
|
|
68
|
+
const totalFiles = stats.totalFilesEstimate || stats.total;
|
|
69
|
+
const coverage = totalFiles > 0 ? Math.min(1, stats.total / totalFiles) : 1;
|
|
107
70
|
|
|
108
71
|
const indexInfo = {
|
|
109
72
|
ready,
|
|
@@ -134,7 +97,6 @@ export class CodeIndexStatusToolHandler extends BaseToolHandler {
|
|
|
134
97
|
totalFiles,
|
|
135
98
|
indexedFiles: stats.total,
|
|
136
99
|
coverage,
|
|
137
|
-
breakdown,
|
|
138
100
|
index: indexInfo
|
|
139
101
|
};
|
|
140
102
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
2
|
import { CodeIndex } from '../../core/codeIndex.js';
|
|
3
|
+
import { JobManager } from '../../core/jobManager.js';
|
|
3
4
|
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
4
5
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
5
6
|
|
|
@@ -60,6 +61,7 @@ export class ScriptRefsFindToolHandler extends BaseToolHandler {
|
|
|
60
61
|
this.unityConnection = unityConnection;
|
|
61
62
|
this.index = new CodeIndex(unityConnection);
|
|
62
63
|
this.projectInfo = new ProjectInfoProvider(unityConnection);
|
|
64
|
+
this.jobManager = JobManager.getInstance();
|
|
63
65
|
this.lsp = null;
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -82,7 +84,56 @@ export class ScriptRefsFindToolHandler extends BaseToolHandler {
|
|
|
82
84
|
maxMatchesPerFile = 5
|
|
83
85
|
} = params;
|
|
84
86
|
|
|
85
|
-
//
|
|
87
|
+
// Check if code index is ready - required for references search
|
|
88
|
+
const ready = await this.index.isReady();
|
|
89
|
+
if (this.index.disabled) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: 'code_index_unavailable',
|
|
93
|
+
message:
|
|
94
|
+
this.index.disableReason ||
|
|
95
|
+
'Code index is disabled because the SQLite driver could not be loaded.',
|
|
96
|
+
remediation:
|
|
97
|
+
'Install native build tools and run "npm rebuild better-sqlite3 --build-from-source", then restart the server.'
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!ready) {
|
|
102
|
+
// Check if a build job is currently running
|
|
103
|
+
const allJobs = this.jobManager.getAllJobs();
|
|
104
|
+
const buildJob = allJobs.find(
|
|
105
|
+
job =>
|
|
106
|
+
(job.id.startsWith('build-') || job.id.startsWith('watcher-')) && job.status === 'running'
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (buildJob) {
|
|
110
|
+
const progress = buildJob.progress || {};
|
|
111
|
+
const pct =
|
|
112
|
+
progress.total > 0 ? Math.round((progress.processed / progress.total) * 100) : 0;
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: 'index_building',
|
|
116
|
+
message: `Code index is currently being built. Please wait and retry. Progress: ${pct}% (${progress.processed || 0}/${progress.total || 0})`,
|
|
117
|
+
jobId: buildJob.id,
|
|
118
|
+
progress: {
|
|
119
|
+
processed: progress.processed || 0,
|
|
120
|
+
total: progress.total || 0,
|
|
121
|
+
percentage: pct
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// No build job running - index not available
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
error: 'index_not_ready',
|
|
130
|
+
message:
|
|
131
|
+
'Code index is not built. Run code_index_build first, or wait for auto-build to complete on server startup.',
|
|
132
|
+
hint: 'Use code_index_status to check index state, or code_index_build to start a build manually.'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// DB index is ready - use LSP for references (LSP requires index for performance)
|
|
86
137
|
const info = await this.projectInfo.get();
|
|
87
138
|
if (!this.lsp) this.lsp = await LspRpcClientSingleton.getInstance(info.projectRoot);
|
|
88
139
|
const resp = await this.lsp.request('mcp/referencesByName', { name: String(name) });
|