merge in v0.9.0

This commit is contained in:
webdevcody
2026-01-07 18:22:32 -05:00
9 changed files with 1215 additions and 17 deletions

View File

@@ -53,6 +53,8 @@ import { SettingsService } from './services/settings-service.js';
import { createSpecRegenerationRoutes } from './routes/app-spec/index.js';
import { createClaudeRoutes } from './routes/claude/index.js';
import { ClaudeUsageService } from './services/claude-usage-service.js';
import { createCodexRoutes } from './routes/codex/index.js';
import { CodexUsageService } from './services/codex-usage-service.js';
import { createGitHubRoutes } from './routes/github/index.js';
import { createContextRoutes } from './routes/context/index.js';
import { createBacklogPlanRoutes } from './routes/backlog-plan/index.js';
@@ -166,6 +168,7 @@ const agentService = new AgentService(DATA_DIR, events, settingsService);
const featureLoader = new FeatureLoader();
const autoModeService = new AutoModeService(events, settingsService);
const claudeUsageService = new ClaudeUsageService();
const codexUsageService = new CodexUsageService();
const mcpTestService = new MCPTestService(settingsService);
const ideationService = new IdeationService(events, settingsService, featureLoader);
@@ -216,6 +219,7 @@ app.use('/api/templates', createTemplatesRoutes());
app.use('/api/terminal', createTerminalRoutes());
app.use('/api/settings', createSettingsRoutes(settingsService));
app.use('/api/claude', createClaudeRoutes(claudeUsageService));
app.use('/api/codex', createCodexRoutes(codexUsageService));
app.use('/api/github', createGitHubRoutes(events, settingsService));
app.use('/api/context', createContextRoutes(settingsService));
app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService));

View File

@@ -72,7 +72,7 @@ const CODEX_EVENT_TYPES = {
itemCompleted: 'item.completed',
itemStarted: 'item.started',
itemUpdated: 'item.updated',
threadCompleted: 'thread.completed',
turnCompleted: 'turn.completed',
error: 'error',
} as const;
@@ -817,7 +817,7 @@ export class CodexProvider extends BaseProvider {
continue;
}
if (eventType === CODEX_EVENT_TYPES.threadCompleted) {
if (eventType === CODEX_EVENT_TYPES.turnCompleted) {
const resultText = extractText(event.result) || undefined;
yield { type: 'result', subtype: 'success', result: resultText };
continue;

View File

@@ -0,0 +1,52 @@
import { Router, Request, Response } from 'express';
import { CodexUsageService } from '../../services/codex-usage-service.js';
import { createLogger } from '@automaker/utils';
const logger = createLogger('Codex');
export function createCodexRoutes(service: CodexUsageService): Router {
const router = Router();
// Get current usage (attempts to fetch from Codex CLI)
router.get('/usage', async (req: Request, res: Response) => {
try {
// Check if Codex CLI is available first
const isAvailable = await service.isAvailable();
if (!isAvailable) {
res.status(503).json({
error: 'Codex CLI not found',
message: "Please install Codex CLI and run 'codex login' to authenticate",
});
return;
}
const usage = await service.fetchUsageData();
res.json(usage);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
if (message.includes('not authenticated') || message.includes('login')) {
res.status(401).json({
error: 'Authentication required',
message: "Please run 'codex login' to authenticate",
});
} else if (message.includes('not available') || message.includes('does not provide')) {
// This is the expected case - Codex doesn't provide usage stats
res.status(503).json({
error: 'Usage statistics not available',
message: message,
});
} else if (message.includes('timed out')) {
res.status(504).json({
error: 'Command timed out',
message: 'The Codex CLI took too long to respond',
});
} else {
logger.error('Error fetching usage:', error);
res.status(500).json({ error: message });
}
}
});
return router;
}

View File

@@ -0,0 +1,112 @@
import { spawn } from 'child_process';
import * as os from 'os';
export interface CodexRateLimitWindow {
limit: number;
used: number;
remaining: number;
usedPercent: number;
windowDurationMins: number;
resetsAt: number;
}
export interface CodexCreditsSnapshot {
balance?: string;
unlimited?: boolean;
hasCredits?: boolean;
}
export type CodexPlanType = 'free' | 'plus' | 'pro' | 'team' | 'enterprise' | 'edu' | 'unknown';
export interface CodexUsageData {
rateLimits: {
primary?: CodexRateLimitWindow;
secondary?: CodexRateLimitWindow;
credits?: CodexCreditsSnapshot;
planType?: CodexPlanType;
} | null;
lastUpdated: string;
}
/**
* Codex Usage Service
*
* Unlike Claude Code CLI which provides a `/usage` command, Codex CLI
* does not expose usage statistics directly. This service returns a
* clear message explaining this limitation.
*
* Future enhancement: Could query OpenAI API headers for rate limit info.
*/
export class CodexUsageService {
private codexBinary = 'codex';
private isWindows = os.platform() === 'win32';
/**
* Check if Codex CLI is available on the system
*/
async isAvailable(): Promise<boolean> {
return new Promise((resolve) => {
const checkCmd = this.isWindows ? 'where' : 'which';
const proc = spawn(checkCmd, [this.codexBinary]);
proc.on('close', (code) => {
resolve(code === 0);
});
proc.on('error', () => {
resolve(false);
});
});
}
/**
* Attempt to fetch usage data
*
* Note: Codex CLI doesn't provide usage statistics like Claude Code does.
* This method returns an error explaining this limitation.
*/
async fetchUsageData(): Promise<CodexUsageData> {
// Check authentication status first
const isAuthenticated = await this.checkAuthentication();
if (!isAuthenticated) {
throw new Error("Codex is not authenticated. Please run 'codex login' to authenticate.");
}
// Codex CLI doesn't provide a usage command
// Return an error that will be caught and displayed
throw new Error(
'Codex usage statistics are not available. Unlike Claude Code, the Codex CLI does not provide a built-in usage command. ' +
'Usage limits are enforced by OpenAI but cannot be queried via the CLI. ' +
'Check your OpenAI dashboard at https://platform.openai.com/usage for detailed usage information.'
);
}
/**
* Check if Codex is authenticated
*/
private async checkAuthentication(): Promise<boolean> {
return new Promise((resolve) => {
const proc = spawn(this.codexBinary, ['login', 'status'], {
env: {
...process.env,
TERM: 'dumb', // Avoid interactive output
},
});
let output = '';
proc.stdout.on('data', (data) => {
output += data.toString();
});
proc.on('close', (code) => {
// Check if output indicates logged in
const isLoggedIn = output.toLowerCase().includes('logged in');
resolve(code === 0 && isLoggedIn);
});
proc.on('error', () => {
resolve(false);
});
});
}
}