@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 CHANGED
@@ -309,64 +309,46 @@ Add to your `claude_desktop_config.json`:
309
309
 
310
310
  ## Architecture
311
311
 
312
- ### Native Dependencies
312
+ ### Dependencies
313
313
 
314
- Unity MCP Server uses platform-specific native binaries for performance-critical operations:
314
+ Unity MCP Server uses the following components for code indexing and analysis:
315
315
 
316
- | Component | Purpose | Distribution | Size per platform |
317
- |-----------|---------|--------------|-------------------|
318
- | **better-sqlite3** | Code index database | Bundled in npm package | ~3-5 MB |
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 better-sqlite3 over JavaScript-based SQLite?
321
+ #### Why sql.js?
322
322
 
323
- We chose **better-sqlite3** (native C binding) instead of JavaScript-based alternatives like **sql.js** for the following reasons:
323
+ We use **sql.js** (pure JavaScript/WebAssembly SQLite) for the code index database:
324
324
 
325
- | Aspect | better-sqlite3 (native) | sql.js (WASM/JS) |
326
- |--------|------------------------|------------------|
327
- | **Performance** | 10-100x faster for bulk operations | Slower due to WASM overhead |
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
- The code index database may contain tens of thousands of symbols from Unity projects. Native SQLite provides the performance characteristics needed for responsive symbol searches and incremental updates.
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
- **Trade-off**: Requires platform-specific binaries (bundled for all supported platforms), while sql.js would work everywhere but with unacceptable performance for our use case.
331
+ #### csharp-lsp Distribution
336
332
 
337
- #### Why Different Distribution Methods?
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 (would increase package size 100x)
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 | better-sqlite3 | csharp-lsp |
354
- |----------|---------------|------------|
355
- | Linux x64 | ✅ Bundled | ✅ Downloaded (~79 MB) |
356
- | Linux arm64 | ✅ Bundled | ✅ Downloaded (~86 MB) |
357
- | macOS x64 | ✅ Bundled | ✅ Downloaded (~80 MB) |
358
- | macOS arm64 (Apple Silicon) | ✅ Bundled | ✅ Downloaded (~86 MB) |
359
- | Windows x64 | ✅ Bundled | ✅ Downloaded (~80 MB) |
360
- | Windows arm64 | ✅ Bundled | ✅ Downloaded (~85 MB) |
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.43.3",
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 tests/unit/scripts/ensure-better-sqlite3.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
- "prebuild:better-sqlite3": "node scripts/prebuild-better-sqlite3.mjs",
30
- "prebuilt:manifest": "node scripts/generate-prebuilt-manifest.mjs",
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
- "better-sqlite3": "^9.4.3",
55
- "find-up": "^6.3.0"
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
  ],
@@ -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
- writeDb: null,
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; // legacy reference, points to writeDb
31
+ this.db = null;
26
32
  this.dbPath = null;
27
- this.disabled = false; // set true if better-sqlite3 is unavailable
33
+ this.disabled = false;
28
34
  this.disableReason = null;
29
- this._Database = null;
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._Database) return true;
44
+ if (this._SQL) return true;
39
45
  try {
40
- // Dynamic import to avoid hard failure when native binding is missing
41
- const mod = await import('better-sqlite3');
42
- this._Database = mod.default || mod;
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
- this.disableReason = `better-sqlite3 native binding unavailable: ${e?.message || e}. Code index features are disabled.`;
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; // index disabled
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 connections for all CodeIndex instances
68
- if (sharedConnections.writeDb && sharedConnections.dbPath === dbPath) {
69
- this.db = sharedConnections.writeDb;
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 (this._Database) {
75
- // Create write connection (default)
76
- sharedConnections.writeDb = new this._Database(dbPath);
77
- sharedConnections.dbPath = dbPath;
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
- throw new Error('No database driver available');
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
- this.disableReason = e?.message || 'Failed to open code index database';
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
- * Get read-only DB connection for queries (non-blocking during writes)
100
- * Falls back to write connection if read connection unavailable
104
+ * Save in-memory database to file
105
+ * sql.js requires explicit save (unlike better-sqlite3 which auto-persists)
101
106
  */
102
- _getReadDb() {
103
- return sharedConnections.readDb || this.db;
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?.warn?.(`[index] code index disabled: ${reason}`);
111
- } catch {}
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
- const db = this.db;
117
- db.exec(`
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
- // Use read-only connection for non-blocking check
148
- const readDb = this._getReadDb();
149
- const row = readDb.prepare('SELECT COUNT(*) AS c FROM symbols').get();
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 (better-sqlite3 not installed)');
156
- const insert = db.prepare(
157
- 'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
158
- );
159
- const tx = db.transaction(rows => {
160
- db.exec('DELETE FROM symbols');
161
- db.exec('DELETE FROM files');
162
- for (const r of rows) {
163
- insert.run(
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
- db.prepare('REPLACE INTO meta(key,value) VALUES (?,?)').run(
174
- 'lastIndexedAt',
175
- new Date().toISOString()
176
- );
177
- });
178
- tx(symbols || []);
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
- // Use read-only connection for non-blocking read
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
- for (const r of rows) map.set(String(r.path), String(r.sig || ''));
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 (?,?,?)').run(
198
- pathStr,
199
- sig || '',
200
- new Date().toISOString()
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
- const tx = db.transaction(p => {
208
- db.prepare('DELETE FROM symbols WHERE path = ?').run(p);
209
- db.prepare('DELETE FROM files WHERE path = ?').run(p);
210
- });
211
- tx(pathStr);
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
- const tx = db.transaction((p, list) => {
218
- db.prepare('DELETE FROM symbols WHERE path = ?').run(p);
219
- const insert = db.prepare(
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 list)
223
- insert.run(
224
- p,
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
- db.prepare('REPLACE INTO meta(key,value) VALUES (?,?)').run(
233
- 'lastIndexedAt',
234
- new Date().toISOString()
235
- );
236
- });
237
- tx(pathStr, rows || []);
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
- // Use read-only connection for non-blocking query
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 = @name';
250
- params.name = name;
301
+ sql += ' AND name = ?';
302
+ params.push(name);
251
303
  } else {
252
- sql += ' AND name LIKE @name';
253
- params.name = `%${name}%`;
304
+ sql += ' AND name LIKE ?';
305
+ params.push(`%${name}%`);
254
306
  }
255
307
  }
256
308
  if (kind) {
257
- sql += ' AND kind = @kind';
258
- params.kind = 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
- const rows = readDb.prepare(sql).all(params);
261
- // Apply path-based scope filter in JS (simpler than CASE in SQL)
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(/\\\\/g, '/');
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
- // Use read-only connection for non-blocking stats
285
- const readDb = this._getReadDb();
286
- const total = readDb.prepare('SELECT COUNT(*) AS c FROM symbols').get().c || 0;
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
- readDb.prepare("SELECT value AS v FROM meta WHERE key = 'lastIndexedAt'").get()?.v || null;
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.writeDb) {
383
+ if (sharedConnections.db) {
300
384
  try {
301
- sharedConnections.writeDb.close();
302
- } catch {}
303
- }
304
- if (sharedConnections.readDb) {
305
- try {
306
- sharedConnections.readDb.close();
307
- } catch {}
385
+ sharedConnections.db.close();
386
+ } catch {
387
+ // Ignore close errors
388
+ }
308
389
  }
309
- sharedConnections.writeDb = null;
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
- * better-sqlite3's synchronous database operations.
15
+ * sql.js database operations.
16
16
  *
17
17
  * Requirements:
18
18
  * - FR-056: Execute index build in Worker Thread