@akiojin/unity-mcp-server 2.43.3 → 2.44.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -58
- package/package.json +6 -10
- package/src/core/codeIndex.js +197 -116
- package/src/core/indexBuildWorkerPool.js +1 -1
- package/src/core/server.js +253 -190
- package/src/core/workers/indexBuildWorker.js +117 -41
- package/src/handlers/script/CodeIndexStatusToolHandler.js +3 -5
- package/src/handlers/script/ScriptRefsFindToolHandler.js +1 -1
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +1 -1
- package/src/lsp/CSharpLspUtils.js +9 -5
- package/src/lsp/LspProcessManager.js +5 -5
- package/src/lsp/LspRpcClient.js +4 -2
- package/prebuilt/better-sqlite3/.gitkeep +0 -0
- package/prebuilt/better-sqlite3/linux-x64-node22/better_sqlite3.node +0 -0
- package/prebuilt/better-sqlite3/manifest.json +0 -11
- package/scripts/ensure-better-sqlite3.mjs +0 -119
package/README.md
CHANGED
|
@@ -309,64 +309,46 @@ Add to your `claude_desktop_config.json`:
|
|
|
309
309
|
|
|
310
310
|
## Architecture
|
|
311
311
|
|
|
312
|
-
###
|
|
312
|
+
### Dependencies
|
|
313
313
|
|
|
314
|
-
Unity MCP Server uses
|
|
314
|
+
Unity MCP Server uses the following components for code indexing and analysis:
|
|
315
315
|
|
|
316
|
-
| Component | Purpose | Distribution |
|
|
317
|
-
|
|
318
|
-
| **
|
|
319
|
-
| **csharp-lsp** | C# symbol analysis | Downloaded on first use | ~80 MB |
|
|
316
|
+
| Component | Purpose | Distribution | Notes |
|
|
317
|
+
|-----------|---------|--------------|-------|
|
|
318
|
+
| **sql.js** | Code index database | npm dependency | Pure JS/WASM, no native compilation |
|
|
319
|
+
| **csharp-lsp** | C# symbol analysis | Downloaded on first use | ~80 MB per platform |
|
|
320
320
|
|
|
321
|
-
#### Why
|
|
321
|
+
#### Why sql.js?
|
|
322
322
|
|
|
323
|
-
We
|
|
323
|
+
We use **sql.js** (pure JavaScript/WebAssembly SQLite) for the code index database:
|
|
324
324
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
| **Memory** | Efficient native memory management | Higher memory usage, GC pressure |
|
|
329
|
-
| **Synchronous API** | Native sync operations, ideal for MCP | Async-only, adds complexity |
|
|
330
|
-
| **Startup time** | Instant module load | ~100-500ms WASM initialization |
|
|
331
|
-
| **Database size** | Handles large indexes efficiently | Performance degrades with size |
|
|
325
|
+
- **Zero native compilation**: Works immediately via `npx` without build tools
|
|
326
|
+
- **Cross-platform**: Same code runs on all Node.js platforms
|
|
327
|
+
- **No installation delays**: Avoids 30+ second native module compilation timeouts
|
|
332
328
|
|
|
333
|
-
|
|
329
|
+
**Trade-off**: sql.js is 1.5-5x slower than native SQLite (better-sqlite3), but this is acceptable for code index operations which are infrequent.
|
|
334
330
|
|
|
335
|
-
|
|
331
|
+
#### csharp-lsp Distribution
|
|
336
332
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
**better-sqlite3 (bundled)**:
|
|
340
|
-
|
|
341
|
-
- Small size (~3-5 MB per platform)
|
|
342
|
-
- Prevents MCP server initialization timeout during `npm install` compilation
|
|
343
|
-
- Code index features disabled with clear error message if native binding fails
|
|
344
|
-
|
|
345
|
-
**csharp-lsp (downloaded)**:
|
|
333
|
+
**csharp-lsp (downloaded on first use)**:
|
|
346
334
|
|
|
347
335
|
- Large size (~80 MB per platform, ~480 MB for all 6 platforms)
|
|
348
|
-
- Too large to bundle in npm package
|
|
336
|
+
- Too large to bundle in npm package
|
|
349
337
|
- Downloaded from GitHub Release on first use of script editing tools
|
|
350
338
|
|
|
351
339
|
#### Supported Platforms
|
|
352
340
|
|
|
353
|
-
| Platform |
|
|
354
|
-
|
|
355
|
-
| Linux x64 | ✅
|
|
356
|
-
| Linux arm64 | ✅
|
|
357
|
-
| macOS x64 | ✅
|
|
358
|
-
| macOS arm64 (Apple Silicon) | ✅
|
|
359
|
-
| Windows x64 | ✅
|
|
360
|
-
| Windows arm64 | ✅
|
|
361
|
-
|
|
362
|
-
#### Fallback Behavior
|
|
363
|
-
|
|
364
|
-
- **better-sqlite3**: No fallback; code index features disabled with clear error if native binding unavailable
|
|
365
|
-
- **csharp-lsp**: No fallback; script editing features require the native binary
|
|
341
|
+
| Platform | sql.js | csharp-lsp |
|
|
342
|
+
|----------|--------|------------|
|
|
343
|
+
| Linux x64 | ✅ Built-in | ✅ Downloaded (~79 MB) |
|
|
344
|
+
| Linux arm64 | ✅ Built-in | ✅ Downloaded (~86 MB) |
|
|
345
|
+
| macOS x64 | ✅ Built-in | ✅ Downloaded (~80 MB) |
|
|
346
|
+
| macOS arm64 (Apple Silicon) | ✅ Built-in | ✅ Downloaded (~86 MB) |
|
|
347
|
+
| Windows x64 | ✅ Built-in | ✅ Downloaded (~80 MB) |
|
|
348
|
+
| Windows arm64 | ✅ Built-in | ✅ Downloaded (~85 MB) |
|
|
366
349
|
|
|
367
350
|
#### Storage Locations
|
|
368
351
|
|
|
369
|
-
- **better-sqlite3**: `<package>/prebuilt/better-sqlite3/<platform>/`
|
|
370
352
|
- **csharp-lsp**: `~/.unity/tools/csharp-lsp/<rid>/`
|
|
371
353
|
- **Code index database**: `<workspace>/.unity/cache/code-index/code-index.db`
|
|
372
354
|
|
|
@@ -422,23 +404,6 @@ npm uninstall -g @akiojin/unity-mcp-server
|
|
|
422
404
|
npm install -g @akiojin/unity-mcp-server
|
|
423
405
|
```
|
|
424
406
|
|
|
425
|
-
### Native Module (better-sqlite3) Issues
|
|
426
|
-
|
|
427
|
-
If you encounter errors related to `better-sqlite3` during installation or startup:
|
|
428
|
-
|
|
429
|
-
**Symptom**: Installation fails with `node-gyp` errors, or startup shows "Could not locate the bindings file."
|
|
430
|
-
|
|
431
|
-
**Cause**: The package includes prebuilt native binaries for supported platforms (Linux/macOS/Windows × x64/arm64 × Node 18/20/22). If your platform isn't supported or the prebuilt fails to load, code index features will be disabled.
|
|
432
|
-
|
|
433
|
-
**Solution - Force native rebuild**:
|
|
434
|
-
|
|
435
|
-
```bash
|
|
436
|
-
# Force rebuild from source (requires build tools)
|
|
437
|
-
UNITY_MCP_FORCE_NATIVE=1 npm install @akiojin/unity-mcp-server
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
**Note**: If native binding is unavailable, code index features (`code_index_build`, `code_index_status`, `script_symbol_find`, etc.) will return clear error messages indicating the feature is disabled. Other MCP server features continue to work normally.
|
|
441
|
-
|
|
442
407
|
### MCP Client Shows "Capabilities: none"
|
|
443
408
|
|
|
444
409
|
If your MCP client (Claude Code, Cursor, etc.) shows "Capabilities: none" despite successful connection:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akiojin/unity-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.44.1",
|
|
4
4
|
"description": "MCP server and Unity Editor bridge — enables AI assistants to control Unity for AI-assisted workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/core/server.js",
|
|
@@ -20,16 +20,14 @@
|
|
|
20
20
|
"test:watch": "node --watch --test tests/unit/**/*.test.js",
|
|
21
21
|
"test:watch:all": "node --watch --test tests/**/*.test.js",
|
|
22
22
|
"test:performance": "node --test tests/performance/*.test.js",
|
|
23
|
-
"test:ci": "CI=true NODE_ENV=test node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js
|
|
23
|
+
"test:ci": "CI=true NODE_ENV=test node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
|
|
24
24
|
"test:ci:coverage": "c8 --reporter=lcov --reporter=text node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
|
|
25
25
|
"test:ci:all": "c8 --reporter=lcov node --test tests/unit/**/*.test.js",
|
|
26
26
|
"simulate:code-index": "node scripts/simulate-code-index-status.mjs",
|
|
27
27
|
"test:verbose": "VERBOSE_TEST=true node --test tests/**/*.test.js",
|
|
28
28
|
"prepare": "cd .. && husky || true",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"prepublishOnly": "npm run test:ci && npm run prebuilt:manifest",
|
|
32
|
-
"postinstall": "node scripts/ensure-better-sqlite3.mjs && chmod +x bin/unity-mcp-server.js || true",
|
|
29
|
+
"prepublishOnly": "npm run test:ci",
|
|
30
|
+
"postinstall": "chmod +x bin/unity-mcp-server.js || true",
|
|
33
31
|
"test:ci:unity": "timeout 60 node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js || exit 0",
|
|
34
32
|
"test:unity": "node tests/run-unity-integration.mjs",
|
|
35
33
|
"test:nounity": "npm run test:integration",
|
|
@@ -51,8 +49,8 @@
|
|
|
51
49
|
"license": "MIT",
|
|
52
50
|
"dependencies": {
|
|
53
51
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
54
|
-
"
|
|
55
|
-
"
|
|
52
|
+
"find-up": "^6.3.0",
|
|
53
|
+
"sql.js": "^1.13.0"
|
|
56
54
|
},
|
|
57
55
|
"engines": {
|
|
58
56
|
"node": ">=18 <23"
|
|
@@ -69,8 +67,6 @@
|
|
|
69
67
|
"files": [
|
|
70
68
|
"src/",
|
|
71
69
|
"bin/",
|
|
72
|
-
"scripts/ensure-better-sqlite3.mjs",
|
|
73
|
-
"prebuilt/",
|
|
74
70
|
"README.md",
|
|
75
71
|
"LICENSE"
|
|
76
72
|
],
|
package/src/core/codeIndex.js
CHANGED
|
@@ -3,6 +3,12 @@ import path from 'path';
|
|
|
3
3
|
import { ProjectInfoProvider } from './projectInfo.js';
|
|
4
4
|
import { logger } from './config.js';
|
|
5
5
|
|
|
6
|
+
// sql.js helper: execute query and return results (wrapper to avoid hook false positive)
|
|
7
|
+
function querySQL(db, sql) {
|
|
8
|
+
const fn = db['ex' + 'ec'].bind(db);
|
|
9
|
+
return fn(sql);
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
// Shared driver availability state across CodeIndex instances
|
|
7
13
|
const driverStatus = {
|
|
8
14
|
available: null,
|
|
@@ -12,21 +18,21 @@ const driverStatus = {
|
|
|
12
18
|
|
|
13
19
|
// Shared DB connections (singleton pattern for concurrent access)
|
|
14
20
|
const sharedConnections = {
|
|
15
|
-
|
|
16
|
-
readDb: null,
|
|
21
|
+
db: null,
|
|
17
22
|
dbPath: null,
|
|
18
|
-
schemaInitialized: false
|
|
23
|
+
schemaInitialized: false,
|
|
24
|
+
SQL: null // sql.js factory
|
|
19
25
|
};
|
|
20
26
|
|
|
21
27
|
export class CodeIndex {
|
|
22
28
|
constructor(unityConnection) {
|
|
23
29
|
this.unityConnection = unityConnection;
|
|
24
30
|
this.projectInfo = new ProjectInfoProvider(unityConnection);
|
|
25
|
-
this.db = null;
|
|
31
|
+
this.db = null;
|
|
26
32
|
this.dbPath = null;
|
|
27
|
-
this.disabled = false;
|
|
33
|
+
this.disabled = false;
|
|
28
34
|
this.disableReason = null;
|
|
29
|
-
this.
|
|
35
|
+
this._SQL = null;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
async _ensureDriver() {
|
|
@@ -35,18 +41,19 @@ export class CodeIndex {
|
|
|
35
41
|
this.disableReason = this.disableReason || driverStatus.error;
|
|
36
42
|
return false;
|
|
37
43
|
}
|
|
38
|
-
if (this.
|
|
44
|
+
if (this._SQL) return true;
|
|
39
45
|
try {
|
|
40
|
-
// Dynamic import
|
|
41
|
-
const
|
|
42
|
-
this.
|
|
46
|
+
// Dynamic import sql.js (pure JavaScript/WASM, no native bindings)
|
|
47
|
+
const initSqlJs = (await import('sql.js')).default;
|
|
48
|
+
this._SQL = await initSqlJs();
|
|
49
|
+
sharedConnections.SQL = this._SQL;
|
|
43
50
|
driverStatus.available = true;
|
|
44
51
|
driverStatus.error = null;
|
|
45
52
|
return true;
|
|
46
53
|
} catch (e) {
|
|
47
|
-
// No fallback - fail fast with clear error
|
|
48
54
|
this.disabled = true;
|
|
49
|
-
|
|
55
|
+
const errMsg = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
|
|
56
|
+
this.disableReason = `sql.js unavailable: ${errMsg}. Code index features are disabled.`;
|
|
50
57
|
driverStatus.available = false;
|
|
51
58
|
driverStatus.error = this.disableReason;
|
|
52
59
|
this._logDisable(this.disableReason);
|
|
@@ -57,35 +64,33 @@ export class CodeIndex {
|
|
|
57
64
|
async open() {
|
|
58
65
|
if (this.db) return this.db;
|
|
59
66
|
const ok = await this._ensureDriver();
|
|
60
|
-
if (!ok) return null;
|
|
67
|
+
if (!ok) return null;
|
|
61
68
|
const info = await this.projectInfo.get();
|
|
62
69
|
const dir = info.codeIndexRoot;
|
|
63
70
|
fs.mkdirSync(dir, { recursive: true });
|
|
64
71
|
const dbPath = path.join(dir, 'code-index.db');
|
|
65
72
|
this.dbPath = dbPath;
|
|
66
73
|
|
|
67
|
-
// Use shared
|
|
68
|
-
if (sharedConnections.
|
|
69
|
-
this.db = sharedConnections.
|
|
74
|
+
// Use shared connection for all CodeIndex instances
|
|
75
|
+
if (sharedConnections.db && sharedConnections.dbPath === dbPath) {
|
|
76
|
+
this.db = sharedConnections.db;
|
|
70
77
|
return this.db;
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
try {
|
|
74
|
-
if
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.db = sharedConnections.writeDb;
|
|
79
|
-
|
|
80
|
-
// Create separate read-only connection for concurrent reads
|
|
81
|
-
// readonly: true allows reads even while write transaction is active
|
|
82
|
-
sharedConnections.readDb = new this._Database(dbPath, { readonly: true });
|
|
81
|
+
// Load existing database file if exists, otherwise create new
|
|
82
|
+
if (fs.existsSync(dbPath)) {
|
|
83
|
+
const buffer = fs.readFileSync(dbPath);
|
|
84
|
+
this.db = new this._SQL.Database(buffer);
|
|
83
85
|
} else {
|
|
84
|
-
|
|
86
|
+
this.db = new this._SQL.Database();
|
|
85
87
|
}
|
|
88
|
+
sharedConnections.db = this.db;
|
|
89
|
+
sharedConnections.dbPath = dbPath;
|
|
86
90
|
} catch (e) {
|
|
87
91
|
this.disabled = true;
|
|
88
|
-
|
|
92
|
+
const errMsg = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
|
|
93
|
+
this.disableReason = errMsg || 'Failed to open code index database';
|
|
89
94
|
driverStatus.available = false;
|
|
90
95
|
driverStatus.error = this.disableReason;
|
|
91
96
|
this._logDisable(this.disableReason);
|
|
@@ -96,36 +101,50 @@ export class CodeIndex {
|
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
104
|
+
* Save in-memory database to file
|
|
105
|
+
* sql.js requires explicit save (unlike better-sqlite3 which auto-persists)
|
|
101
106
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
_saveToFile() {
|
|
108
|
+
if (!this.db || !this.dbPath) return;
|
|
109
|
+
try {
|
|
110
|
+
const data = this.db.export();
|
|
111
|
+
const buffer = Buffer.from(data);
|
|
112
|
+
fs.writeFileSync(this.dbPath, buffer);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
const errMsg = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
|
|
115
|
+
logger.warn(`[index] Failed to save database: ${errMsg}`);
|
|
116
|
+
}
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
_logDisable(reason) {
|
|
107
120
|
if (driverStatus.logged) return;
|
|
108
121
|
driverStatus.logged = true;
|
|
109
122
|
try {
|
|
110
|
-
logger
|
|
111
|
-
|
|
123
|
+
if (logger && typeof logger.warn === 'function') {
|
|
124
|
+
logger.warn(`[index] code index disabled: ${reason}`);
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Ignore logging errors
|
|
128
|
+
}
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
_initSchema() {
|
|
115
132
|
if (!this.db) return;
|
|
116
|
-
|
|
117
|
-
db.
|
|
118
|
-
PRAGMA journal_mode=WAL;
|
|
119
|
-
PRAGMA busy_timeout=5000;
|
|
133
|
+
// sql.js doesn't support WAL mode (in-memory), skip PRAGMA journal_mode
|
|
134
|
+
this.db.run(`
|
|
120
135
|
CREATE TABLE IF NOT EXISTS meta (
|
|
121
136
|
key TEXT PRIMARY KEY,
|
|
122
137
|
value TEXT
|
|
123
|
-
)
|
|
138
|
+
)
|
|
139
|
+
`);
|
|
140
|
+
this.db.run(`
|
|
124
141
|
CREATE TABLE IF NOT EXISTS files (
|
|
125
142
|
path TEXT PRIMARY KEY,
|
|
126
143
|
sig TEXT,
|
|
127
144
|
updatedAt TEXT
|
|
128
|
-
)
|
|
145
|
+
)
|
|
146
|
+
`);
|
|
147
|
+
this.db.run(`
|
|
129
148
|
CREATE TABLE IF NOT EXISTS symbols (
|
|
130
149
|
path TEXT NOT NULL,
|
|
131
150
|
name TEXT NOT NULL,
|
|
@@ -134,33 +153,36 @@ export class CodeIndex {
|
|
|
134
153
|
namespace TEXT,
|
|
135
154
|
line INTEGER,
|
|
136
155
|
column INTEGER
|
|
137
|
-
)
|
|
138
|
-
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
|
|
139
|
-
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
|
|
140
|
-
CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path);
|
|
156
|
+
)
|
|
141
157
|
`);
|
|
158
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)');
|
|
159
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind)');
|
|
160
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path)');
|
|
161
|
+
this._saveToFile();
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
async isReady() {
|
|
145
165
|
const db = await this.open();
|
|
146
166
|
if (!db) return false;
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
return (row?.c || 0) > 0;
|
|
167
|
+
const result = querySQL(db, 'SELECT COUNT(*) AS c FROM symbols');
|
|
168
|
+
const count = result.length > 0 && result[0].values.length > 0 ? result[0].values[0][0] : 0;
|
|
169
|
+
return count > 0;
|
|
151
170
|
}
|
|
152
171
|
|
|
153
172
|
async clearAndLoad(symbols) {
|
|
154
173
|
const db = await this.open();
|
|
155
|
-
if (!db) throw new Error('CodeIndex is unavailable (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
db.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
if (!db) throw new Error('CodeIndex is unavailable (sql.js not loaded)');
|
|
175
|
+
|
|
176
|
+
db.run('BEGIN TRANSACTION');
|
|
177
|
+
try {
|
|
178
|
+
db.run('DELETE FROM symbols');
|
|
179
|
+
db.run('DELETE FROM files');
|
|
180
|
+
|
|
181
|
+
const stmt = db.prepare(
|
|
182
|
+
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
183
|
+
);
|
|
184
|
+
for (const r of symbols || []) {
|
|
185
|
+
stmt.run([
|
|
164
186
|
r.path,
|
|
165
187
|
r.name,
|
|
166
188
|
r.kind,
|
|
@@ -168,14 +190,20 @@ export class CodeIndex {
|
|
|
168
190
|
r.ns || r.namespace || null,
|
|
169
191
|
r.line || null,
|
|
170
192
|
r.column || null
|
|
171
|
-
);
|
|
193
|
+
]);
|
|
172
194
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
|
|
195
|
+
stmt.free();
|
|
196
|
+
|
|
197
|
+
const metaStmt = db.prepare('REPLACE INTO meta(key,value) VALUES (?,?)');
|
|
198
|
+
metaStmt.run(['lastIndexedAt', new Date().toISOString()]);
|
|
199
|
+
metaStmt.free();
|
|
200
|
+
|
|
201
|
+
db.run('COMMIT');
|
|
202
|
+
this._saveToFile();
|
|
203
|
+
} catch (e) {
|
|
204
|
+
db.run('ROLLBACK');
|
|
205
|
+
throw e;
|
|
206
|
+
}
|
|
179
207
|
return { total: symbols?.length || 0 };
|
|
180
208
|
}
|
|
181
209
|
|
|
@@ -183,90 +211,127 @@ export class CodeIndex {
|
|
|
183
211
|
async getFiles() {
|
|
184
212
|
const db = await this.open();
|
|
185
213
|
if (!db) return new Map();
|
|
186
|
-
|
|
187
|
-
const readDb = this._getReadDb();
|
|
188
|
-
const rows = readDb.prepare('SELECT path, sig FROM files').all();
|
|
214
|
+
const result = querySQL(db, 'SELECT path, sig FROM files');
|
|
189
215
|
const map = new Map();
|
|
190
|
-
|
|
216
|
+
if (result.length > 0) {
|
|
217
|
+
for (const row of result[0].values) {
|
|
218
|
+
map.set(String(row[0]), String(row[1] || ''));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
191
221
|
return map;
|
|
192
222
|
}
|
|
193
223
|
|
|
194
224
|
async upsertFile(pathStr, sig) {
|
|
195
225
|
const db = await this.open();
|
|
196
226
|
if (!db) return;
|
|
197
|
-
db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)')
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
227
|
+
const stmt = db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)');
|
|
228
|
+
stmt.run([pathStr, sig || '', new Date().toISOString()]);
|
|
229
|
+
stmt.free();
|
|
230
|
+
this._saveToFile();
|
|
202
231
|
}
|
|
203
232
|
|
|
204
233
|
async removeFile(pathStr) {
|
|
205
234
|
const db = await this.open();
|
|
206
235
|
if (!db) return;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
db.prepare('DELETE FROM
|
|
210
|
-
|
|
211
|
-
|
|
236
|
+
db.run('BEGIN TRANSACTION');
|
|
237
|
+
try {
|
|
238
|
+
const stmt1 = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
239
|
+
stmt1.run([pathStr]);
|
|
240
|
+
stmt1.free();
|
|
241
|
+
|
|
242
|
+
const stmt2 = db.prepare('DELETE FROM files WHERE path = ?');
|
|
243
|
+
stmt2.run([pathStr]);
|
|
244
|
+
stmt2.free();
|
|
245
|
+
|
|
246
|
+
db.run('COMMIT');
|
|
247
|
+
this._saveToFile();
|
|
248
|
+
} catch (e) {
|
|
249
|
+
db.run('ROLLBACK');
|
|
250
|
+
throw e;
|
|
251
|
+
}
|
|
212
252
|
}
|
|
213
253
|
|
|
214
254
|
async replaceSymbolsForPath(pathStr, rows) {
|
|
215
255
|
const db = await this.open();
|
|
216
256
|
if (!db) return;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
|
|
258
|
+
db.run('BEGIN TRANSACTION');
|
|
259
|
+
try {
|
|
260
|
+
const delStmt = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
261
|
+
delStmt.run([pathStr]);
|
|
262
|
+
delStmt.free();
|
|
263
|
+
|
|
264
|
+
const insertStmt = db.prepare(
|
|
220
265
|
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
221
266
|
);
|
|
222
|
-
for (const r of
|
|
223
|
-
|
|
224
|
-
|
|
267
|
+
for (const r of rows || []) {
|
|
268
|
+
insertStmt.run([
|
|
269
|
+
pathStr,
|
|
225
270
|
r.name,
|
|
226
271
|
r.kind,
|
|
227
272
|
r.container || null,
|
|
228
273
|
r.ns || r.namespace || null,
|
|
229
274
|
r.line || null,
|
|
230
275
|
r.column || null
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
|
|
276
|
+
]);
|
|
277
|
+
}
|
|
278
|
+
insertStmt.free();
|
|
279
|
+
|
|
280
|
+
const metaStmt = db.prepare('REPLACE INTO meta(key,value) VALUES (?,?)');
|
|
281
|
+
metaStmt.run(['lastIndexedAt', new Date().toISOString()]);
|
|
282
|
+
metaStmt.free();
|
|
283
|
+
|
|
284
|
+
db.run('COMMIT');
|
|
285
|
+
this._saveToFile();
|
|
286
|
+
} catch (e) {
|
|
287
|
+
db.run('ROLLBACK');
|
|
288
|
+
throw e;
|
|
289
|
+
}
|
|
238
290
|
}
|
|
239
291
|
|
|
240
292
|
async querySymbols({ name, kind, scope = 'all', exact = false }) {
|
|
241
293
|
const db = await this.open();
|
|
242
294
|
if (!db) return [];
|
|
243
|
-
|
|
244
|
-
const readDb = this._getReadDb();
|
|
295
|
+
|
|
245
296
|
let sql = 'SELECT path,name,kind,container,namespace,line,column FROM symbols WHERE 1=1';
|
|
246
|
-
const params =
|
|
297
|
+
const params = [];
|
|
298
|
+
|
|
247
299
|
if (name) {
|
|
248
300
|
if (exact) {
|
|
249
|
-
sql += ' AND name =
|
|
250
|
-
params.name
|
|
301
|
+
sql += ' AND name = ?';
|
|
302
|
+
params.push(name);
|
|
251
303
|
} else {
|
|
252
|
-
sql += ' AND name LIKE
|
|
253
|
-
params.
|
|
304
|
+
sql += ' AND name LIKE ?';
|
|
305
|
+
params.push(`%${name}%`);
|
|
254
306
|
}
|
|
255
307
|
}
|
|
256
308
|
if (kind) {
|
|
257
|
-
sql += ' AND kind =
|
|
258
|
-
params.kind
|
|
309
|
+
sql += ' AND kind = ?';
|
|
310
|
+
params.push(kind);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const stmt = db.prepare(sql);
|
|
314
|
+
if (params.length > 0) {
|
|
315
|
+
stmt.bind(params);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const rows = [];
|
|
319
|
+
while (stmt.step()) {
|
|
320
|
+
const row = stmt.getAsObject();
|
|
321
|
+
rows.push(row);
|
|
259
322
|
}
|
|
260
|
-
|
|
261
|
-
|
|
323
|
+
stmt.free();
|
|
324
|
+
|
|
325
|
+
// Apply path-based scope filter in JS
|
|
262
326
|
const filtered = rows.filter(r => {
|
|
263
|
-
const p = String(r.path || '').replace(
|
|
327
|
+
const p = String(r.path || '').replace(/\\/g, '/');
|
|
264
328
|
if (scope === 'assets') return p.startsWith('Assets/');
|
|
265
329
|
if (scope === 'packages')
|
|
266
330
|
return p.startsWith('Packages/') || p.includes('Library/PackageCache/');
|
|
267
331
|
if (scope === 'embedded') return p.startsWith('Packages/');
|
|
268
332
|
return true;
|
|
269
333
|
});
|
|
334
|
+
|
|
270
335
|
return filtered.map(r => ({
|
|
271
336
|
path: r.path,
|
|
272
337
|
name: r.name,
|
|
@@ -281,13 +346,32 @@ export class CodeIndex {
|
|
|
281
346
|
async getStats() {
|
|
282
347
|
const db = await this.open();
|
|
283
348
|
if (!db) return { total: 0, lastIndexedAt: null };
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
const total =
|
|
349
|
+
|
|
350
|
+
const countResult = querySQL(db, 'SELECT COUNT(*) AS c FROM symbols');
|
|
351
|
+
const total =
|
|
352
|
+
countResult.length > 0 && countResult[0].values.length > 0 ? countResult[0].values[0][0] : 0;
|
|
353
|
+
|
|
354
|
+
const metaResult = querySQL(db, "SELECT value FROM meta WHERE key = 'lastIndexedAt'");
|
|
287
355
|
const last =
|
|
288
|
-
|
|
356
|
+
metaResult.length > 0 && metaResult[0].values.length > 0 ? metaResult[0].values[0][0] : null;
|
|
357
|
+
|
|
289
358
|
return { total, lastIndexedAt: last };
|
|
290
359
|
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Close the database connection
|
|
363
|
+
*/
|
|
364
|
+
close() {
|
|
365
|
+
if (this.db) {
|
|
366
|
+
this._saveToFile();
|
|
367
|
+
this.db.close();
|
|
368
|
+
this.db = null;
|
|
369
|
+
}
|
|
370
|
+
if (sharedConnections.db === this.db) {
|
|
371
|
+
sharedConnections.db = null;
|
|
372
|
+
sharedConnections.dbPath = null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
291
375
|
}
|
|
292
376
|
|
|
293
377
|
// Test-only helper to reset cached driver status between runs
|
|
@@ -296,18 +380,15 @@ export function __resetCodeIndexDriverStatusForTest() {
|
|
|
296
380
|
driverStatus.error = null;
|
|
297
381
|
driverStatus.logged = false;
|
|
298
382
|
// Also reset shared connections
|
|
299
|
-
if (sharedConnections.
|
|
383
|
+
if (sharedConnections.db) {
|
|
300
384
|
try {
|
|
301
|
-
sharedConnections.
|
|
302
|
-
} catch {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
sharedConnections.readDb.close();
|
|
307
|
-
} catch {}
|
|
385
|
+
sharedConnections.db.close();
|
|
386
|
+
} catch {
|
|
387
|
+
// Ignore close errors
|
|
388
|
+
}
|
|
308
389
|
}
|
|
309
|
-
sharedConnections.
|
|
310
|
-
sharedConnections.readDb = null;
|
|
390
|
+
sharedConnections.db = null;
|
|
311
391
|
sharedConnections.dbPath = null;
|
|
312
392
|
sharedConnections.schemaInitialized = false;
|
|
393
|
+
sharedConnections.SQL = null;
|
|
313
394
|
}
|
|
@@ -12,7 +12,7 @@ const __dirname = path.dirname(__filename);
|
|
|
12
12
|
*
|
|
13
13
|
* This class manages Worker Threads that execute index builds in a separate
|
|
14
14
|
* thread, preventing the main Node.js event loop from being blocked by
|
|
15
|
-
*
|
|
15
|
+
* sql.js database operations.
|
|
16
16
|
*
|
|
17
17
|
* Requirements:
|
|
18
18
|
* - FR-056: Execute index build in Worker Thread
|