()
| 38 | import { fileURLToPath } from 'url'; |
| 39 | import { logger } from './utils/logger.js'; |
| 40 | |
| 41 | async function bootstrap(): Promise<{ |
| 42 | config: Awaited<ReturnType<typeof loadConfig>>; |
| 43 | router: ModelRouter; |
| 44 | registry: ToolRegistry; |
| 45 | permissions: PermissionEngine; |
| 46 | mcpManager: MCPManager; |
| 47 | codeGraph: CodeGraphDB; |
| 48 | indexer: Indexer; |
| 49 | hooks: HooksManager; |
| 50 | }> { |
| 51 | await ensureQodexHome(); |
| 52 | await logger.init('info'); |
| 53 | logger.info('QodeX starting', { node: process.version, cwd: process.cwd() }); |
| 54 | |
| 55 | // Load any API keys stored in ~/.qodex/.env (written by `qodex provider add`) into the |
| 56 | // environment BEFORE the router reads them. An explicit `export` in the user's shell still |
| 57 | // wins, so this never overrides a key they set themselves. |
| 58 | try { |
| 59 | const { loadEnvFileIntoProcess } = await import('./setup/env-writer.js'); |
| 60 | const n = await loadEnvFileIntoProcess(); |
| 61 | if (n > 0) logger.info(`Loaded ${n} API key(s) from ~/.qodex/.env`); |
| 62 | } catch (e: any) { |
| 63 | logger.debug('No ~/.qodex/.env loaded', { err: e?.message }); |
| 64 | } |
| 65 | |
| 66 | // Warm the real BPE tokenizer in the background (gpt-tokenizer, if installed). |
| 67 | // Non-blocking: token counts use the calibrated heuristic until this resolves. |
| 68 | void import('./utils/tokenizer.js').then(t => t.warmTokenizer()).catch((err) => logger.debug('tokenizer warm-up failed', { err })); |
| 69 | |
| 70 | const config = await loadConfig(process.cwd()); |
| 71 | // Import Claude Code plugins/standalone assets: agents → dispatchable roles (for the |
| 72 | // `task` tool + plugin commands like /review-pr), plus plugin-declared MCP servers and |
| 73 | // hooks. User config always wins on collisions. (Skills/commands are loaded separately |
| 74 | // by their own loaders.) Disable all of this with QODEX_DISABLE_CLAUDE_PLUGINS=1. |
| 75 | try { |
| 76 | const { applyClaudeCodeIntegration } = await import('./integrations/claude-plugins.js'); |
| 77 | const r = await applyClaudeCodeIntegration(config, process.cwd()); |
| 78 | if (r.agents || r.mcp || r.hooks) { |
| 79 | logger.info('Claude Code integration applied', r); |
| 80 | } |
| 81 | } catch (e: any) { |
| 82 | logger.debug('Claude Code integration skipped', { err: e?.message }); |
| 83 | } |
| 84 | setActiveConfig(config); |
| 85 | // Browser CDP attach: if configured, the browser_* tools attach to the user's running |
| 86 | // browser instead of launching a fresh headless one. (QODEX_BROWSER_CDP_URL env wins.) |
| 87 | if ((config as any).browser?.cdpUrl) { |
| 88 | try { |
| 89 | const { setBrowserCdpUrl } = await import('./tools/browser/session.js'); |
| 90 | setBrowserCdpUrl((config as any).browser.cdpUrl); |
| 91 | } catch { /* browser module optional */ } |
| 92 | } |
| 93 | const router = new ModelRouter(config); |
| 94 | const registry = new ToolRegistry(); |
| 95 | const permissions = new PermissionEngine(config); |
| 96 | |
| 97 | // Code graph — project-local SQLite |
no test coverage detected