| import { spawn, type ChildProcess } from 'child_process'; |
| import readline from 'readline'; |
| import { findCodexCliPath } from '@automaker/platform'; |
| import { createLogger } from '@automaker/utils'; |
| import type { |
| AppServerModelResponse, |
| AppServerAccountResponse, |
| AppServerRateLimitsResponse, |
| JsonRpcRequest, |
| } from '@automaker/types'; |
|
|
| const logger = createLogger('CodexAppServer'); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export class CodexAppServerService { |
| private cachedCliPath: string | null = null; |
|
|
| |
| |
| |
| async isAvailable(): Promise<boolean> { |
| this.cachedCliPath = await findCodexCliPath(); |
| return Boolean(this.cachedCliPath); |
| } |
|
|
| |
| |
| |
| async getModels(): Promise<AppServerModelResponse | null> { |
| const result = await this.executeJsonRpc<AppServerModelResponse>((sendRequest) => { |
| return sendRequest('model/list', {}); |
| }); |
|
|
| if (result) { |
| logger.info(`[getModels] ✓ Fetched ${result.data.length} models`); |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
| async getAccount(): Promise<AppServerAccountResponse | null> { |
| return this.executeJsonRpc<AppServerAccountResponse>((sendRequest) => { |
| return sendRequest('account/read', { refreshToken: false }); |
| }); |
| } |
|
|
| |
| |
| |
| async getRateLimits(): Promise<AppServerRateLimitsResponse | null> { |
| return this.executeJsonRpc<AppServerRateLimitsResponse>((sendRequest) => { |
| return sendRequest('account/rateLimits/read', {}); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private async executeJsonRpc<T>( |
| requestFn: (sendRequest: <R>(method: string, params?: unknown) => Promise<R>) => Promise<T> |
| ): Promise<T | null> { |
| let childProcess: ChildProcess | null = null; |
|
|
| try { |
| const cliPath = this.cachedCliPath || (await findCodexCliPath()); |
|
|
| if (!cliPath) { |
| return null; |
| } |
|
|
| |
| const needsShell = process.platform === 'win32' && cliPath.toLowerCase().endsWith('.cmd'); |
|
|
| childProcess = spawn(cliPath, ['app-server'], { |
| cwd: process.cwd(), |
| env: { |
| ...process.env, |
| TERM: 'dumb', |
| }, |
| stdio: ['pipe', 'pipe', 'pipe'], |
| shell: needsShell, |
| }); |
|
|
| if (!childProcess.stdin || !childProcess.stdout) { |
| throw new Error('Failed to create stdio pipes'); |
| } |
|
|
| |
| const rl = readline.createInterface({ |
| input: childProcess.stdout, |
| crlfDelay: Infinity, |
| }); |
|
|
| |
| let messageId = 0; |
| const pendingRequests = new Map< |
| number, |
| { |
| resolve: (value: unknown) => void; |
| reject: (error: Error) => void; |
| timeout: NodeJS.Timeout; |
| } |
| >(); |
|
|
| |
| rl.on('line', (line) => { |
| if (!line.trim()) return; |
|
|
| try { |
| const message = JSON.parse(line); |
|
|
| |
| if ('id' in message && message.id !== undefined) { |
| const pending = pendingRequests.get(message.id); |
| if (pending) { |
| clearTimeout(pending.timeout); |
| pendingRequests.delete(message.id); |
| if (message.error) { |
| pending.reject(new Error(message.error.message || 'Unknown error')); |
| } else { |
| pending.resolve(message.result); |
| } |
| } |
| } |
| |
| } catch { |
| |
| } |
| }); |
|
|
| |
| const sendRequest = <R>(method: string, params?: unknown): Promise<R> => { |
| return new Promise((resolve, reject) => { |
| const id = ++messageId; |
| const request: JsonRpcRequest = { |
| method, |
| id, |
| params: params ?? {}, |
| }; |
|
|
| |
| const timeout = setTimeout(() => { |
| pendingRequests.delete(id); |
| reject(new Error(`Request timeout: ${method}`)); |
| }, 10000); |
|
|
| pendingRequests.set(id, { |
| resolve: resolve as (value: unknown) => void, |
| reject, |
| timeout, |
| }); |
|
|
| childProcess!.stdin!.write(JSON.stringify(request) + '\n'); |
| }); |
| }; |
|
|
| |
| const sendNotification = (method: string, params?: unknown): void => { |
| const notification = params ? { method, params } : { method }; |
| childProcess!.stdin!.write(JSON.stringify(notification) + '\n'); |
| }; |
|
|
| |
| await sendRequest('initialize', { |
| clientInfo: { |
| name: 'automaker', |
| title: 'AutoMaker', |
| version: '1.0.0', |
| }, |
| }); |
|
|
| |
| sendNotification('initialized'); |
|
|
| |
| const result = await requestFn(sendRequest); |
|
|
| |
| rl.close(); |
| childProcess.kill('SIGTERM'); |
|
|
| return result; |
| } catch (error) { |
| logger.error('[executeJsonRpc] Failed:', error); |
| return null; |
| } finally { |
| |
| if (childProcess && !childProcess.killed) { |
| childProcess.kill('SIGTERM'); |
| } |
| } |
| } |
| } |
|
|