From e6027ec5a6ce98ae5991de46c6f0bb1d6d009124 Mon Sep 17 00:00:00 2001 From: jgor20 <102353650+jgor20@users.noreply.github.com> Date: Mon, 5 Jan 2026 00:43:21 +0000 Subject: [PATCH 1/7] feat: auto-rebuild native modules on Node.js version mismatch When Node.js is updated, native modules like better-sqlite3 can become incompatible due to NODE_MODULE_VERSION differences. This change adds automatic detection and rebuild capability: - Add native-module-helper.js utility for detecting version errors - Lazy-load better-sqlite3 to catch import errors at runtime - Automatically run npm rebuild when version mismatch is detected - Clear require cache and retry loading after successful rebuild - Provide clear instructions if automatic rebuild fails Fixes the issue where users running via npx encounter module errors after updating Node.js. --- CLAUDE.md | 12 ++- src/auth/database.js | 104 ++++++++++++++++++++++- src/utils/native-module-helper.js | 136 ++++++++++++++++++++++++++++++ 3 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 src/utils/native-module-helper.js diff --git a/CLAUDE.md b/CLAUDE.md index 6aae1b3..285a142 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,7 +101,8 @@ src/ │ └── utils/ # Utilities ├── helpers.js # formatDuration, sleep - └── logger.js # Structured logging + ├── logger.js # Structured logging + └── native-module-helper.js # Auto-rebuild for native modules ``` **Key Modules:** @@ -109,7 +110,7 @@ src/ - **src/server.js**: Express server exposing Anthropic-compatible endpoints (`/v1/messages`, `/v1/models`, `/health`, `/account-limits`) - **src/cloudcode/**: Cloud Code API client with retry/failover logic, streaming and non-streaming support - **src/account-manager/**: Multi-account pool with sticky selection, rate limit handling, and automatic cooldown -- **src/auth/**: Authentication including Google OAuth, token extraction, and database access +- **src/auth/**: Authentication including Google OAuth, token extraction, database access, and auto-rebuild of native modules - **src/format/**: Format conversion between Anthropic and Google Generative AI formats - **src/constants.js**: API endpoints, model mappings, fallback config, OAuth config, and all configuration values - **src/fallback-config.js**: Model fallback mappings (`getFallbackModel()`, `hasFallback()`) @@ -144,6 +145,13 @@ src/ - For Gemini targets: strict validation - drops unknown or mismatched signatures - For Claude targets: lenient - lets Claude validate its own signatures +**Native Module Auto-Rebuild:** +- When Node.js is updated, native modules like `better-sqlite3` may become incompatible +- The proxy automatically detects `NODE_MODULE_VERSION` mismatch errors +- On detection, it attempts to rebuild the module using `npm rebuild` +- If rebuild succeeds, the module is reloaded; if reload fails, a server restart is required +- Implementation in `src/utils/native-module-helper.js` and lazy loading in `src/auth/database.js` + ## Testing Notes - Tests require the server to be running (`npm start` in separate terminal) diff --git a/src/auth/database.js b/src/auth/database.js index 980aa9a..db05290 100644 --- a/src/auth/database.js +++ b/src/auth/database.js @@ -6,10 +6,102 @@ * - Windows compatibility (no CLI dependency) * - Native performance * - Synchronous API (simple error handling) + * + * Includes auto-rebuild capability for handling Node.js version updates + * that cause native module incompatibility. */ -import Database from 'better-sqlite3'; +import { createRequire } from 'module'; import { ANTIGRAVITY_DB_PATH } from '../constants.js'; +import { isModuleVersionError, attemptAutoRebuild } from '../utils/native-module-helper.js'; +import { logger } from '../utils/logger.js'; + +const require = createRequire(import.meta.url); + +// Lazy-loaded Database constructor +let Database = null; +let moduleLoadError = null; + +/** + * Load the better-sqlite3 module with auto-rebuild on version mismatch + * Uses synchronous require to maintain API compatibility + * @returns {Function} The Database constructor + * @throws {Error} If module cannot be loaded even after rebuild + */ +function loadDatabaseModule() { + // Return cached module if already loaded + if (Database) return Database; + + // Re-throw cached error if previous load failed permanently + if (moduleLoadError) throw moduleLoadError; + + try { + Database = require('better-sqlite3'); + return Database; + } catch (error) { + if (isModuleVersionError(error)) { + logger.warn('[Database] Native module version mismatch detected'); + + if (attemptAutoRebuild(error)) { + // Clear require cache and retry + try { + const resolvedPath = require.resolve('better-sqlite3'); + // Clear the module and all its dependencies from cache + clearRequireCache(resolvedPath); + + Database = require('better-sqlite3'); + logger.success('[Database] Module reloaded successfully after rebuild'); + return Database; + } catch (retryError) { + // Rebuild succeeded but reload failed - user needs to restart + moduleLoadError = new Error( + 'Native module rebuild completed. Please restart the server to apply the fix.' + ); + logger.info('[Database] Rebuild succeeded - server restart required'); + throw moduleLoadError; + } + } else { + moduleLoadError = new Error( + 'Failed to auto-rebuild native module. Please run manually:\n' + + ' npm rebuild better-sqlite3\n' + + 'Or if using npx, find the package location in the error and run:\n' + + ' cd /path/to/better-sqlite3 && npm rebuild' + ); + throw moduleLoadError; + } + } + + // Non-version-mismatch error, just throw it + throw error; + } +} + +/** + * Clear a module and its dependencies from the require cache + * @param {string} modulePath - Resolved path to the module + */ +function clearRequireCache(modulePath) { + const mod = require.cache[modulePath]; + if (!mod) return; + + // Recursively clear children first + if (mod.children) { + for (const child of mod.children) { + clearRequireCache(child.id); + } + } + + // Remove from parent's children + if (mod.parent && mod.parent.children) { + const idx = mod.parent.children.indexOf(mod); + if (idx !== -1) { + mod.parent.children.splice(idx, 1); + } + } + + // Delete from cache + delete require.cache[modulePath]; +} /** * Query Antigravity database for authentication status @@ -18,10 +110,11 @@ import { ANTIGRAVITY_DB_PATH } from '../constants.js'; * @throws {Error} If database doesn't exist, query fails, or no auth status found */ export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) { + const Db = loadDatabaseModule(); let db; try { // Open database in read-only mode - db = new Database(dbPath, { + db = new Db(dbPath, { readonly: true, fileMustExist: true }); @@ -56,6 +149,10 @@ export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) { if (error.message.includes('No auth status') || error.message.includes('missing apiKey')) { throw error; } + // Check for version mismatch that might have been thrown by loadDatabaseModule + if (error.message.includes('restart the server') || error.message.includes('auto-rebuild')) { + throw error; + } throw new Error(`Failed to read Antigravity database: ${error.message}`); } finally { // Always close database connection @@ -73,7 +170,8 @@ export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) { export function isDatabaseAccessible(dbPath = ANTIGRAVITY_DB_PATH) { let db; try { - db = new Database(dbPath, { + const Db = loadDatabaseModule(); + db = new Db(dbPath, { readonly: true, fileMustExist: true }); diff --git a/src/utils/native-module-helper.js b/src/utils/native-module-helper.js new file mode 100644 index 0000000..d5285b7 --- /dev/null +++ b/src/utils/native-module-helper.js @@ -0,0 +1,136 @@ +/** + * Native Module Helper + * Detects and auto-rebuilds native Node.js modules when they become + * incompatible after a Node.js version update. + */ + +import { execSync } from 'child_process'; +import { dirname, join } from 'path'; +import { existsSync } from 'fs'; +import { createRequire } from 'module'; +import { logger } from './logger.js'; + +/** + * Check if an error is a NODE_MODULE_VERSION mismatch error + * @param {Error} error - The error to check + * @returns {boolean} True if it's a version mismatch error + */ +export function isModuleVersionError(error) { + const message = error?.message || ''; + return message.includes('NODE_MODULE_VERSION') && + message.includes('was compiled against a different Node.js version'); +} + +/** + * Extract the module path from a NODE_MODULE_VERSION error message + * @param {Error} error - The error containing the module path + * @returns {string|null} The path to the .node file, or null if not found + */ +export function extractModulePath(error) { + const message = error?.message || ''; + // Match pattern like: "The module '/path/to/module.node'" + const match = message.match(/The module '([^']+\.node)'/); + return match ? match[1] : null; +} + +/** + * Find the package root directory from a .node file path + * @param {string} nodeFilePath - Path to the .node file + * @returns {string|null} Path to the package root, or null if not found + */ +export function findPackageRoot(nodeFilePath) { + // Walk up from the .node file to find package.json + let dir = dirname(nodeFilePath); + while (dir && dir !== '/') { + const packageJsonPath = join(dir, 'package.json'); + if (existsSync(packageJsonPath)) { + return dir; + } + dir = dirname(dir); + } + return null; +} + +/** + * Attempt to rebuild a native module + * @param {string} packagePath - Path to the package root directory + * @returns {boolean} True if rebuild succeeded, false otherwise + */ +export function rebuildModule(packagePath) { + try { + logger.info(`[NativeModule] Rebuilding native module at: ${packagePath}`); + + // Run npm rebuild in the package directory + execSync('npm rebuild', { + cwd: packagePath, + stdio: 'pipe', // Capture output instead of printing + timeout: 120000 // 2 minute timeout + }); + + logger.success('[NativeModule] Rebuild completed successfully'); + return true; + } catch (error) { + logger.error(`[NativeModule] Rebuild failed: ${error.message}`); + return false; + } +} + +/** + * Attempt to auto-rebuild a native module from an error + * @param {Error} error - The NODE_MODULE_VERSION error + * @returns {boolean} True if rebuild succeeded, false otherwise + */ +export function attemptAutoRebuild(error) { + const nodePath = extractModulePath(error); + if (!nodePath) { + logger.error('[NativeModule] Could not extract module path from error'); + return false; + } + + const packagePath = findPackageRoot(nodePath); + if (!packagePath) { + logger.error('[NativeModule] Could not find package root'); + return false; + } + + logger.warn('[NativeModule] Native module version mismatch detected'); + logger.info('[NativeModule] Attempting automatic rebuild...'); + + return rebuildModule(packagePath); +} + +/** + * Clear the require cache for a module to force re-import + * This is needed after rebuilding a native module + * @param {string} moduleName - The module name (e.g., 'better-sqlite3') + */ +export function clearModuleCache(moduleName) { + const require = createRequire(import.meta.url); + try { + const resolved = require.resolve(moduleName); + // Clear the main module and its dependencies + const mod = require.cache[resolved]; + if (mod) { + // Remove from parent's children array + if (mod.parent) { + const idx = mod.parent.children.indexOf(mod); + if (idx !== -1) { + mod.parent.children.splice(idx, 1); + } + } + // Delete from cache + delete require.cache[resolved]; + } + } catch { + // Module might not be in cache, that's okay + } +} + +export default { + isModuleVersionError, + extractModulePath, + findPackageRoot, + rebuildModule, + attemptAutoRebuild, + clearModuleCache +}; From ff188f5cf63c58e2a3ad3baa22a6f1626da775a8 Mon Sep 17 00:00:00 2001 From: jgor20 <102353650+jgor20@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:09:00 +0000 Subject: [PATCH 2/7] fix(utils): fix cross-platform root detection in findPackageRoot Improve the findPackageRoot function to correctly detect filesystem root on all platforms by checking if dirname returns the same path, replacing the Unix-specific '/' check. --- src/utils/native-module-helper.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/utils/native-module-helper.js b/src/utils/native-module-helper.js index d5285b7..9a1d6fa 100644 --- a/src/utils/native-module-helper.js +++ b/src/utils/native-module-helper.js @@ -41,12 +41,17 @@ export function extractModulePath(error) { export function findPackageRoot(nodeFilePath) { // Walk up from the .node file to find package.json let dir = dirname(nodeFilePath); - while (dir && dir !== '/') { + while (dir) { const packageJsonPath = join(dir, 'package.json'); if (existsSync(packageJsonPath)) { return dir; } - dir = dirname(dir); + const parentDir = dirname(dir); + // Stop when we've reached the filesystem root (dirname returns same path) + if (parentDir === dir) { + break; + } + dir = parentDir; } return null; } From 02ceeb2ff5f11dd99cf9cc682428ac813edcbde5 Mon Sep 17 00:00:00 2001 From: jgor20 <102353650+jgor20@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:11:13 +0000 Subject: [PATCH 3/7] feat(utils): enhance rebuildModule error handling and logging Add detailed stdout/stderr capture and logging for failed npm rebuild commands to improve troubleshooting. Also log successful rebuild output for debugging purposes. --- src/utils/native-module-helper.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/utils/native-module-helper.js b/src/utils/native-module-helper.js index 9a1d6fa..5b71cfd 100644 --- a/src/utils/native-module-helper.js +++ b/src/utils/native-module-helper.js @@ -66,16 +66,32 @@ export function rebuildModule(packagePath) { logger.info(`[NativeModule] Rebuilding native module at: ${packagePath}`); // Run npm rebuild in the package directory - execSync('npm rebuild', { + const output = execSync('npm rebuild', { cwd: packagePath, stdio: 'pipe', // Capture output instead of printing timeout: 120000 // 2 minute timeout }); + // Log rebuild output for debugging + const outputStr = output?.toString().trim(); + if (outputStr) { + logger.debug(`[NativeModule] Rebuild output:\n${outputStr}`); + } + logger.success('[NativeModule] Rebuild completed successfully'); return true; } catch (error) { - logger.error(`[NativeModule] Rebuild failed: ${error.message}`); + // Include stdout/stderr from the failed command for troubleshooting + const stdout = error.stdout?.toString().trim(); + const stderr = error.stderr?.toString().trim(); + let errorDetails = `[NativeModule] Rebuild failed: ${error.message}`; + if (stdout) { + errorDetails += `\n[NativeModule] stdout: ${stdout}`; + } + if (stderr) { + errorDetails += `\n[NativeModule] stderr: ${stderr}`; + } + logger.error(errorDetails); return false; } } From 2d4693b4c6ef7ddb7795d0a8b7b8ed27ae1e53dd Mon Sep 17 00:00:00 2001 From: jgor20 <102353650+jgor20@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:13:45 +0000 Subject: [PATCH 4/7] refactor(utils): rename and refactor clearModuleCache to recursively clear dependencies The function has been renamed to clearRequireCache and updated to recursively clear the require cache for a module and all its dependencies, preventing cycles with a visited set. This improves reliability after rebuilding native modules by ensuring complete cache invalidation. Removed unused createRequire import as it's no longer needed. --- src/utils/native-module-helper.js | 51 +++++++++++++++++-------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/utils/native-module-helper.js b/src/utils/native-module-helper.js index 5b71cfd..c885684 100644 --- a/src/utils/native-module-helper.js +++ b/src/utils/native-module-helper.js @@ -7,7 +7,6 @@ import { execSync } from 'child_process'; import { dirname, join } from 'path'; import { existsSync } from 'fs'; -import { createRequire } from 'module'; import { logger } from './logger.js'; /** @@ -121,30 +120,36 @@ export function attemptAutoRebuild(error) { } /** - * Clear the require cache for a module to force re-import - * This is needed after rebuilding a native module - * @param {string} moduleName - The module name (e.g., 'better-sqlite3') + * Recursively clear a module and its dependencies from the require cache + * This is needed after rebuilding a native module to force re-import + * @param {string} modulePath - Resolved path to the module + * @param {object} cache - The require.cache object + * @param {Set} [visited] - Set of already-visited paths to prevent cycles */ -export function clearModuleCache(moduleName) { - const require = createRequire(import.meta.url); - try { - const resolved = require.resolve(moduleName); - // Clear the main module and its dependencies - const mod = require.cache[resolved]; - if (mod) { - // Remove from parent's children array - if (mod.parent) { - const idx = mod.parent.children.indexOf(mod); - if (idx !== -1) { - mod.parent.children.splice(idx, 1); - } - } - // Delete from cache - delete require.cache[resolved]; +export function clearRequireCache(modulePath, cache, visited = new Set()) { + if (visited.has(modulePath)) return; + visited.add(modulePath); + + const mod = cache[modulePath]; + if (!mod) return; + + // Recursively clear children first + if (mod.children) { + for (const child of mod.children) { + clearRequireCache(child.id, cache, visited); } - } catch { - // Module might not be in cache, that's okay } + + // Remove from parent's children array + if (mod.parent && mod.parent.children) { + const idx = mod.parent.children.indexOf(mod); + if (idx !== -1) { + mod.parent.children.splice(idx, 1); + } + } + + // Delete from cache + delete cache[modulePath]; } export default { @@ -153,5 +158,5 @@ export default { findPackageRoot, rebuildModule, attemptAutoRebuild, - clearModuleCache + clearRequireCache }; From 69b7e130a0d8c1c8361013862f7e87feddcb49cf Mon Sep 17 00:00:00 2001 From: jgor20 <102353650+jgor20@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:13:55 +0000 Subject: [PATCH 5/7] refactor(auth): move clearRequireCache to utils and update import Remove the local clearRequireCache function from database.js and import it from utils/native-module-helper.js. Update the function call to pass require.cache as the second parameter for proper cache clearing. --- src/auth/database.js | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/src/auth/database.js b/src/auth/database.js index db05290..a20b6e4 100644 --- a/src/auth/database.js +++ b/src/auth/database.js @@ -13,7 +13,7 @@ import { createRequire } from 'module'; import { ANTIGRAVITY_DB_PATH } from '../constants.js'; -import { isModuleVersionError, attemptAutoRebuild } from '../utils/native-module-helper.js'; +import { isModuleVersionError, attemptAutoRebuild, clearRequireCache } from '../utils/native-module-helper.js'; import { logger } from '../utils/logger.js'; const require = createRequire(import.meta.url); @@ -47,7 +47,7 @@ function loadDatabaseModule() { try { const resolvedPath = require.resolve('better-sqlite3'); // Clear the module and all its dependencies from cache - clearRequireCache(resolvedPath); + clearRequireCache(resolvedPath, require.cache); Database = require('better-sqlite3'); logger.success('[Database] Module reloaded successfully after rebuild'); @@ -76,33 +76,6 @@ function loadDatabaseModule() { } } -/** - * Clear a module and its dependencies from the require cache - * @param {string} modulePath - Resolved path to the module - */ -function clearRequireCache(modulePath) { - const mod = require.cache[modulePath]; - if (!mod) return; - - // Recursively clear children first - if (mod.children) { - for (const child of mod.children) { - clearRequireCache(child.id); - } - } - - // Remove from parent's children - if (mod.parent && mod.parent.children) { - const idx = mod.parent.children.indexOf(mod); - if (idx !== -1) { - mod.parent.children.splice(idx, 1); - } - } - - // Delete from cache - delete require.cache[modulePath]; -} - /** * Query Antigravity database for authentication status * @param {string} [dbPath] - Optional custom database path From b90eb63f22cccf916051d8a817c737c4e3563a6d Mon Sep 17 00:00:00 2001 From: jgor20 <102353650+jgor20@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:20:28 +0000 Subject: [PATCH 6/7] feat(errors): add NativeModuleError for native module version mismatches Add a new error class to handle native module errors, including version mismatches and rebuild requirements. This supports the auto-rebuild functionality by providing structured error information for rebuild success and restart needs. --- src/errors.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/errors.js b/src/errors.js index 43f8abc..eb2e755 100644 --- a/src/errors.js +++ b/src/errors.js @@ -118,6 +118,23 @@ export class ApiError extends AntigravityError { } } +/** + * Native module error (version mismatch, rebuild required) + */ +export class NativeModuleError extends AntigravityError { + /** + * @param {string} message - Error message + * @param {boolean} rebuildSucceeded - Whether auto-rebuild succeeded + * @param {boolean} restartRequired - Whether server restart is needed + */ + constructor(message, rebuildSucceeded = false, restartRequired = false) { + super(message, 'NATIVE_MODULE_ERROR', false, { rebuildSucceeded, restartRequired }); + this.name = 'NativeModuleError'; + this.rebuildSucceeded = rebuildSucceeded; + this.restartRequired = restartRequired; + } +} + /** * Check if an error is a rate limit error * Works with both custom error classes and legacy string-based errors @@ -154,6 +171,7 @@ export default { NoAccountsError, MaxRetriesError, ApiError, + NativeModuleError, isRateLimitError, isAuthError }; From e29cd5fa9d298e860ffa565864481001b8247d0b Mon Sep 17 00:00:00 2001 From: jgor20 <102353650+jgor20@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:20:35 +0000 Subject: [PATCH 7/7] refactor(auth): use NativeModuleError for native module load failures Replace generic Error instances with NativeModuleError in loadDatabaseModule to provide more structured error information, including rebuild status and restart requirements. Update getAuthStatus to re-throw NativeModuleError instances without wrapping. --- src/auth/database.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/auth/database.js b/src/auth/database.js index a20b6e4..a753667 100644 --- a/src/auth/database.js +++ b/src/auth/database.js @@ -15,6 +15,7 @@ import { createRequire } from 'module'; import { ANTIGRAVITY_DB_PATH } from '../constants.js'; import { isModuleVersionError, attemptAutoRebuild, clearRequireCache } from '../utils/native-module-helper.js'; import { logger } from '../utils/logger.js'; +import { NativeModuleError } from '../errors.js'; const require = createRequire(import.meta.url); @@ -54,18 +55,22 @@ function loadDatabaseModule() { return Database; } catch (retryError) { // Rebuild succeeded but reload failed - user needs to restart - moduleLoadError = new Error( - 'Native module rebuild completed. Please restart the server to apply the fix.' + moduleLoadError = new NativeModuleError( + 'Native module rebuild completed. Please restart the server to apply the fix.', + true, // rebuildSucceeded + true // restartRequired ); logger.info('[Database] Rebuild succeeded - server restart required'); throw moduleLoadError; } } else { - moduleLoadError = new Error( + moduleLoadError = new NativeModuleError( 'Failed to auto-rebuild native module. Please run manually:\n' + ' npm rebuild better-sqlite3\n' + 'Or if using npx, find the package location in the error and run:\n' + - ' cd /path/to/better-sqlite3 && npm rebuild' + ' cd /path/to/better-sqlite3 && npm rebuild', + false, // rebuildSucceeded + false // restartRequired ); throw moduleLoadError; } @@ -122,8 +127,8 @@ export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) { if (error.message.includes('No auth status') || error.message.includes('missing apiKey')) { throw error; } - // Check for version mismatch that might have been thrown by loadDatabaseModule - if (error.message.includes('restart the server') || error.message.includes('auto-rebuild')) { + // Re-throw native module errors from loadDatabaseModule without wrapping + if (error instanceof NativeModuleError) { throw error; } throw new Error(`Failed to read Antigravity database: ${error.message}`);