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 +};