mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Merge pull request #400 from AutoMaker-Org/feat/codex-usage
feat: improve codex plan and usage detection
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -73,6 +73,9 @@ blob-report/
|
||||
!.env.example
|
||||
!.env.local.example
|
||||
|
||||
# Codex config (contains API keys)
|
||||
.codex/config.toml
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ 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 { CodexAppServerService } from './services/codex-app-server-service.js';
|
||||
import { CodexModelCacheService } from './services/codex-model-cache-service.js';
|
||||
import { createGitHubRoutes } from './routes/github/index.js';
|
||||
import { createContextRoutes } from './routes/context/index.js';
|
||||
import { createBacklogPlanRoutes } from './routes/backlog-plan/index.js';
|
||||
@@ -168,7 +170,9 @@ 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 codexAppServerService = new CodexAppServerService();
|
||||
const codexModelCacheService = new CodexModelCacheService(DATA_DIR, codexAppServerService);
|
||||
const codexUsageService = new CodexUsageService(codexAppServerService);
|
||||
const mcpTestService = new MCPTestService(settingsService);
|
||||
const ideationService = new IdeationService(events, settingsService, featureLoader);
|
||||
|
||||
@@ -176,6 +180,11 @@ const ideationService = new IdeationService(events, settingsService, featureLoad
|
||||
(async () => {
|
||||
await agentService.initialize();
|
||||
logger.info('Agent service initialized');
|
||||
|
||||
// Bootstrap Codex model cache in background (don't block server startup)
|
||||
void codexModelCacheService.getModels().catch((err) => {
|
||||
logger.error('Failed to bootstrap Codex model cache:', err);
|
||||
});
|
||||
})();
|
||||
|
||||
// Run stale validation cleanup every hour to prevent memory leaks from crashed validations
|
||||
@@ -219,7 +228,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/codex', createCodexRoutes(codexUsageService, codexModelCacheService));
|
||||
app.use('/api/github', createGitHubRoutes(events, settingsService));
|
||||
app.use('/api/context', createContextRoutes(settingsService));
|
||||
app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService));
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
* Never assumes authenticated - only returns true if CLI confirms.
|
||||
*/
|
||||
|
||||
import { spawnProcess, getCodexAuthPath } from '@automaker/platform';
|
||||
import { spawnProcess } from '@automaker/platform';
|
||||
import { findCodexCliPath } from '@automaker/platform';
|
||||
import * as fs from 'fs';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
|
||||
const logger = createLogger('CodexAuth');
|
||||
|
||||
const CODEX_COMMAND = 'codex';
|
||||
const OPENAI_API_KEY_ENV = 'OPENAI_API_KEY';
|
||||
@@ -26,36 +28,16 @@ export interface CodexAuthCheckResult {
|
||||
export async function checkCodexAuthentication(
|
||||
cliPath?: string | null
|
||||
): Promise<CodexAuthCheckResult> {
|
||||
console.log('[CodexAuth] checkCodexAuthentication called with cliPath:', cliPath);
|
||||
|
||||
const resolvedCliPath = cliPath || (await findCodexCliPath());
|
||||
const hasApiKey = !!process.env[OPENAI_API_KEY_ENV];
|
||||
|
||||
console.log('[CodexAuth] resolvedCliPath:', resolvedCliPath);
|
||||
console.log('[CodexAuth] hasApiKey:', hasApiKey);
|
||||
|
||||
// Debug: Check auth file
|
||||
const authFilePath = getCodexAuthPath();
|
||||
console.log('[CodexAuth] Auth file path:', authFilePath);
|
||||
try {
|
||||
const authFileExists = fs.existsSync(authFilePath);
|
||||
console.log('[CodexAuth] Auth file exists:', authFileExists);
|
||||
if (authFileExists) {
|
||||
const authContent = fs.readFileSync(authFilePath, 'utf-8');
|
||||
console.log('[CodexAuth] Auth file content:', authContent.substring(0, 500)); // First 500 chars
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[CodexAuth] Error reading auth file:', error);
|
||||
}
|
||||
|
||||
// If CLI is not installed, cannot be authenticated
|
||||
if (!resolvedCliPath) {
|
||||
console.log('[CodexAuth] No CLI path found, returning not authenticated');
|
||||
logger.info('CLI not found');
|
||||
return { authenticated: false, method: 'none' };
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[CodexAuth] Running: ' + resolvedCliPath + ' login status');
|
||||
const result = await spawnProcess({
|
||||
command: resolvedCliPath || CODEX_COMMAND,
|
||||
args: ['login', 'status'],
|
||||
@@ -66,33 +48,21 @@ export async function checkCodexAuthentication(
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[CodexAuth] Command result:');
|
||||
console.log('[CodexAuth] exitCode:', result.exitCode);
|
||||
console.log('[CodexAuth] stdout:', JSON.stringify(result.stdout));
|
||||
console.log('[CodexAuth] stderr:', JSON.stringify(result.stderr));
|
||||
|
||||
// Check both stdout and stderr for "logged in" - Codex CLI outputs to stderr
|
||||
const combinedOutput = (result.stdout + result.stderr).toLowerCase();
|
||||
const isLoggedIn = combinedOutput.includes('logged in');
|
||||
console.log('[CodexAuth] isLoggedIn (contains "logged in" in stdout or stderr):', isLoggedIn);
|
||||
|
||||
if (result.exitCode === 0 && isLoggedIn) {
|
||||
// Determine auth method based on what we know
|
||||
const method = hasApiKey ? 'api_key_env' : 'cli_authenticated';
|
||||
console.log('[CodexAuth] Authenticated! method:', method);
|
||||
logger.info(`✓ Authenticated (${method})`);
|
||||
return { authenticated: true, method };
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[CodexAuth] Not authenticated. exitCode:',
|
||||
result.exitCode,
|
||||
'isLoggedIn:',
|
||||
isLoggedIn
|
||||
);
|
||||
logger.info('Not authenticated');
|
||||
return { authenticated: false, method: 'none' };
|
||||
} catch (error) {
|
||||
console.log('[CodexAuth] Error running command:', error);
|
||||
logger.error('Failed to check authentication:', error);
|
||||
return { authenticated: false, method: 'none' };
|
||||
}
|
||||
|
||||
console.log('[CodexAuth] Returning not authenticated');
|
||||
return { authenticated: false, method: 'none' };
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
extractTextFromContent,
|
||||
classifyError,
|
||||
getUserFriendlyErrorMessage,
|
||||
createLogger,
|
||||
} from '@automaker/utils';
|
||||
import type {
|
||||
ExecuteOptions,
|
||||
@@ -658,6 +659,8 @@ async function loadCodexInstructions(cwd: string, enabled: boolean): Promise<str
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
const logger = createLogger('CodexProvider');
|
||||
|
||||
export class CodexProvider extends BaseProvider {
|
||||
getName(): string {
|
||||
return 'codex';
|
||||
@@ -967,21 +970,11 @@ export class CodexProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
async detectInstallation(): Promise<InstallationStatus> {
|
||||
console.log('[CodexProvider.detectInstallation] Starting...');
|
||||
|
||||
const cliPath = await findCodexCliPath();
|
||||
const hasApiKey = !!process.env[OPENAI_API_KEY_ENV];
|
||||
const authIndicators = await getCodexAuthIndicators();
|
||||
const installed = !!cliPath;
|
||||
|
||||
console.log('[CodexProvider.detectInstallation] cliPath:', cliPath);
|
||||
console.log('[CodexProvider.detectInstallation] hasApiKey:', hasApiKey);
|
||||
console.log(
|
||||
'[CodexProvider.detectInstallation] authIndicators:',
|
||||
JSON.stringify(authIndicators)
|
||||
);
|
||||
console.log('[CodexProvider.detectInstallation] installed:', installed);
|
||||
|
||||
let version = '';
|
||||
if (installed) {
|
||||
try {
|
||||
@@ -991,20 +984,16 @@ export class CodexProvider extends BaseProvider {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
version = result.stdout.trim();
|
||||
console.log('[CodexProvider.detectInstallation] version:', version);
|
||||
} catch (error) {
|
||||
console.log('[CodexProvider.detectInstallation] Error getting version:', error);
|
||||
version = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Determine auth status - always verify with CLI, never assume authenticated
|
||||
console.log('[CodexProvider.detectInstallation] Calling checkCodexAuthentication...');
|
||||
const authCheck = await checkCodexAuthentication(cliPath);
|
||||
console.log('[CodexProvider.detectInstallation] authCheck result:', JSON.stringify(authCheck));
|
||||
const authenticated = authCheck.authenticated;
|
||||
|
||||
const result = {
|
||||
return {
|
||||
installed,
|
||||
path: cliPath || undefined,
|
||||
version: version || undefined,
|
||||
@@ -1012,8 +1001,6 @@ export class CodexProvider extends BaseProvider {
|
||||
hasApiKey,
|
||||
authenticated,
|
||||
};
|
||||
console.log('[CodexProvider.detectInstallation] Final result:', JSON.stringify(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
getAvailableModels(): ModelDefinition[] {
|
||||
@@ -1025,36 +1012,24 @@ export class CodexProvider extends BaseProvider {
|
||||
* Check authentication status for Codex CLI
|
||||
*/
|
||||
async checkAuth(): Promise<CodexAuthStatus> {
|
||||
console.log('[CodexProvider.checkAuth] Starting auth check...');
|
||||
|
||||
const cliPath = await findCodexCliPath();
|
||||
const hasApiKey = !!process.env[OPENAI_API_KEY_ENV];
|
||||
const authIndicators = await getCodexAuthIndicators();
|
||||
|
||||
console.log('[CodexProvider.checkAuth] cliPath:', cliPath);
|
||||
console.log('[CodexProvider.checkAuth] hasApiKey:', hasApiKey);
|
||||
console.log('[CodexProvider.checkAuth] authIndicators:', JSON.stringify(authIndicators));
|
||||
|
||||
// Check for API key in environment
|
||||
if (hasApiKey) {
|
||||
console.log('[CodexProvider.checkAuth] Has API key, returning authenticated');
|
||||
return { authenticated: true, method: 'api_key' };
|
||||
}
|
||||
|
||||
// Check for OAuth/token from Codex CLI
|
||||
if (authIndicators.hasOAuthToken || authIndicators.hasApiKey) {
|
||||
console.log(
|
||||
'[CodexProvider.checkAuth] Has OAuth token or API key in auth file, returning authenticated'
|
||||
);
|
||||
return { authenticated: true, method: 'oauth' };
|
||||
}
|
||||
|
||||
// CLI is installed but not authenticated via indicators - try CLI command
|
||||
console.log('[CodexProvider.checkAuth] No indicators found, trying CLI command...');
|
||||
if (cliPath) {
|
||||
try {
|
||||
// Try 'codex login status' first (same as checkCodexAuthentication)
|
||||
console.log('[CodexProvider.checkAuth] Running: ' + cliPath + ' login status');
|
||||
const result = await spawnProcess({
|
||||
command: cliPath || CODEX_COMMAND,
|
||||
args: ['login', 'status'],
|
||||
@@ -1064,26 +1039,19 @@ export class CodexProvider extends BaseProvider {
|
||||
TERM: 'dumb',
|
||||
},
|
||||
});
|
||||
console.log('[CodexProvider.checkAuth] login status result:');
|
||||
console.log('[CodexProvider.checkAuth] exitCode:', result.exitCode);
|
||||
console.log('[CodexProvider.checkAuth] stdout:', JSON.stringify(result.stdout));
|
||||
console.log('[CodexProvider.checkAuth] stderr:', JSON.stringify(result.stderr));
|
||||
|
||||
// Check both stdout and stderr - Codex CLI outputs to stderr
|
||||
const combinedOutput = (result.stdout + result.stderr).toLowerCase();
|
||||
const isLoggedIn = combinedOutput.includes('logged in');
|
||||
console.log('[CodexProvider.checkAuth] isLoggedIn:', isLoggedIn);
|
||||
|
||||
if (result.exitCode === 0 && isLoggedIn) {
|
||||
console.log('[CodexProvider.checkAuth] CLI says logged in, returning authenticated');
|
||||
return { authenticated: true, method: 'oauth' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[CodexProvider.checkAuth] Error running login status:', error);
|
||||
logger.warn('Error running login status command during auth check:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[CodexProvider.checkAuth] Not authenticated');
|
||||
return { authenticated: false, method: 'none' };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { CodexUsageService } from '../../services/codex-usage-service.js';
|
||||
import { CodexModelCacheService } from '../../services/codex-model-cache-service.js';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
|
||||
const logger = createLogger('Codex');
|
||||
|
||||
export function createCodexRoutes(service: CodexUsageService): Router {
|
||||
export function createCodexRoutes(
|
||||
usageService: CodexUsageService,
|
||||
modelCacheService: CodexModelCacheService
|
||||
): Router {
|
||||
const router = Router();
|
||||
|
||||
// Get current usage (attempts to fetch from Codex CLI)
|
||||
router.get('/usage', async (req: Request, res: Response) => {
|
||||
router.get('/usage', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
// Check if Codex CLI is available first
|
||||
const isAvailable = await service.isAvailable();
|
||||
const isAvailable = await usageService.isAvailable();
|
||||
if (!isAvailable) {
|
||||
// IMPORTANT: This endpoint is behind Automaker session auth already.
|
||||
// Use a 200 + error payload for Codex CLI issues so the UI doesn't
|
||||
@@ -23,7 +27,7 @@ export function createCodexRoutes(service: CodexUsageService): Router {
|
||||
return;
|
||||
}
|
||||
|
||||
const usage = await service.fetchUsageData();
|
||||
const usage = await usageService.fetchUsageData();
|
||||
res.json(usage);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
@@ -52,5 +56,35 @@ export function createCodexRoutes(service: CodexUsageService): Router {
|
||||
}
|
||||
});
|
||||
|
||||
// Get available Codex models (cached)
|
||||
router.get('/models', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const forceRefresh = req.query.refresh === 'true';
|
||||
const { models, cachedAt } = await modelCacheService.getModelsWithMetadata(forceRefresh);
|
||||
|
||||
if (models.length === 0) {
|
||||
res.status(503).json({
|
||||
success: false,
|
||||
error: 'Codex CLI not available or not authenticated',
|
||||
message: "Please install Codex CLI and run 'codex login' to authenticate",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
models,
|
||||
cachedAt,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error fetching models:', error);
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
212
apps/server/src/services/codex-app-server-service.ts
Normal file
212
apps/server/src/services/codex-app-server-service.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
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');
|
||||
|
||||
/**
|
||||
* CodexAppServerService
|
||||
*
|
||||
* Centralized service for communicating with Codex CLI's app-server via JSON-RPC protocol.
|
||||
* Handles process spawning, JSON-RPC messaging, and cleanup.
|
||||
*
|
||||
* Connection strategy: Spawn on-demand (new process for each method call)
|
||||
*/
|
||||
export class CodexAppServerService {
|
||||
private cachedCliPath: string | null = null;
|
||||
|
||||
/**
|
||||
* Check if Codex CLI is available on the system
|
||||
*/
|
||||
async isAvailable(): Promise<boolean> {
|
||||
this.cachedCliPath = await findCodexCliPath();
|
||||
return Boolean(this.cachedCliPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch available models from app-server
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch account information from app-server
|
||||
*/
|
||||
async getAccount(): Promise<AppServerAccountResponse | null> {
|
||||
return this.executeJsonRpc<AppServerAccountResponse>((sendRequest) => {
|
||||
return sendRequest('account/read', { refreshToken: false });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch rate limits from app-server
|
||||
*/
|
||||
async getRateLimits(): Promise<AppServerRateLimitsResponse | null> {
|
||||
return this.executeJsonRpc<AppServerRateLimitsResponse>((sendRequest) => {
|
||||
return sendRequest('account/rateLimits/read', {});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute JSON-RPC requests via Codex app-server
|
||||
*
|
||||
* This method:
|
||||
* 1. Spawns a new `codex app-server` process
|
||||
* 2. Handles JSON-RPC initialization handshake
|
||||
* 3. Executes user-provided requests
|
||||
* 4. Cleans up the process
|
||||
*
|
||||
* @param requestFn - Function that receives sendRequest helper and returns a promise
|
||||
* @returns Result of the JSON-RPC request or null on failure
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// On Windows, .cmd files must be run through shell
|
||||
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');
|
||||
}
|
||||
|
||||
// Setup readline for reading JSONL responses
|
||||
const rl = readline.createInterface({
|
||||
input: childProcess.stdout,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
// Message ID counter for JSON-RPC
|
||||
let messageId = 0;
|
||||
const pendingRequests = new Map<
|
||||
number,
|
||||
{
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (error: Error) => void;
|
||||
timeout: NodeJS.Timeout;
|
||||
}
|
||||
>();
|
||||
|
||||
// Process incoming messages
|
||||
rl.on('line', (line) => {
|
||||
if (!line.trim()) return;
|
||||
|
||||
try {
|
||||
const message = JSON.parse(line);
|
||||
|
||||
// Handle response to our request
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ignore notifications (no id field)
|
||||
} catch {
|
||||
// Ignore parse errors for non-JSON lines
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to send JSON-RPC request and wait for response
|
||||
const sendRequest = <R>(method: string, params?: unknown): Promise<R> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = ++messageId;
|
||||
const request: JsonRpcRequest = {
|
||||
method,
|
||||
id,
|
||||
params: params ?? {},
|
||||
};
|
||||
|
||||
// Set timeout for request (10 seconds)
|
||||
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');
|
||||
});
|
||||
};
|
||||
|
||||
// Helper to send notification (no response expected)
|
||||
const sendNotification = (method: string, params?: unknown): void => {
|
||||
const notification = params ? { method, params } : { method };
|
||||
childProcess!.stdin!.write(JSON.stringify(notification) + '\n');
|
||||
};
|
||||
|
||||
// 1. Initialize the app-server
|
||||
await sendRequest('initialize', {
|
||||
clientInfo: {
|
||||
name: 'automaker',
|
||||
title: 'AutoMaker',
|
||||
version: '1.0.0',
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Send initialized notification
|
||||
sendNotification('initialized');
|
||||
|
||||
// 3. Execute user-provided requests
|
||||
const result = await requestFn(sendRequest);
|
||||
|
||||
// Clean up
|
||||
rl.close();
|
||||
childProcess.kill('SIGTERM');
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[executeJsonRpc] Failed:', error);
|
||||
return null;
|
||||
} finally {
|
||||
// Ensure process is killed
|
||||
if (childProcess && !childProcess.killed) {
|
||||
childProcess.kill('SIGTERM');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
258
apps/server/src/services/codex-model-cache-service.ts
Normal file
258
apps/server/src/services/codex-model-cache-service.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import path from 'path';
|
||||
import { secureFs } from '@automaker/platform';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import type { AppServerModel } from '@automaker/types';
|
||||
import type { CodexAppServerService } from './codex-app-server-service.js';
|
||||
|
||||
const logger = createLogger('CodexModelCache');
|
||||
|
||||
/**
|
||||
* Codex model with UI-compatible format
|
||||
*/
|
||||
export interface CodexModel {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
hasThinking: boolean;
|
||||
supportsVision: boolean;
|
||||
tier: 'premium' | 'standard' | 'basic';
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache structure stored on disk
|
||||
*/
|
||||
interface CodexModelCache {
|
||||
models: CodexModel[];
|
||||
cachedAt: number;
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodexModelCacheService
|
||||
*
|
||||
* Caches Codex models fetched from app-server with TTL-based invalidation and disk persistence.
|
||||
*
|
||||
* Features:
|
||||
* - 1-hour TTL (configurable)
|
||||
* - Atomic file writes (temp file + rename)
|
||||
* - Thread-safe (deduplicates concurrent refresh requests)
|
||||
* - Auto-bootstrap on service creation
|
||||
* - Graceful fallback (returns empty array on errors)
|
||||
*/
|
||||
export class CodexModelCacheService {
|
||||
private cacheFilePath: string;
|
||||
private ttl: number;
|
||||
private appServerService: CodexAppServerService;
|
||||
private inFlightRefresh: Promise<CodexModel[]> | null = null;
|
||||
|
||||
constructor(
|
||||
dataDir: string,
|
||||
appServerService: CodexAppServerService,
|
||||
ttl: number = 3600000 // 1 hour default
|
||||
) {
|
||||
this.cacheFilePath = path.join(dataDir, 'codex-models-cache.json');
|
||||
this.ttl = ttl;
|
||||
this.appServerService = appServerService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get models from cache or fetch if stale
|
||||
*
|
||||
* @param forceRefresh - If true, bypass cache and fetch fresh data
|
||||
* @returns Array of Codex models (empty array if unavailable)
|
||||
*/
|
||||
async getModels(forceRefresh = false): Promise<CodexModel[]> {
|
||||
// If force refresh, skip cache
|
||||
if (forceRefresh) {
|
||||
return this.refreshModels();
|
||||
}
|
||||
|
||||
// Try to load from cache
|
||||
const cached = await this.loadFromCache();
|
||||
if (cached) {
|
||||
const age = Date.now() - cached.cachedAt;
|
||||
const isStale = age > cached.ttl;
|
||||
|
||||
if (!isStale) {
|
||||
logger.info(
|
||||
`[getModels] ✓ Using cached models (${cached.models.length} models, age: ${Math.round(age / 60000)}min)`
|
||||
);
|
||||
return cached.models;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache is stale or missing, refresh
|
||||
return this.refreshModels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get models with cache metadata
|
||||
*
|
||||
* @param forceRefresh - If true, bypass cache and fetch fresh data
|
||||
* @returns Object containing models and cache timestamp
|
||||
*/
|
||||
async getModelsWithMetadata(
|
||||
forceRefresh = false
|
||||
): Promise<{ models: CodexModel[]; cachedAt: number }> {
|
||||
const models = await this.getModels(forceRefresh);
|
||||
|
||||
// Try to get the actual cache timestamp
|
||||
const cached = await this.loadFromCache();
|
||||
const cachedAt = cached?.cachedAt ?? Date.now();
|
||||
|
||||
return { models, cachedAt };
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh models from app-server and update cache
|
||||
*
|
||||
* Thread-safe: Deduplicates concurrent refresh requests
|
||||
*/
|
||||
async refreshModels(): Promise<CodexModel[]> {
|
||||
// Deduplicate concurrent refresh requests
|
||||
if (this.inFlightRefresh) {
|
||||
return this.inFlightRefresh;
|
||||
}
|
||||
|
||||
// Start new refresh
|
||||
this.inFlightRefresh = this.doRefresh();
|
||||
|
||||
try {
|
||||
const models = await this.inFlightRefresh;
|
||||
return models;
|
||||
} finally {
|
||||
this.inFlightRefresh = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache file
|
||||
*/
|
||||
async clearCache(): Promise<void> {
|
||||
logger.info('[clearCache] Clearing cache...');
|
||||
|
||||
try {
|
||||
await secureFs.unlink(this.cacheFilePath);
|
||||
logger.info('[clearCache] Cache cleared');
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
logger.error('[clearCache] Failed to clear cache:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to perform the actual refresh
|
||||
*/
|
||||
private async doRefresh(): Promise<CodexModel[]> {
|
||||
try {
|
||||
// Check if app-server is available
|
||||
const isAvailable = await this.appServerService.isAvailable();
|
||||
if (!isAvailable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Fetch models from app-server
|
||||
const response = await this.appServerService.getModels();
|
||||
if (!response || !response.data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Transform models to UI format
|
||||
const models = response.data.map((model) => this.transformModel(model));
|
||||
|
||||
// Save to cache
|
||||
await this.saveToCache(models);
|
||||
|
||||
logger.info(`[refreshModels] ✓ Fetched fresh models (${models.length} models)`);
|
||||
|
||||
return models;
|
||||
} catch (error) {
|
||||
logger.error('[doRefresh] Refresh failed:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform app-server model to UI-compatible format
|
||||
*/
|
||||
private transformModel(appServerModel: AppServerModel): CodexModel {
|
||||
return {
|
||||
id: `codex-${appServerModel.id}`, // Add 'codex-' prefix for compatibility
|
||||
label: appServerModel.displayName,
|
||||
description: appServerModel.description,
|
||||
hasThinking: appServerModel.supportedReasoningEfforts.length > 0,
|
||||
supportsVision: true, // All Codex models support vision
|
||||
tier: this.inferTier(appServerModel.id),
|
||||
isDefault: appServerModel.isDefault,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer tier from model ID
|
||||
*/
|
||||
private inferTier(modelId: string): 'premium' | 'standard' | 'basic' {
|
||||
if (modelId.includes('max') || modelId.includes('gpt-5.2-codex')) {
|
||||
return 'premium';
|
||||
}
|
||||
if (modelId.includes('mini')) {
|
||||
return 'basic';
|
||||
}
|
||||
return 'standard';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cache from disk
|
||||
*/
|
||||
private async loadFromCache(): Promise<CodexModelCache | null> {
|
||||
try {
|
||||
const content = await secureFs.readFile(this.cacheFilePath, 'utf-8');
|
||||
const cache = JSON.parse(content.toString()) as CodexModelCache;
|
||||
|
||||
// Validate cache structure
|
||||
if (!Array.isArray(cache.models) || typeof cache.cachedAt !== 'number') {
|
||||
logger.warn('[loadFromCache] Invalid cache structure, ignoring');
|
||||
return null;
|
||||
}
|
||||
|
||||
return cache;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
logger.warn('[loadFromCache] Failed to read cache:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save cache to disk (atomic write)
|
||||
*/
|
||||
private async saveToCache(models: CodexModel[]): Promise<void> {
|
||||
const cache: CodexModelCache = {
|
||||
models,
|
||||
cachedAt: Date.now(),
|
||||
ttl: this.ttl,
|
||||
};
|
||||
|
||||
const tempPath = `${this.cacheFilePath}.tmp.${Date.now()}`;
|
||||
|
||||
try {
|
||||
// Write to temp file
|
||||
const content = JSON.stringify(cache, null, 2);
|
||||
await secureFs.writeFile(tempPath, content, 'utf-8');
|
||||
|
||||
// Atomic rename
|
||||
await secureFs.rename(tempPath, this.cacheFilePath);
|
||||
} catch (error) {
|
||||
logger.error('[saveToCache] Failed to save cache:', error);
|
||||
|
||||
// Clean up temp file
|
||||
try {
|
||||
await secureFs.unlink(tempPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
findCodexCliPath,
|
||||
spawnProcess,
|
||||
getCodexAuthPath,
|
||||
systemPathExists,
|
||||
systemPathReadFile,
|
||||
} from '@automaker/platform';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import type { CodexAppServerService } from './codex-app-server-service.js';
|
||||
|
||||
const logger = createLogger('CodexUsage');
|
||||
|
||||
@@ -18,19 +18,12 @@ export interface CodexRateLimitWindow {
|
||||
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;
|
||||
@@ -39,13 +32,24 @@ export interface CodexUsageData {
|
||||
/**
|
||||
* Codex Usage Service
|
||||
*
|
||||
* Attempts to fetch usage data from Codex CLI and OpenAI API.
|
||||
* Codex CLI doesn't provide a direct usage command, but we can:
|
||||
* 1. Parse usage info from error responses (rate limit errors contain plan info)
|
||||
* 2. Check for OpenAI API usage if API key is available
|
||||
* Fetches usage data from Codex CLI using the app-server JSON-RPC API.
|
||||
* Falls back to auth file parsing if app-server is unavailable.
|
||||
*/
|
||||
export class CodexUsageService {
|
||||
private cachedCliPath: string | null = null;
|
||||
private appServerService: CodexAppServerService | null = null;
|
||||
private accountPlanTypeArray: CodexPlanType[] = [
|
||||
'free',
|
||||
'plus',
|
||||
'pro',
|
||||
'team',
|
||||
'enterprise',
|
||||
'edu',
|
||||
];
|
||||
|
||||
constructor(appServerService?: CodexAppServerService) {
|
||||
this.appServerService = appServerService || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Codex CLI is available on the system
|
||||
@@ -58,60 +62,131 @@ export class CodexUsageService {
|
||||
/**
|
||||
* Attempt to fetch usage data
|
||||
*
|
||||
* Tries multiple approaches:
|
||||
* 1. Always try to get plan type from auth file first (authoritative source)
|
||||
* 2. Check for OpenAI API key in environment for API usage
|
||||
* 3. Make a test request to capture rate limit headers from CLI
|
||||
* 4. Combine results from auth file and CLI
|
||||
* Priority order:
|
||||
* 1. Codex app-server JSON-RPC API (most reliable, provides real-time data)
|
||||
* 2. Auth file JWT parsing (fallback for plan type)
|
||||
*/
|
||||
async fetchUsageData(): Promise<CodexUsageData> {
|
||||
logger.info('[fetchUsageData] Starting...');
|
||||
const cliPath = this.cachedCliPath || (await findCodexCliPath());
|
||||
|
||||
if (!cliPath) {
|
||||
logger.error('[fetchUsageData] Codex CLI not found');
|
||||
throw new Error('Codex CLI not found. Please install it with: npm install -g @openai/codex');
|
||||
}
|
||||
|
||||
// Always try to get plan type from auth file first - this is the authoritative source
|
||||
const authPlanType = await this.getPlanTypeFromAuthFile();
|
||||
logger.info(`[fetchUsageData] Using CLI path: ${cliPath}`);
|
||||
|
||||
// Check if user has an API key that we can use
|
||||
const hasApiKey = !!process.env.OPENAI_API_KEY;
|
||||
|
||||
if (hasApiKey) {
|
||||
// Try to get usage from OpenAI API
|
||||
const openaiUsage = await this.fetchOpenAIUsage();
|
||||
if (openaiUsage) {
|
||||
// Merge with auth file plan type if available
|
||||
if (authPlanType && openaiUsage.rateLimits) {
|
||||
openaiUsage.rateLimits.planType = authPlanType;
|
||||
}
|
||||
return openaiUsage;
|
||||
}
|
||||
// Try to get usage from Codex app-server (most reliable method)
|
||||
const appServerUsage = await this.fetchFromAppServer();
|
||||
if (appServerUsage) {
|
||||
logger.info('[fetchUsageData] ✓ Fetched usage from app-server');
|
||||
return appServerUsage;
|
||||
}
|
||||
|
||||
// Try to get usage from Codex CLI by making a simple request
|
||||
const codexUsage = await this.fetchCodexUsage(cliPath, authPlanType);
|
||||
if (codexUsage) {
|
||||
return codexUsage;
|
||||
}
|
||||
logger.info('[fetchUsageData] App-server failed, trying auth file fallback...');
|
||||
|
||||
// Fallback: try to parse full usage from auth file
|
||||
// Fallback: try to parse usage from auth file
|
||||
const authUsage = await this.fetchFromAuthFile();
|
||||
if (authUsage) {
|
||||
logger.info('[fetchUsageData] ✓ Fetched usage from auth file');
|
||||
return authUsage;
|
||||
}
|
||||
|
||||
// If all else fails, return a message with helpful information
|
||||
throw new Error(
|
||||
'Codex usage statistics require additional configuration. ' +
|
||||
'To enable usage tracking:\n\n' +
|
||||
'1. Set your OpenAI API key in the environment:\n' +
|
||||
' export OPENAI_API_KEY=sk-...\n\n' +
|
||||
'2. Or check your usage at:\n' +
|
||||
' https://platform.openai.com/usage\n\n' +
|
||||
'Note: If using Codex CLI with ChatGPT OAuth authentication, ' +
|
||||
'usage data must be queried through your OpenAI account.'
|
||||
);
|
||||
logger.info('[fetchUsageData] All methods failed, returning unknown');
|
||||
|
||||
// If all else fails, return unknown
|
||||
return {
|
||||
rateLimits: {
|
||||
planType: 'unknown',
|
||||
},
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch usage data from Codex app-server using JSON-RPC API
|
||||
* This is the most reliable method as it gets real-time data from OpenAI
|
||||
*/
|
||||
private async fetchFromAppServer(): Promise<CodexUsageData | null> {
|
||||
try {
|
||||
// Use CodexAppServerService if available
|
||||
if (!this.appServerService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetch account and rate limits in parallel
|
||||
const [accountResult, rateLimitsResult] = await Promise.all([
|
||||
this.appServerService.getAccount(),
|
||||
this.appServerService.getRateLimits(),
|
||||
]);
|
||||
|
||||
if (!accountResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build response
|
||||
// Prefer planType from rateLimits (more accurate/current) over account (can be stale)
|
||||
let planType: CodexPlanType = 'unknown';
|
||||
|
||||
// First try rate limits planType (most accurate)
|
||||
const rateLimitsPlanType = rateLimitsResult?.rateLimits?.planType;
|
||||
if (rateLimitsPlanType) {
|
||||
const normalizedType = rateLimitsPlanType.toLowerCase() as CodexPlanType;
|
||||
if (this.accountPlanTypeArray.includes(normalizedType)) {
|
||||
planType = normalizedType;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to account planType if rate limits didn't have it
|
||||
if (planType === 'unknown' && accountResult.account?.planType) {
|
||||
const normalizedType = accountResult.account.planType.toLowerCase() as CodexPlanType;
|
||||
if (this.accountPlanTypeArray.includes(normalizedType)) {
|
||||
planType = normalizedType;
|
||||
}
|
||||
}
|
||||
|
||||
const result: CodexUsageData = {
|
||||
rateLimits: {
|
||||
planType,
|
||||
},
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Add rate limit info if available
|
||||
if (rateLimitsResult?.rateLimits?.primary) {
|
||||
const primary = rateLimitsResult.rateLimits.primary;
|
||||
result.rateLimits!.primary = {
|
||||
limit: -1, // Not provided by API
|
||||
used: -1, // Not provided by API
|
||||
remaining: -1, // Not provided by API
|
||||
usedPercent: primary.usedPercent,
|
||||
windowDurationMins: primary.windowDurationMins,
|
||||
resetsAt: primary.resetsAt,
|
||||
};
|
||||
}
|
||||
|
||||
// Add secondary rate limit if available
|
||||
if (rateLimitsResult?.rateLimits?.secondary) {
|
||||
const secondary = rateLimitsResult.rateLimits.secondary;
|
||||
result.rateLimits!.secondary = {
|
||||
limit: -1, // Not provided by API
|
||||
used: -1, // Not provided by API
|
||||
remaining: -1, // Not provided by API
|
||||
usedPercent: secondary.usedPercent,
|
||||
windowDurationMins: secondary.windowDurationMins,
|
||||
resetsAt: secondary.resetsAt,
|
||||
};
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[fetchFromAppServer] ✓ Plan: ${planType}, Primary: ${result.rateLimits?.primary?.usedPercent || 'N/A'}%, Secondary: ${result.rateLimits?.secondary?.usedPercent || 'N/A'}%`
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[fetchFromAppServer] Failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,9 +196,11 @@ export class CodexUsageService {
|
||||
private async getPlanTypeFromAuthFile(): Promise<CodexPlanType> {
|
||||
try {
|
||||
const authFilePath = getCodexAuthPath();
|
||||
const exists = await systemPathExists(authFilePath);
|
||||
logger.info(`[getPlanTypeFromAuthFile] Auth file path: ${authFilePath}`);
|
||||
const exists = systemPathExists(authFilePath);
|
||||
|
||||
if (!exists) {
|
||||
logger.warn('[getPlanTypeFromAuthFile] Auth file does not exist');
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
@@ -131,16 +208,24 @@ export class CodexUsageService {
|
||||
const authData = JSON.parse(authContent);
|
||||
|
||||
if (!authData.tokens?.id_token) {
|
||||
logger.info('[getPlanTypeFromAuthFile] No id_token in auth file');
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const claims = this.parseJwt(authData.tokens.id_token);
|
||||
if (!claims) {
|
||||
logger.info('[getPlanTypeFromAuthFile] Failed to parse JWT');
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
logger.info('[getPlanTypeFromAuthFile] JWT claims keys:', Object.keys(claims));
|
||||
|
||||
// Extract plan type from nested OpenAI auth object with type validation
|
||||
const openaiAuthClaim = claims['https://api.openai.com/auth'];
|
||||
logger.info(
|
||||
'[getPlanTypeFromAuthFile] OpenAI auth claim:',
|
||||
JSON.stringify(openaiAuthClaim, null, 2)
|
||||
);
|
||||
|
||||
let accountType: string | undefined;
|
||||
let isSubscriptionExpired = false;
|
||||
@@ -188,154 +273,23 @@ export class CodexUsageService {
|
||||
}
|
||||
|
||||
if (accountType) {
|
||||
const normalizedType = accountType.toLowerCase();
|
||||
if (['free', 'plus', 'pro', 'team', 'enterprise', 'edu'].includes(normalizedType)) {
|
||||
return normalizedType as CodexPlanType;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to get plan type from auth file:', error);
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to fetch usage from OpenAI API using the API key
|
||||
*/
|
||||
private async fetchOpenAIUsage(): Promise<CodexUsageData | null> {
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const endTime = Math.floor(Date.now() / 1000);
|
||||
const startTime = endTime - 7 * 24 * 60 * 60; // Last 7 days
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.openai.com/v1/organization/usage/completions?start_time=${startTime}&end_time=${endTime}&limit=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return this.parseOpenAIUsage(data);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch from OpenAI API:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse OpenAI usage API response
|
||||
*/
|
||||
private parseOpenAIUsage(data: any): CodexUsageData {
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
if (data.data && Array.isArray(data.data)) {
|
||||
for (const bucket of data.data) {
|
||||
if (bucket.results && Array.isArray(bucket.results)) {
|
||||
for (const result of bucket.results) {
|
||||
totalInputTokens += result.input_tokens || 0;
|
||||
totalOutputTokens += result.output_tokens || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rateLimits: {
|
||||
planType: 'unknown',
|
||||
credits: {
|
||||
hasCredits: true,
|
||||
},
|
||||
},
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to fetch usage by making a test request to Codex CLI
|
||||
* and parsing rate limit information from the response
|
||||
*/
|
||||
private async fetchCodexUsage(
|
||||
cliPath: string,
|
||||
authPlanType: CodexPlanType
|
||||
): Promise<CodexUsageData | null> {
|
||||
try {
|
||||
// Make a simple request to trigger rate limit info if at limit
|
||||
const result = await spawnProcess({
|
||||
command: cliPath,
|
||||
args: ['exec', '--', 'echo', 'test'],
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
TERM: 'dumb',
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Parse the output for rate limit information
|
||||
const combinedOutput = (result.stdout + result.stderr).toLowerCase();
|
||||
|
||||
// Check if we got a rate limit error
|
||||
const rateLimitMatch = combinedOutput.match(
|
||||
/usage_limit_reached.*?"plan_type":"([^"]+)".*?"resets_at":(\d+).*?"resets_in_seconds":(\d+)/
|
||||
);
|
||||
|
||||
if (rateLimitMatch) {
|
||||
// Rate limit error contains the plan type - use that as it's the most authoritative
|
||||
const planType = rateLimitMatch[1] as CodexPlanType;
|
||||
const resetsAt = parseInt(rateLimitMatch[2], 10);
|
||||
const resetsInSeconds = parseInt(rateLimitMatch[3], 10);
|
||||
|
||||
const normalizedType = accountType.toLowerCase() as CodexPlanType;
|
||||
logger.info(
|
||||
`Rate limit hit - plan: ${planType}, resets in ${Math.ceil(resetsInSeconds / 60)} mins`
|
||||
`[getPlanTypeFromAuthFile] Account type: "${accountType}", normalized: "${normalizedType}"`
|
||||
);
|
||||
|
||||
return {
|
||||
rateLimits: {
|
||||
planType,
|
||||
primary: {
|
||||
limit: 0,
|
||||
used: 0,
|
||||
remaining: 0,
|
||||
usedPercent: 100,
|
||||
windowDurationMins: Math.ceil(resetsInSeconds / 60),
|
||||
resetsAt,
|
||||
},
|
||||
},
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
if (this.accountPlanTypeArray.includes(normalizedType)) {
|
||||
logger.info(`[getPlanTypeFromAuthFile] Returning plan type: ${normalizedType}`);
|
||||
return normalizedType;
|
||||
}
|
||||
} else {
|
||||
logger.info('[getPlanTypeFromAuthFile] No account type found in claims');
|
||||
}
|
||||
|
||||
// No rate limit error - use the plan type from auth file
|
||||
const isFreePlan = authPlanType === 'free';
|
||||
|
||||
return {
|
||||
rateLimits: {
|
||||
planType: authPlanType,
|
||||
credits: {
|
||||
hasCredits: true,
|
||||
unlimited: !isFreePlan && authPlanType !== 'unknown',
|
||||
},
|
||||
},
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch from Codex CLI:', error);
|
||||
logger.error('[getPlanTypeFromAuthFile] Failed to get plan type from auth file:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
logger.info('[getPlanTypeFromAuthFile] Returning unknown');
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -343,27 +297,27 @@ export class CodexUsageService {
|
||||
* Reuses getPlanTypeFromAuthFile to avoid code duplication
|
||||
*/
|
||||
private async fetchFromAuthFile(): Promise<CodexUsageData | null> {
|
||||
logger.info('[fetchFromAuthFile] Starting...');
|
||||
try {
|
||||
const planType = await this.getPlanTypeFromAuthFile();
|
||||
logger.info(`[fetchFromAuthFile] Got plan type: ${planType}`);
|
||||
|
||||
if (planType === 'unknown') {
|
||||
logger.info('[fetchFromAuthFile] Plan type unknown, returning null');
|
||||
return null;
|
||||
}
|
||||
|
||||
const isFreePlan = planType === 'free';
|
||||
|
||||
return {
|
||||
const result: CodexUsageData = {
|
||||
rateLimits: {
|
||||
planType,
|
||||
credits: {
|
||||
hasCredits: true,
|
||||
unlimited: !isFreePlan,
|
||||
},
|
||||
},
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
|
||||
logger.info('[fetchFromAuthFile] Returning result:', JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse auth file:', error);
|
||||
logger.error('[fetchFromAuthFile] Failed to parse auth file:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -372,7 +326,7 @@ export class CodexUsageService {
|
||||
/**
|
||||
* Parse JWT token to extract claims
|
||||
*/
|
||||
private parseJwt(token: string): any {
|
||||
private parseJwt(token: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
|
||||
@@ -383,18 +337,8 @@ export class CodexUsageService {
|
||||
const base64Url = parts[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
||||
// Use Buffer for Node.js environment instead of atob
|
||||
let jsonPayload: string;
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
jsonPayload = Buffer.from(base64, 'base64').toString('utf-8');
|
||||
} else {
|
||||
jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join('')
|
||||
);
|
||||
}
|
||||
// Use Buffer for Node.js environment
|
||||
const jsonPayload = Buffer.from(base64, 'base64').toString('utf-8');
|
||||
|
||||
return JSON.parse(jsonPayload);
|
||||
} catch {
|
||||
|
||||
@@ -9,7 +9,9 @@ import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { getModelProvider, PROVIDER_PREFIXES, stripProviderPrefix } from '@automaker/types';
|
||||
import type { ModelProvider } from '@automaker/types';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, ModelOption } from './model-constants';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS, ModelOption } from './model-constants';
|
||||
import { useEffect } from 'react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
|
||||
interface ModelSelectorProps {
|
||||
selectedModel: string; // Can be ModelAlias or "cursor-{id}"
|
||||
@@ -22,7 +24,14 @@ export function ModelSelector({
|
||||
onModelSelect,
|
||||
testIdPrefix = 'model-select',
|
||||
}: ModelSelectorProps) {
|
||||
const { enabledCursorModels, cursorDefaultModel } = useAppStore();
|
||||
const {
|
||||
enabledCursorModels,
|
||||
cursorDefaultModel,
|
||||
codexModels,
|
||||
codexModelsLoading,
|
||||
codexModelsError,
|
||||
fetchCodexModels,
|
||||
} = useAppStore();
|
||||
const { cursorCliStatus, codexCliStatus } = useSetupStore();
|
||||
|
||||
const selectedProvider = getModelProvider(selectedModel);
|
||||
@@ -33,6 +42,31 @@ export function ModelSelector({
|
||||
// Check if Codex CLI is available
|
||||
const isCodexAvailable = codexCliStatus?.installed && codexCliStatus?.auth?.authenticated;
|
||||
|
||||
// Fetch Codex models on mount
|
||||
useEffect(() => {
|
||||
if (isCodexAvailable && codexModels.length === 0 && !codexModelsLoading) {
|
||||
fetchCodexModels();
|
||||
}
|
||||
}, [isCodexAvailable, codexModels.length, codexModelsLoading, fetchCodexModels]);
|
||||
|
||||
// Transform codex models from store to ModelOption format
|
||||
const dynamicCodexModels: ModelOption[] = codexModels.map((model) => {
|
||||
// Infer badge based on tier
|
||||
let badge: string | undefined;
|
||||
if (model.tier === 'premium') badge = 'Premium';
|
||||
else if (model.tier === 'basic') badge = 'Speed';
|
||||
else if (model.tier === 'standard') badge = 'Balanced';
|
||||
|
||||
return {
|
||||
id: model.id,
|
||||
label: model.label,
|
||||
description: model.description,
|
||||
badge,
|
||||
provider: 'codex' as ModelProvider,
|
||||
hasThinking: model.hasThinking,
|
||||
};
|
||||
});
|
||||
|
||||
// Filter Cursor models based on enabled models from global settings
|
||||
const filteredCursorModels = CURSOR_MODELS.filter((model) => {
|
||||
// Extract the cursor model ID from the prefixed ID (e.g., "cursor-auto" -> "auto")
|
||||
@@ -45,8 +79,10 @@ export function ModelSelector({
|
||||
// Switch to Cursor's default model (from global settings)
|
||||
onModelSelect(`${PROVIDER_PREFIXES.cursor}${cursorDefaultModel}`);
|
||||
} else if (provider === 'codex' && selectedProvider !== 'codex') {
|
||||
// Switch to Codex's default model (codex-gpt-5.2-codex)
|
||||
onModelSelect('codex-gpt-5.2-codex');
|
||||
// Switch to Codex's default model (use isDefault flag from dynamic models)
|
||||
const defaultModel = codexModels.find((m) => m.isDefault);
|
||||
const defaultModelId = defaultModel?.id || codexModels[0]?.id || 'codex-gpt-5.2-codex';
|
||||
onModelSelect(defaultModelId);
|
||||
} else if (provider === 'claude' && selectedProvider !== 'claude') {
|
||||
// Switch to Claude's default model
|
||||
onModelSelect('sonnet');
|
||||
@@ -234,41 +270,91 @@ export function ModelSelector({
|
||||
CLI
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{CODEX_MODELS.map((option) => {
|
||||
const isSelected = selectedModel === option.id;
|
||||
return (
|
||||
|
||||
{/* Loading state */}
|
||||
{codexModelsLoading && dynamicCodexModels.length === 0 && (
|
||||
<div className="flex items-center justify-center gap-2 p-6 text-sm text-muted-foreground">
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
Loading models...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{codexModelsError && !codexModelsLoading && (
|
||||
<div className="flex items-start gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/20">
|
||||
<AlertTriangle className="w-4 h-4 text-red-400 mt-0.5 shrink-0" />
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm text-red-400">Failed to load Codex models</div>
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
onClick={() => onModelSelect(option.id)}
|
||||
title={option.description}
|
||||
className={cn(
|
||||
'w-full px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-between',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-${option.id}`}
|
||||
onClick={() => fetchCodexModels(true)}
|
||||
className="text-xs text-red-400 underline hover:no-underline"
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
{option.badge && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'text-xs',
|
||||
isSelected
|
||||
? 'border-primary-foreground/50 text-primary-foreground'
|
||||
: 'border-muted-foreground/50 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{option.badge}
|
||||
</Badge>
|
||||
)}
|
||||
Retry
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model list */}
|
||||
{!codexModelsLoading && !codexModelsError && dynamicCodexModels.length === 0 && (
|
||||
<div className="text-sm text-muted-foreground p-3 border border-dashed rounded-md text-center">
|
||||
No Codex models available
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!codexModelsLoading && dynamicCodexModels.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{dynamicCodexModels.map((option) => {
|
||||
const isSelected = selectedModel === option.id;
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
onClick={() => onModelSelect(option.id)}
|
||||
title={option.description}
|
||||
className={cn(
|
||||
'w-full px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-between',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-${option.id}`}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
<div className="flex gap-1">
|
||||
{option.hasThinking && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'text-xs',
|
||||
isSelected
|
||||
? 'border-primary-foreground/50 text-primary-foreground'
|
||||
: 'border-emerald-500/50 text-emerald-600 dark:text-emerald-400'
|
||||
)}
|
||||
>
|
||||
Thinking
|
||||
</Badge>
|
||||
)}
|
||||
{option.badge && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'text-xs',
|
||||
isSelected
|
||||
? 'border-primary-foreground/50 text-primary-foreground'
|
||||
: 'border-muted-foreground/50 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{option.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import {
|
||||
formatCodexCredits,
|
||||
formatCodexPlanType,
|
||||
formatCodexResetTime,
|
||||
getCodexWindowLabel,
|
||||
@@ -25,7 +24,6 @@ const UPDATED_LABEL = 'Updated';
|
||||
const CODEX_FETCH_ERROR = 'Failed to fetch usage';
|
||||
const CODEX_REFRESH_LABEL = 'Refresh Codex usage';
|
||||
const PLAN_LABEL = 'Plan';
|
||||
const CREDITS_LABEL = 'Credits';
|
||||
const WARNING_THRESHOLD = 75;
|
||||
const CAUTION_THRESHOLD = 50;
|
||||
const MAX_PERCENTAGE = 100;
|
||||
@@ -49,7 +47,6 @@ export function CodexUsageSection() {
|
||||
const rateLimits = codexUsage?.rateLimits ?? null;
|
||||
const primary = rateLimits?.primary ?? null;
|
||||
const secondary = rateLimits?.secondary ?? null;
|
||||
const credits = rateLimits?.credits ?? null;
|
||||
const planType = rateLimits?.planType ?? null;
|
||||
const rateLimitWindows = [primary, secondary].filter(isRateLimitWindow);
|
||||
const hasMetrics = rateLimitWindows.length > 0;
|
||||
@@ -206,20 +203,11 @@ export function CodexUsageSection() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{(planType || credits) && (
|
||||
{planType && (
|
||||
<div className="rounded-xl border border-border/60 bg-secondary/20 p-4 text-xs text-muted-foreground">
|
||||
{planType && (
|
||||
<div>
|
||||
{PLAN_LABEL}:{' '}
|
||||
<span className="text-foreground">{formatCodexPlanType(planType)}</span>
|
||||
</div>
|
||||
)}
|
||||
{credits && (
|
||||
<div>
|
||||
{CREDITS_LABEL}:{' '}
|
||||
<span className="text-foreground">{formatCodexCredits(credits)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{PLAN_LABEL}: <span className="text-foreground">{formatCodexPlanType(planType)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!hasMetrics && !error && canFetchUsage && !isLoading && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type {
|
||||
@@ -8,8 +8,6 @@ import type {
|
||||
OpencodeModelId,
|
||||
GroupedModel,
|
||||
PhaseModelEntry,
|
||||
ThinkingLevel,
|
||||
ReasoningEffort,
|
||||
} from '@automaker/types';
|
||||
import {
|
||||
stripProviderPrefix,
|
||||
@@ -17,13 +15,11 @@ import {
|
||||
getModelGroup,
|
||||
isGroupSelected,
|
||||
getSelectedVariant,
|
||||
isCursorModel,
|
||||
codexModelHasThinking,
|
||||
} from '@automaker/types';
|
||||
import {
|
||||
CLAUDE_MODELS,
|
||||
CURSOR_MODELS,
|
||||
CODEX_MODELS,
|
||||
OPENCODE_MODELS,
|
||||
THINKING_LEVELS,
|
||||
THINKING_LEVEL_LABELS,
|
||||
@@ -73,23 +69,39 @@ export function PhaseModelSelector({
|
||||
align = 'end',
|
||||
disabled = false,
|
||||
}: PhaseModelSelectorProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [expandedGroup, setExpandedGroup] = React.useState<string | null>(null);
|
||||
const [expandedClaudeModel, setExpandedClaudeModel] = React.useState<ModelAlias | null>(null);
|
||||
const [expandedCodexModel, setExpandedCodexModel] = React.useState<CodexModelId | null>(null);
|
||||
const commandListRef = React.useRef<HTMLDivElement>(null);
|
||||
const expandedTriggerRef = React.useRef<HTMLDivElement>(null);
|
||||
const expandedClaudeTriggerRef = React.useRef<HTMLDivElement>(null);
|
||||
const expandedCodexTriggerRef = React.useRef<HTMLDivElement>(null);
|
||||
const { enabledCursorModels, favoriteModels, toggleFavoriteModel } = useAppStore();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [expandedGroup, setExpandedGroup] = useState<string | null>(null);
|
||||
const [expandedClaudeModel, setExpandedClaudeModel] = useState<ModelAlias | null>(null);
|
||||
const [expandedCodexModel, setExpandedCodexModel] = useState<CodexModelId | null>(null);
|
||||
const commandListRef = useRef<HTMLDivElement>(null);
|
||||
const expandedTriggerRef = useRef<HTMLDivElement>(null);
|
||||
const expandedClaudeTriggerRef = useRef<HTMLDivElement>(null);
|
||||
const expandedCodexTriggerRef = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
enabledCursorModels,
|
||||
favoriteModels,
|
||||
toggleFavoriteModel,
|
||||
codexModels,
|
||||
codexModelsLoading,
|
||||
fetchCodexModels,
|
||||
} = useAppStore();
|
||||
|
||||
// Extract model and thinking/reasoning levels from value
|
||||
const selectedModel = value.model;
|
||||
const selectedThinkingLevel = value.thinkingLevel || 'none';
|
||||
const selectedReasoningEffort = value.reasoningEffort || 'none';
|
||||
|
||||
// Fetch Codex models on mount
|
||||
useEffect(() => {
|
||||
if (codexModels.length === 0 && !codexModelsLoading) {
|
||||
fetchCodexModels().catch(() => {
|
||||
// Silently fail - user will see empty Codex section
|
||||
});
|
||||
}
|
||||
}, [codexModels.length, codexModelsLoading, fetchCodexModels]);
|
||||
|
||||
// Close expanded group when trigger scrolls out of view
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const triggerElement = expandedTriggerRef.current;
|
||||
const listElement = commandListRef.current;
|
||||
if (!triggerElement || !listElement || !expandedGroup) return;
|
||||
@@ -112,7 +124,7 @@ export function PhaseModelSelector({
|
||||
}, [expandedGroup]);
|
||||
|
||||
// Close expanded Claude model popover when trigger scrolls out of view
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const triggerElement = expandedClaudeTriggerRef.current;
|
||||
const listElement = commandListRef.current;
|
||||
if (!triggerElement || !listElement || !expandedClaudeModel) return;
|
||||
@@ -135,7 +147,7 @@ export function PhaseModelSelector({
|
||||
}, [expandedClaudeModel]);
|
||||
|
||||
// Close expanded Codex model popover when trigger scrolls out of view
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const triggerElement = expandedCodexTriggerRef.current;
|
||||
const listElement = commandListRef.current;
|
||||
if (!triggerElement || !listElement || !expandedCodexModel) return;
|
||||
@@ -157,6 +169,17 @@ export function PhaseModelSelector({
|
||||
return () => observer.disconnect();
|
||||
}, [expandedCodexModel]);
|
||||
|
||||
// Transform dynamic Codex models from store to component format
|
||||
const transformedCodexModels = useMemo(() => {
|
||||
return codexModels.map((model) => ({
|
||||
id: model.id,
|
||||
label: model.label,
|
||||
description: model.description,
|
||||
provider: 'codex' as const,
|
||||
badge: model.tier === 'premium' ? 'Premium' : model.tier === 'basic' ? 'Speed' : undefined,
|
||||
}));
|
||||
}, [codexModels]);
|
||||
|
||||
// Filter Cursor models to only show enabled ones
|
||||
const availableCursorModels = CURSOR_MODELS.filter((model) => {
|
||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
||||
@@ -164,7 +187,7 @@ export function PhaseModelSelector({
|
||||
});
|
||||
|
||||
// Helper to find current selected model details
|
||||
const currentModel = React.useMemo(() => {
|
||||
const currentModel = useMemo(() => {
|
||||
const claudeModel = CLAUDE_MODELS.find((m) => m.id === selectedModel);
|
||||
if (claudeModel) {
|
||||
// Add thinking level to label if not 'none'
|
||||
@@ -198,7 +221,7 @@ export function PhaseModelSelector({
|
||||
}
|
||||
|
||||
// Check Codex models
|
||||
const codexModel = CODEX_MODELS.find((m) => m.id === selectedModel);
|
||||
const codexModel = transformedCodexModels.find((m) => m.id === selectedModel);
|
||||
if (codexModel) return { ...codexModel, icon: OpenAIIcon };
|
||||
|
||||
// Check OpenCode models
|
||||
@@ -206,10 +229,10 @@ export function PhaseModelSelector({
|
||||
if (opencodeModel) return { ...opencodeModel, icon: OpenCodeIcon };
|
||||
|
||||
return null;
|
||||
}, [selectedModel, selectedThinkingLevel, availableCursorModels]);
|
||||
}, [selectedModel, selectedThinkingLevel, availableCursorModels, transformedCodexModels]);
|
||||
|
||||
// Compute grouped vs standalone Cursor models
|
||||
const { groupedModels, standaloneCursorModels } = React.useMemo(() => {
|
||||
const { groupedModels, standaloneCursorModels } = useMemo(() => {
|
||||
const grouped: GroupedModel[] = [];
|
||||
const standalone: typeof CURSOR_MODELS = [];
|
||||
const seenGroups = new Set<string>();
|
||||
@@ -242,11 +265,11 @@ export function PhaseModelSelector({
|
||||
}, [availableCursorModels, enabledCursorModels]);
|
||||
|
||||
// Group models
|
||||
const { favorites, claude, cursor, codex, opencode } = React.useMemo(() => {
|
||||
const { favorites, claude, cursor, codex, opencode } = useMemo(() => {
|
||||
const favs: typeof CLAUDE_MODELS = [];
|
||||
const cModels: typeof CLAUDE_MODELS = [];
|
||||
const curModels: typeof CURSOR_MODELS = [];
|
||||
const codModels: typeof CODEX_MODELS = [];
|
||||
const codModels: typeof transformedCodexModels = [];
|
||||
const ocModels: typeof OPENCODE_MODELS = [];
|
||||
|
||||
// Process Claude Models
|
||||
@@ -268,7 +291,7 @@ export function PhaseModelSelector({
|
||||
});
|
||||
|
||||
// Process Codex Models
|
||||
CODEX_MODELS.forEach((model) => {
|
||||
transformedCodexModels.forEach((model) => {
|
||||
if (favoriteModels.includes(model.id)) {
|
||||
favs.push(model);
|
||||
} else {
|
||||
@@ -292,10 +315,10 @@ export function PhaseModelSelector({
|
||||
codex: codModels,
|
||||
opencode: ocModels,
|
||||
};
|
||||
}, [favoriteModels, availableCursorModels]);
|
||||
}, [favoriteModels, availableCursorModels, transformedCodexModels]);
|
||||
|
||||
// Render Codex model item with secondary popover for reasoning effort (only for models that support it)
|
||||
const renderCodexModelItem = (model: (typeof CODEX_MODELS)[0]) => {
|
||||
const renderCodexModelItem = (model: (typeof transformedCodexModels)[0]) => {
|
||||
const isSelected = selectedModel === model.id;
|
||||
const isFavorite = favoriteModels.includes(model.id);
|
||||
const hasReasoning = codexModelHasThinking(model.id as CodexModelId);
|
||||
@@ -919,7 +942,7 @@ export function PhaseModelSelector({
|
||||
}
|
||||
// Codex model
|
||||
if (model.provider === 'codex') {
|
||||
return renderCodexModelItem(model);
|
||||
return renderCodexModelItem(model as (typeof transformedCodexModels)[0]);
|
||||
}
|
||||
// OpenCode model
|
||||
if (model.provider === 'opencode') {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { type CodexCreditsSnapshot, type CodexPlanType } from '@/store/app-store';
|
||||
import { type CodexPlanType } from '@/store/app-store';
|
||||
|
||||
const WINDOW_DEFAULT_LABEL = 'Usage window';
|
||||
const RESET_LABEL = 'Resets';
|
||||
const UNKNOWN_LABEL = 'Unknown';
|
||||
const UNAVAILABLE_LABEL = 'Unavailable';
|
||||
const UNLIMITED_LABEL = 'Unlimited';
|
||||
const AVAILABLE_LABEL = 'Available';
|
||||
const NONE_LABEL = 'None';
|
||||
const DAY_UNIT = 'day';
|
||||
const HOUR_UNIT = 'hour';
|
||||
const MINUTE_UNIT = 'min';
|
||||
@@ -77,10 +73,3 @@ export function formatCodexPlanType(plan: CodexPlanType | null): string {
|
||||
if (!plan) return UNKNOWN_LABEL;
|
||||
return PLAN_TYPE_LABELS[plan] ?? plan;
|
||||
}
|
||||
|
||||
export function formatCodexCredits(snapshot: CodexCreditsSnapshot | null): string {
|
||||
if (!snapshot) return UNAVAILABLE_LABEL;
|
||||
if (snapshot.unlimited) return UNLIMITED_LABEL;
|
||||
if (snapshot.balance) return snapshot.balance;
|
||||
return snapshot.hasCredits ? AVAILABLE_LABEL : NONE_LABEL;
|
||||
}
|
||||
|
||||
@@ -727,6 +727,20 @@ export interface ElectronAPI {
|
||||
ideation?: IdeationAPI;
|
||||
codex?: {
|
||||
getUsage: () => Promise<CodexUsageResponse>;
|
||||
getModels: (refresh?: boolean) => Promise<{
|
||||
success: boolean;
|
||||
models?: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
hasThinking: boolean;
|
||||
supportsVision: boolean;
|
||||
tier: 'premium' | 'standard' | 'basic';
|
||||
isDefault: boolean;
|
||||
}>;
|
||||
cachedAt?: number;
|
||||
error?: string;
|
||||
}>;
|
||||
};
|
||||
settings?: {
|
||||
getStatus: () => Promise<{
|
||||
|
||||
@@ -2053,6 +2053,25 @@ export class HttpApiClient implements ElectronAPI {
|
||||
// Codex API
|
||||
codex = {
|
||||
getUsage: (): Promise<CodexUsageResponse> => this.get('/api/codex/usage'),
|
||||
getModels: (
|
||||
refresh = false
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
models?: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
hasThinking: boolean;
|
||||
supportsVision: boolean;
|
||||
tier: 'premium' | 'standard' | 'basic';
|
||||
isDefault: boolean;
|
||||
}>;
|
||||
cachedAt?: number;
|
||||
error?: string;
|
||||
}> => {
|
||||
const url = `/api/codex/models${refresh ? '?refresh=true' : ''}`;
|
||||
return this.get(url);
|
||||
},
|
||||
};
|
||||
|
||||
// Context API
|
||||
|
||||
@@ -68,8 +68,9 @@ function RootLayoutContent() {
|
||||
getEffectiveTheme,
|
||||
skipSandboxWarning,
|
||||
setSkipSandboxWarning,
|
||||
fetchCodexModels,
|
||||
} = useAppStore();
|
||||
const { setupComplete } = useSetupStore();
|
||||
const { setupComplete, codexCliStatus } = useSetupStore();
|
||||
const navigate = useNavigate();
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [streamerPanelOpen, setStreamerPanelOpen] = useState(false);
|
||||
@@ -431,6 +432,20 @@ function RootLayoutContent() {
|
||||
}
|
||||
}, [isMounted, currentProject, location.pathname, navigate]);
|
||||
|
||||
// Bootstrap Codex models on app startup (after auth completes)
|
||||
useEffect(() => {
|
||||
// Only fetch if authenticated and Codex CLI is available
|
||||
if (!authChecked || !isAuthenticated) return;
|
||||
|
||||
const isCodexAvailable = codexCliStatus?.installed && codexCliStatus?.auth?.authenticated;
|
||||
if (!isCodexAvailable) return;
|
||||
|
||||
// Fetch models in the background
|
||||
fetchCodexModels().catch((error) => {
|
||||
logger.warn('Failed to bootstrap Codex models:', error);
|
||||
});
|
||||
}, [authChecked, isAuthenticated, codexCliStatus, fetchCodexModels]);
|
||||
|
||||
// Apply theme class to document - use deferred value to avoid blocking UI
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
// Note: persist middleware removed - settings now sync via API (use-settings-sync.ts)
|
||||
import type { Project, TrashedProject } from '@/lib/electron';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { setItem, getItem } from '@/lib/storage';
|
||||
import type {
|
||||
@@ -638,6 +639,20 @@ export interface AppState {
|
||||
codexUsage: CodexUsage | null;
|
||||
codexUsageLastUpdated: number | null;
|
||||
|
||||
// Codex Models (dynamically fetched)
|
||||
codexModels: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
hasThinking: boolean;
|
||||
supportsVision: boolean;
|
||||
tier: 'premium' | 'standard' | 'basic';
|
||||
isDefault: boolean;
|
||||
}>;
|
||||
codexModelsLoading: boolean;
|
||||
codexModelsError: string | null;
|
||||
codexModelsLastFetched: number | null;
|
||||
|
||||
// Pipeline Configuration (per-project, keyed by project path)
|
||||
pipelineConfigByProject: Record<string, PipelineConfig>;
|
||||
|
||||
@@ -690,12 +705,6 @@ export type CodexPlanType =
|
||||
| 'edu'
|
||||
| 'unknown';
|
||||
|
||||
export interface CodexCreditsSnapshot {
|
||||
balance?: string;
|
||||
unlimited?: boolean;
|
||||
hasCredits?: boolean;
|
||||
}
|
||||
|
||||
export interface CodexRateLimitWindow {
|
||||
limit: number;
|
||||
used: number;
|
||||
@@ -709,7 +718,6 @@ export interface CodexUsage {
|
||||
rateLimits: {
|
||||
primary?: CodexRateLimitWindow;
|
||||
secondary?: CodexRateLimitWindow;
|
||||
credits?: CodexCreditsSnapshot;
|
||||
planType?: CodexPlanType;
|
||||
} | null;
|
||||
lastUpdated: string;
|
||||
@@ -1068,6 +1076,20 @@ export interface AppActions {
|
||||
// Codex Usage Tracking actions
|
||||
setCodexUsage: (usage: CodexUsage | null) => void;
|
||||
|
||||
// Codex Models actions
|
||||
fetchCodexModels: (forceRefresh?: boolean) => Promise<void>;
|
||||
setCodexModels: (
|
||||
models: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
hasThinking: boolean;
|
||||
supportsVision: boolean;
|
||||
tier: 'premium' | 'standard' | 'basic';
|
||||
isDefault: boolean;
|
||||
}>
|
||||
) => void;
|
||||
|
||||
// Reset
|
||||
reset: () => void;
|
||||
}
|
||||
@@ -1159,6 +1181,10 @@ const initialState: AppState = {
|
||||
claudeUsageLastUpdated: null,
|
||||
codexUsage: null,
|
||||
codexUsageLastUpdated: null,
|
||||
codexModels: [],
|
||||
codexModelsLoading: false,
|
||||
codexModelsError: null,
|
||||
codexModelsLastFetched: null,
|
||||
pipelineConfigByProject: {},
|
||||
// UI State (previously in localStorage, now synced via API)
|
||||
worktreePanelCollapsed: false,
|
||||
@@ -2898,6 +2924,53 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
codexUsageLastUpdated: usage ? Date.now() : null,
|
||||
}),
|
||||
|
||||
// Codex Models actions
|
||||
fetchCodexModels: async (forceRefresh = false) => {
|
||||
const { codexModelsLastFetched, codexModelsLoading } = get();
|
||||
|
||||
// Skip if already loading
|
||||
if (codexModelsLoading) return;
|
||||
|
||||
// Skip if recently fetched (< 5 minutes ago) and not forcing refresh
|
||||
if (!forceRefresh && codexModelsLastFetched && Date.now() - codexModelsLastFetched < 300000) {
|
||||
return;
|
||||
}
|
||||
|
||||
set({ codexModelsLoading: true, codexModelsError: null });
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.codex) {
|
||||
throw new Error('Codex API not available');
|
||||
}
|
||||
|
||||
const result = await api.codex.getModels(forceRefresh);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to fetch Codex models');
|
||||
}
|
||||
|
||||
set({
|
||||
codexModels: result.models || [],
|
||||
codexModelsLastFetched: Date.now(),
|
||||
codexModelsLoading: false,
|
||||
codexModelsError: null,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
set({
|
||||
codexModelsError: errorMessage,
|
||||
codexModelsLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setCodexModels: (models) =>
|
||||
set({
|
||||
codexModels: models,
|
||||
codexModelsLastFetched: Date.now(),
|
||||
}),
|
||||
|
||||
// Pipeline actions
|
||||
setPipelineConfig: (projectPath, config) => {
|
||||
set({
|
||||
|
||||
87
libs/types/src/codex-app-server.ts
Normal file
87
libs/types/src/codex-app-server.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Codex App-Server JSON-RPC Types
|
||||
*
|
||||
* Type definitions for communicating with Codex CLI's app-server via JSON-RPC protocol.
|
||||
* These types match the response structures from the `codex app-server` command.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Response from model/list JSON-RPC method
|
||||
* Returns list of available Codex models for the authenticated user
|
||||
*/
|
||||
export interface AppServerModelResponse {
|
||||
data: AppServerModel[];
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
export interface AppServerModel {
|
||||
id: string;
|
||||
model: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
supportedReasoningEfforts: AppServerReasoningEffort[];
|
||||
defaultReasoningEffort: string;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface AppServerReasoningEffort {
|
||||
reasoningEffort: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from account/read JSON-RPC method
|
||||
* Returns current authentication state and account information
|
||||
*/
|
||||
export interface AppServerAccountResponse {
|
||||
account: AppServerAccount | null;
|
||||
requiresOpenaiAuth: boolean;
|
||||
}
|
||||
|
||||
export interface AppServerAccount {
|
||||
type: 'apiKey' | 'chatgpt';
|
||||
email?: string;
|
||||
planType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from account/rateLimits/read JSON-RPC method
|
||||
* Returns rate limit information for the current user
|
||||
*/
|
||||
export interface AppServerRateLimitsResponse {
|
||||
rateLimits: AppServerRateLimits;
|
||||
}
|
||||
|
||||
export interface AppServerRateLimits {
|
||||
primary: AppServerRateLimitWindow | null;
|
||||
secondary: AppServerRateLimitWindow | null;
|
||||
planType?: string;
|
||||
}
|
||||
|
||||
export interface AppServerRateLimitWindow {
|
||||
usedPercent: number;
|
||||
windowDurationMins: number;
|
||||
resetsAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic JSON-RPC request structure
|
||||
*/
|
||||
export interface JsonRpcRequest {
|
||||
method: string;
|
||||
id: number;
|
||||
params?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic JSON-RPC response structure
|
||||
*/
|
||||
export interface JsonRpcResponse<T = unknown> {
|
||||
id: number;
|
||||
result?: T;
|
||||
error?: {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
};
|
||||
}
|
||||
@@ -30,6 +30,20 @@ export type {
|
||||
} from './codex.js';
|
||||
export * from './codex-models.js';
|
||||
|
||||
// Codex App-Server JSON-RPC types
|
||||
export type {
|
||||
AppServerModelResponse,
|
||||
AppServerModel,
|
||||
AppServerReasoningEffort,
|
||||
AppServerAccountResponse,
|
||||
AppServerAccount,
|
||||
AppServerRateLimitsResponse,
|
||||
AppServerRateLimits,
|
||||
AppServerRateLimitWindow,
|
||||
JsonRpcRequest,
|
||||
JsonRpcResponse,
|
||||
} from './codex-app-server.js';
|
||||
|
||||
// Feature types
|
||||
export type {
|
||||
Feature,
|
||||
|
||||
103
package-lock.json
generated
103
package-lock.json
generated
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"apps/server": {
|
||||
"name": "@automaker/server",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "0.1.76",
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"apps/ui": {
|
||||
"name": "@automaker/ui",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
@@ -677,6 +677,7 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -1260,6 +1261,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz",
|
||||
"integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
@@ -1302,6 +1304,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -2122,7 +2125,6 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cross-dirname": "^0.1.0",
|
||||
"debug": "^4.3.4",
|
||||
@@ -2144,7 +2146,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -2161,7 +2162,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
@@ -2176,7 +2176,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
@@ -2944,7 +2943,6 @@
|
||||
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -3069,7 +3067,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
@@ -3086,7 +3083,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
@@ -3103,7 +3099,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
@@ -3212,7 +3207,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -3235,7 +3229,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -3258,7 +3251,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -3344,7 +3336,6 @@
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
@@ -3367,7 +3358,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -3387,7 +3377,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -3787,8 +3776,7 @@
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz",
|
||||
"integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "16.0.10",
|
||||
@@ -3802,7 +3790,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
@@ -3819,7 +3806,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
@@ -3836,7 +3822,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
@@ -3853,7 +3838,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
@@ -3870,7 +3854,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
@@ -3887,7 +3870,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
@@ -3904,7 +3886,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
@@ -3921,7 +3902,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
@@ -4021,6 +4001,7 @@
|
||||
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"playwright": "1.57.0"
|
||||
},
|
||||
@@ -5461,7 +5442,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
@@ -5795,6 +5775,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.6.tgz",
|
||||
"integrity": "sha512-qWFxi2D6eGc1L03RzUuhyEOplZ7Q6q62YOl7Of9Y0q4YjwQwxRm4zxwDVtvUIoy4RLVCpqp5UoE+Nxv2PY9trg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.141.0",
|
||||
"@tanstack/react-store": "^0.8.0",
|
||||
@@ -6221,6 +6202,7 @@
|
||||
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
@@ -6363,6 +6345,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -6373,6 +6356,7 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -6478,6 +6462,7 @@
|
||||
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
@@ -6971,7 +6956,8 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.10.0",
|
||||
@@ -7069,6 +7055,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -7129,6 +7116,7 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -7727,6 +7715,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -8258,8 +8247,7 @@
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
@@ -8564,8 +8552,7 @@
|
||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.1.0",
|
||||
@@ -8662,6 +8649,7 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -8963,6 +8951,7 @@
|
||||
"integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "26.0.12",
|
||||
"builder-util": "26.0.11",
|
||||
@@ -9289,7 +9278,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"debug": "^4.1.1",
|
||||
@@ -9310,7 +9298,6 @@
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
@@ -9561,6 +9548,7 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -9875,6 +9863,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -11542,7 +11531,6 @@
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -11564,7 +11552,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -11586,7 +11573,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -11608,7 +11594,6 @@
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -11630,7 +11615,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -11652,7 +11636,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -11674,7 +11657,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -11696,7 +11678,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -11718,7 +11699,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -11740,7 +11720,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -11762,7 +11741,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -14050,7 +14028,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
@@ -14067,7 +14044,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^9.4.0"
|
||||
},
|
||||
@@ -14085,7 +14061,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
@@ -14274,6 +14249,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14283,6 +14259,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -14641,7 +14618,6 @@
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
@@ -14830,6 +14806,7 @@
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.0.tgz",
|
||||
"integrity": "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -14878,7 +14855,6 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
@@ -14929,7 +14905,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -14952,7 +14927,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -14975,7 +14949,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
@@ -14992,7 +14965,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
@@ -15009,7 +14981,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
@@ -15026,7 +14997,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
@@ -15043,7 +15013,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
@@ -15060,7 +15029,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
@@ -15077,7 +15045,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
@@ -15094,7 +15061,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -15117,7 +15083,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -15140,7 +15105,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -15163,7 +15127,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -15186,7 +15149,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -15209,7 +15171,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -15678,7 +15639,6 @@
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"client-only": "0.0.1"
|
||||
},
|
||||
@@ -15848,7 +15808,6 @@
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
@@ -15912,7 +15871,6 @@
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
@@ -16010,6 +15968,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16214,6 +16173,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16585,6 +16545,7 @@
|
||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -16674,7 +16635,8 @@
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
|
||||
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
@@ -16700,6 +16662,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16742,6 +16705,7 @@
|
||||
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.16",
|
||||
"@vitest/mocker": "4.0.16",
|
||||
@@ -17067,6 +17031,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
|
||||
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user