feat: update Codex services and UI components for enhanced model management

- Bumped version numbers for @automaker/server and @automaker/ui to 0.9.0 in package-lock.json.
- Introduced CodexAppServerService and CodexModelCacheService to manage communication with the Codex CLI's app-server and cache model data.
- Updated CodexUsageService to utilize app-server for fetching usage data.
- Enhanced Codex routes to support fetching available models and integrated model caching.
- Improved UI components to dynamically load and display Codex models, including error handling and loading states.
- Added new API methods for fetching Codex models and integrated them into the app store for state management.

These changes improve the overall functionality and user experience of the Codex integration, ensuring efficient model management and data retrieval.
This commit is contained in:
Shirone
2026-01-10 14:33:55 +01:00
parent eb94e4de72
commit 99b05d35a2
16 changed files with 981 additions and 409 deletions

View File

@@ -55,6 +55,8 @@ import { createClaudeRoutes } from './routes/claude/index.js';
import { ClaudeUsageService } from './services/claude-usage-service.js'; import { ClaudeUsageService } from './services/claude-usage-service.js';
import { createCodexRoutes } from './routes/codex/index.js'; import { createCodexRoutes } from './routes/codex/index.js';
import { CodexUsageService } from './services/codex-usage-service.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 { createGitHubRoutes } from './routes/github/index.js';
import { createContextRoutes } from './routes/context/index.js'; import { createContextRoutes } from './routes/context/index.js';
import { createBacklogPlanRoutes } from './routes/backlog-plan/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 featureLoader = new FeatureLoader();
const autoModeService = new AutoModeService(events, settingsService); const autoModeService = new AutoModeService(events, settingsService);
const claudeUsageService = new ClaudeUsageService(); 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 mcpTestService = new MCPTestService(settingsService);
const ideationService = new IdeationService(events, settingsService, featureLoader); const ideationService = new IdeationService(events, settingsService, featureLoader);
@@ -176,6 +180,11 @@ const ideationService = new IdeationService(events, settingsService, featureLoad
(async () => { (async () => {
await agentService.initialize(); await agentService.initialize();
logger.info('Agent service initialized'); 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 // 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/terminal', createTerminalRoutes());
app.use('/api/settings', createSettingsRoutes(settingsService)); app.use('/api/settings', createSettingsRoutes(settingsService));
app.use('/api/claude', createClaudeRoutes(claudeUsageService)); 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/github', createGitHubRoutes(events, settingsService));
app.use('/api/context', createContextRoutes(settingsService)); app.use('/api/context', createContextRoutes(settingsService));
app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService)); app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService));

View File

@@ -5,9 +5,11 @@
* Never assumes authenticated - only returns true if CLI confirms. * 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 { findCodexCliPath } from '@automaker/platform';
import * as fs from 'fs'; import { createLogger } from '@automaker/utils';
const logger = createLogger('CodexAuth');
const CODEX_COMMAND = 'codex'; const CODEX_COMMAND = 'codex';
const OPENAI_API_KEY_ENV = 'OPENAI_API_KEY'; const OPENAI_API_KEY_ENV = 'OPENAI_API_KEY';
@@ -26,36 +28,16 @@ export interface CodexAuthCheckResult {
export async function checkCodexAuthentication( export async function checkCodexAuthentication(
cliPath?: string | null cliPath?: string | null
): Promise<CodexAuthCheckResult> { ): Promise<CodexAuthCheckResult> {
console.log('[CodexAuth] checkCodexAuthentication called with cliPath:', cliPath);
const resolvedCliPath = cliPath || (await findCodexCliPath()); const resolvedCliPath = cliPath || (await findCodexCliPath());
const hasApiKey = !!process.env[OPENAI_API_KEY_ENV]; 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 CLI is not installed, cannot be authenticated
if (!resolvedCliPath) { if (!resolvedCliPath) {
console.log('[CodexAuth] No CLI path found, returning not authenticated'); logger.info('CLI not found');
return { authenticated: false, method: 'none' }; return { authenticated: false, method: 'none' };
} }
try { try {
console.log('[CodexAuth] Running: ' + resolvedCliPath + ' login status');
const result = await spawnProcess({ const result = await spawnProcess({
command: resolvedCliPath || CODEX_COMMAND, command: resolvedCliPath || CODEX_COMMAND,
args: ['login', 'status'], 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 // Check both stdout and stderr for "logged in" - Codex CLI outputs to stderr
const combinedOutput = (result.stdout + result.stderr).toLowerCase(); const combinedOutput = (result.stdout + result.stderr).toLowerCase();
const isLoggedIn = combinedOutput.includes('logged in'); const isLoggedIn = combinedOutput.includes('logged in');
console.log('[CodexAuth] isLoggedIn (contains "logged in" in stdout or stderr):', isLoggedIn);
if (result.exitCode === 0 && isLoggedIn) { if (result.exitCode === 0 && isLoggedIn) {
// Determine auth method based on what we know // Determine auth method based on what we know
const method = hasApiKey ? 'api_key_env' : 'cli_authenticated'; const method = hasApiKey ? 'api_key_env' : 'cli_authenticated';
console.log('[CodexAuth] Authenticated! method:', method); logger.info(`✓ Authenticated (${method})`);
return { authenticated: true, method }; return { authenticated: true, method };
} }
console.log( logger.info('Not authenticated');
'[CodexAuth] Not authenticated. exitCode:', return { authenticated: false, method: 'none' };
result.exitCode,
'isLoggedIn:',
isLoggedIn
);
} catch (error) { } catch (error) {
console.log('[CodexAuth] Error running command:', error); logger.error('Failed to check authentication:', error);
}
console.log('[CodexAuth] Returning not authenticated');
return { authenticated: false, method: 'none' }; return { authenticated: false, method: 'none' };
} }
}

View File

@@ -967,21 +967,11 @@ export class CodexProvider extends BaseProvider {
} }
async detectInstallation(): Promise<InstallationStatus> { async detectInstallation(): Promise<InstallationStatus> {
console.log('[CodexProvider.detectInstallation] Starting...');
const cliPath = await findCodexCliPath(); const cliPath = await findCodexCliPath();
const hasApiKey = !!process.env[OPENAI_API_KEY_ENV]; const hasApiKey = !!process.env[OPENAI_API_KEY_ENV];
const authIndicators = await getCodexAuthIndicators(); const authIndicators = await getCodexAuthIndicators();
const installed = !!cliPath; 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 = ''; let version = '';
if (installed) { if (installed) {
try { try {
@@ -991,20 +981,16 @@ export class CodexProvider extends BaseProvider {
cwd: process.cwd(), cwd: process.cwd(),
}); });
version = result.stdout.trim(); version = result.stdout.trim();
console.log('[CodexProvider.detectInstallation] version:', version);
} catch (error) { } catch (error) {
console.log('[CodexProvider.detectInstallation] Error getting version:', error);
version = ''; version = '';
} }
} }
// Determine auth status - always verify with CLI, never assume authenticated // Determine auth status - always verify with CLI, never assume authenticated
console.log('[CodexProvider.detectInstallation] Calling checkCodexAuthentication...');
const authCheck = await checkCodexAuthentication(cliPath); const authCheck = await checkCodexAuthentication(cliPath);
console.log('[CodexProvider.detectInstallation] authCheck result:', JSON.stringify(authCheck));
const authenticated = authCheck.authenticated; const authenticated = authCheck.authenticated;
const result = { return {
installed, installed,
path: cliPath || undefined, path: cliPath || undefined,
version: version || undefined, version: version || undefined,
@@ -1012,8 +998,6 @@ export class CodexProvider extends BaseProvider {
hasApiKey, hasApiKey,
authenticated, authenticated,
}; };
console.log('[CodexProvider.detectInstallation] Final result:', JSON.stringify(result));
return result;
} }
getAvailableModels(): ModelDefinition[] { getAvailableModels(): ModelDefinition[] {
@@ -1025,36 +1009,24 @@ export class CodexProvider extends BaseProvider {
* Check authentication status for Codex CLI * Check authentication status for Codex CLI
*/ */
async checkAuth(): Promise<CodexAuthStatus> { async checkAuth(): Promise<CodexAuthStatus> {
console.log('[CodexProvider.checkAuth] Starting auth check...');
const cliPath = await findCodexCliPath(); const cliPath = await findCodexCliPath();
const hasApiKey = !!process.env[OPENAI_API_KEY_ENV]; const hasApiKey = !!process.env[OPENAI_API_KEY_ENV];
const authIndicators = await getCodexAuthIndicators(); 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 // Check for API key in environment
if (hasApiKey) { if (hasApiKey) {
console.log('[CodexProvider.checkAuth] Has API key, returning authenticated');
return { authenticated: true, method: 'api_key' }; return { authenticated: true, method: 'api_key' };
} }
// Check for OAuth/token from Codex CLI // Check for OAuth/token from Codex CLI
if (authIndicators.hasOAuthToken || authIndicators.hasApiKey) { 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' }; return { authenticated: true, method: 'oauth' };
} }
// CLI is installed but not authenticated via indicators - try CLI command // CLI is installed but not authenticated via indicators - try CLI command
console.log('[CodexProvider.checkAuth] No indicators found, trying CLI command...');
if (cliPath) { if (cliPath) {
try { try {
// Try 'codex login status' first (same as checkCodexAuthentication) // Try 'codex login status' first (same as checkCodexAuthentication)
console.log('[CodexProvider.checkAuth] Running: ' + cliPath + ' login status');
const result = await spawnProcess({ const result = await spawnProcess({
command: cliPath || CODEX_COMMAND, command: cliPath || CODEX_COMMAND,
args: ['login', 'status'], args: ['login', 'status'],
@@ -1064,26 +1036,19 @@ export class CodexProvider extends BaseProvider {
TERM: 'dumb', 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 // Check both stdout and stderr - Codex CLI outputs to stderr
const combinedOutput = (result.stdout + result.stderr).toLowerCase(); const combinedOutput = (result.stdout + result.stderr).toLowerCase();
const isLoggedIn = combinedOutput.includes('logged in'); const isLoggedIn = combinedOutput.includes('logged in');
console.log('[CodexProvider.checkAuth] isLoggedIn:', isLoggedIn);
if (result.exitCode === 0 && isLoggedIn) { if (result.exitCode === 0 && isLoggedIn) {
console.log('[CodexProvider.checkAuth] CLI says logged in, returning authenticated');
return { authenticated: true, method: 'oauth' }; return { authenticated: true, method: 'oauth' };
} }
} catch (error) { } catch (error) {
console.log('[CodexProvider.checkAuth] Error running login status:', error); // Silent fail
} }
} }
console.log('[CodexProvider.checkAuth] Not authenticated');
return { authenticated: false, method: 'none' }; return { authenticated: false, method: 'none' };
} }

View File

@@ -1,17 +1,21 @@
import { Router, Request, Response } from 'express'; import { Router, Request, Response } from 'express';
import { CodexUsageService } from '../../services/codex-usage-service.js'; import { CodexUsageService } from '../../services/codex-usage-service.js';
import { CodexModelCacheService } from '../../services/codex-model-cache-service.js';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
const logger = createLogger('Codex'); const logger = createLogger('Codex');
export function createCodexRoutes(service: CodexUsageService): Router { export function createCodexRoutes(
usageService: CodexUsageService,
modelCacheService: CodexModelCacheService
): Router {
const router = Router(); const router = Router();
// Get current usage (attempts to fetch from Codex CLI) // 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 { try {
// Check if Codex CLI is available first // Check if Codex CLI is available first
const isAvailable = await service.isAvailable(); const isAvailable = await usageService.isAvailable();
if (!isAvailable) { if (!isAvailable) {
// IMPORTANT: This endpoint is behind Automaker session auth already. // IMPORTANT: This endpoint is behind Automaker session auth already.
// Use a 200 + error payload for Codex CLI issues so the UI doesn't // 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; return;
} }
const usage = await service.fetchUsageData(); const usage = await usageService.fetchUsageData();
res.json(usage); res.json(usage);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Unknown 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 = await modelCacheService.getModels(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: Date.now(),
});
} 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; return router;
} }

View 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');
}
}
}
}

View File

@@ -0,0 +1,240 @@
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();
}
/**
* 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
}
}
}
}

View File

@@ -1,5 +1,3 @@
import { spawn, type ChildProcess } from 'child_process';
import readline from 'readline';
import { import {
findCodexCliPath, findCodexCliPath,
getCodexAuthPath, getCodexAuthPath,
@@ -7,6 +5,7 @@ import {
systemPathReadFile, systemPathReadFile,
} from '@automaker/platform'; } from '@automaker/platform';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import type { CodexAppServerService } from './codex-app-server-service.js';
const logger = createLogger('CodexUsage'); const logger = createLogger('CodexUsage');
@@ -37,35 +36,6 @@ export interface CodexUsageData {
lastUpdated: string; lastUpdated: string;
} }
/**
* JSON-RPC response types from Codex app-server
*/
interface AppServerAccountResponse {
account: {
type: 'apiKey' | 'chatgpt';
email?: string;
planType?: string;
} | null;
requiresOpenaiAuth: boolean;
}
interface AppServerRateLimitsResponse {
rateLimits: {
primary: {
usedPercent: number;
windowDurationMins: number;
resetsAt: number;
} | null;
secondary: {
usedPercent: number;
windowDurationMins: number;
resetsAt: number;
} | null;
credits?: unknown;
planType?: string; // This is the most accurate/current plan type
};
}
/** /**
* Codex Usage Service * Codex Usage Service
* *
@@ -74,6 +44,7 @@ interface AppServerRateLimitsResponse {
*/ */
export class CodexUsageService { export class CodexUsageService {
private cachedCliPath: string | null = null; private cachedCliPath: string | null = null;
private appServerService: CodexAppServerService | null = null;
private accountPlanTypeArray: CodexPlanType[] = [ private accountPlanTypeArray: CodexPlanType[] = [
'free', 'free',
'plus', 'plus',
@@ -82,6 +53,11 @@ export class CodexUsageService {
'enterprise', 'enterprise',
'edu', 'edu',
]; ];
constructor(appServerService?: CodexAppServerService) {
this.appServerService = appServerService || null;
}
/** /**
* Check if Codex CLI is available on the system * Check if Codex CLI is available on the system
*/ */
@@ -109,12 +85,9 @@ export class CodexUsageService {
logger.info(`[fetchUsageData] Using CLI path: ${cliPath}`); logger.info(`[fetchUsageData] Using CLI path: ${cliPath}`);
// Try to get usage from Codex app-server (most reliable method) // Try to get usage from Codex app-server (most reliable method)
const appServerUsage = await this.fetchFromAppServer(cliPath); const appServerUsage = await this.fetchFromAppServer();
if (appServerUsage) { if (appServerUsage) {
logger.info( logger.info('[fetchUsageData] ✓ Fetched usage from app-server');
'[fetchUsageData] Got data from app-server:',
JSON.stringify(appServerUsage, null, 2)
);
return appServerUsage; return appServerUsage;
} }
@@ -123,7 +96,7 @@ export class CodexUsageService {
// Fallback: try to parse usage from auth file // Fallback: try to parse usage from auth file
const authUsage = await this.fetchFromAuthFile(); const authUsage = await this.fetchFromAuthFile();
if (authUsage) { if (authUsage) {
logger.info('[fetchUsageData] Got data from auth file:', JSON.stringify(authUsage, null, 2)); logger.info('[fetchUsageData] ✓ Fetched usage from auth file');
return authUsage; return authUsage;
} }
@@ -145,138 +118,22 @@ export class CodexUsageService {
* Fetch usage data from Codex app-server using JSON-RPC API * 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 * This is the most reliable method as it gets real-time data from OpenAI
*/ */
private async fetchFromAppServer(cliPath: string): Promise<CodexUsageData | null> { private async fetchFromAppServer(): Promise<CodexUsageData | null> {
let childProcess: ChildProcess | null = null;
try { try {
// On Windows, .cmd files must be run through shell // Use CodexAppServerService if available
const needsShell = process.platform === 'win32' && cliPath.toLowerCase().endsWith('.cmd'); if (!this.appServerService) {
return null;
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 // Fetch account and rate limits in parallel
const rl = readline.createInterface({ const [accountResult, rateLimitsResult] = await Promise.all([
input: childProcess.stdout, this.appServerService.getAccount(),
crlfDelay: Infinity, this.appServerService.getRateLimits(),
}); ]);
// Message ID counter for JSON-RPC if (!accountResult) {
let messageId = 0; return null;
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 = <T>(method: string, params?: unknown): Promise<T> => {
return new Promise((resolve, reject) => {
const id = ++messageId;
const request = params ? { method, id, params } : { method, id };
// Set timeout for request
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
logger.info('[fetchFromAppServer] Sending initialize request...');
const initResult = await sendRequest('initialize', {
clientInfo: {
name: 'automaker',
title: 'AutoMaker',
version: '1.0.0',
},
});
logger.info('[fetchFromAppServer] Initialize result:', JSON.stringify(initResult, null, 2));
// 2. Send initialized notification
sendNotification('initialized');
logger.info('[fetchFromAppServer] Sent initialized notification');
// 3. Get account info (includes plan type)
logger.info('[fetchFromAppServer] Requesting account/read...');
const accountResult = await sendRequest<AppServerAccountResponse>('account/read', {
refreshToken: false,
});
logger.info('[fetchFromAppServer] Account result:', JSON.stringify(accountResult, null, 2));
// 4. Get rate limits
let rateLimitsResult: AppServerRateLimitsResponse | null = null;
try {
logger.info('[fetchFromAppServer] Requesting account/rateLimits/read...');
rateLimitsResult =
await sendRequest<AppServerRateLimitsResponse>('account/rateLimits/read');
logger.info(
'[fetchFromAppServer] Rate limits result:',
JSON.stringify(rateLimitsResult, null, 2)
);
} catch (rateLimitError) {
// Rate limits may not be available for API key auth
logger.info('[fetchFromAppServer] Rate limits not available:', rateLimitError);
}
// Clean up
rl.close();
childProcess.kill('SIGTERM');
// Build response // Build response
// Prefer planType from rateLimits (more accurate/current) over account (can be stale) // Prefer planType from rateLimits (more accurate/current) over account (can be stale)
@@ -286,9 +143,6 @@ export class CodexUsageService {
const rateLimitsPlanType = rateLimitsResult?.rateLimits?.planType; const rateLimitsPlanType = rateLimitsResult?.rateLimits?.planType;
if (rateLimitsPlanType) { if (rateLimitsPlanType) {
const normalizedType = rateLimitsPlanType.toLowerCase() as CodexPlanType; const normalizedType = rateLimitsPlanType.toLowerCase() as CodexPlanType;
logger.info(
`[fetchFromAppServer] Rate limits planType: "${rateLimitsPlanType}", normalized: "${normalizedType}"`
);
if (this.accountPlanTypeArray.includes(normalizedType)) { if (this.accountPlanTypeArray.includes(normalizedType)) {
planType = normalizedType; planType = normalizedType;
} }
@@ -297,20 +151,11 @@ export class CodexUsageService {
// Fall back to account planType if rate limits didn't have it // Fall back to account planType if rate limits didn't have it
if (planType === 'unknown' && accountResult.account?.planType) { if (planType === 'unknown' && accountResult.account?.planType) {
const normalizedType = accountResult.account.planType.toLowerCase() as CodexPlanType; const normalizedType = accountResult.account.planType.toLowerCase() as CodexPlanType;
logger.info(
`[fetchFromAppServer] Fallback to account planType: "${accountResult.account.planType}", normalized: "${normalizedType}"`
);
if (this.accountPlanTypeArray.includes(normalizedType)) { if (this.accountPlanTypeArray.includes(normalizedType)) {
planType = normalizedType; planType = normalizedType;
} }
} }
if (planType === 'unknown') {
logger.info('[fetchFromAppServer] No planType found in either response');
} else {
logger.info(`[fetchFromAppServer] Final planType: ${planType}`);
}
const result: CodexUsageData = { const result: CodexUsageData = {
rateLimits: { rateLimits: {
planType, planType,
@@ -325,10 +170,6 @@ export class CodexUsageService {
// Add rate limit info if available // Add rate limit info if available
if (rateLimitsResult?.rateLimits?.primary) { if (rateLimitsResult?.rateLimits?.primary) {
const primary = rateLimitsResult.rateLimits.primary; const primary = rateLimitsResult.rateLimits.primary;
logger.info(
'[fetchFromAppServer] Adding primary rate limit:',
JSON.stringify(primary, null, 2)
);
result.rateLimits!.primary = { result.rateLimits!.primary = {
limit: 100, // Not provided by API, using placeholder limit: 100, // Not provided by API, using placeholder
used: primary.usedPercent, used: primary.usedPercent,
@@ -337,17 +178,11 @@ export class CodexUsageService {
windowDurationMins: primary.windowDurationMins, windowDurationMins: primary.windowDurationMins,
resetsAt: primary.resetsAt, resetsAt: primary.resetsAt,
}; };
} else {
logger.info('[fetchFromAppServer] No primary rate limit in result');
} }
// Add secondary rate limit if available // Add secondary rate limit if available
if (rateLimitsResult?.rateLimits?.secondary) { if (rateLimitsResult?.rateLimits?.secondary) {
const secondary = rateLimitsResult.rateLimits.secondary; const secondary = rateLimitsResult.rateLimits.secondary;
logger.info(
'[fetchFromAppServer] Adding secondary rate limit:',
JSON.stringify(secondary, null, 2)
);
result.rateLimits!.secondary = { result.rateLimits!.secondary = {
limit: 100, limit: 100,
used: secondary.usedPercent, used: secondary.usedPercent,
@@ -358,17 +193,13 @@ export class CodexUsageService {
}; };
} }
logger.info('[fetchFromAppServer] Final result:', JSON.stringify(result, null, 2)); logger.info(
`[fetchFromAppServer] ✓ Plan: ${planType}, Primary: ${result.rateLimits?.primary?.usedPercent || 'N/A'}%, Secondary: ${result.rateLimits?.secondary?.usedPercent || 'N/A'}%`
);
return result; return result;
} catch (error) { } catch (error) {
// App-server method failed, will fall back to other methods logger.error('[fetchFromAppServer] Failed:', error);
logger.error('Failed to fetch from app-server:', error);
return null; return null;
} finally {
// Ensure process is killed
if (childProcess && !childProcess.killed) {
childProcess.kill('SIGTERM');
}
} }
} }
@@ -383,7 +214,7 @@ export class CodexUsageService {
const exists = systemPathExists(authFilePath); const exists = systemPathExists(authFilePath);
if (!exists) { if (!exists) {
logger.info('[getPlanTypeFromAuthFile] Auth file does not exist'); logger.warn('[getPlanTypeFromAuthFile] Auth file does not exist');
return 'unknown'; return 'unknown';
} }

View File

@@ -9,7 +9,9 @@ import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store'; import { useSetupStore } from '@/store/setup-store';
import { getModelProvider, PROVIDER_PREFIXES, stripProviderPrefix } from '@automaker/types'; import { getModelProvider, PROVIDER_PREFIXES, stripProviderPrefix } from '@automaker/types';
import type { ModelProvider } 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 { interface ModelSelectorProps {
selectedModel: string; // Can be ModelAlias or "cursor-{id}" selectedModel: string; // Can be ModelAlias or "cursor-{id}"
@@ -22,7 +24,14 @@ export function ModelSelector({
onModelSelect, onModelSelect,
testIdPrefix = 'model-select', testIdPrefix = 'model-select',
}: ModelSelectorProps) { }: ModelSelectorProps) {
const { enabledCursorModels, cursorDefaultModel } = useAppStore(); const {
enabledCursorModels,
cursorDefaultModel,
codexModels,
codexModelsLoading,
codexModelsError,
fetchCodexModels,
} = useAppStore();
const { cursorCliStatus, codexCliStatus } = useSetupStore(); const { cursorCliStatus, codexCliStatus } = useSetupStore();
const selectedProvider = getModelProvider(selectedModel); const selectedProvider = getModelProvider(selectedModel);
@@ -33,6 +42,31 @@ export function ModelSelector({
// Check if Codex CLI is available // Check if Codex CLI is available
const isCodexAvailable = codexCliStatus?.installed && codexCliStatus?.auth?.authenticated; 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 // Filter Cursor models based on enabled models from global settings
const filteredCursorModels = CURSOR_MODELS.filter((model) => { const filteredCursorModels = CURSOR_MODELS.filter((model) => {
// Extract the cursor model ID from the prefixed ID (e.g., "cursor-auto" -> "auto") // 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) // Switch to Cursor's default model (from global settings)
onModelSelect(`${PROVIDER_PREFIXES.cursor}${cursorDefaultModel}`); onModelSelect(`${PROVIDER_PREFIXES.cursor}${cursorDefaultModel}`);
} else if (provider === 'codex' && selectedProvider !== 'codex') { } else if (provider === 'codex' && selectedProvider !== 'codex') {
// Switch to Codex's default model (codex-gpt-5.2-codex) // Switch to Codex's default model (use isDefault flag from dynamic models)
onModelSelect('codex-gpt-5.2-codex'); 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') { } else if (provider === 'claude' && selectedProvider !== 'claude') {
// Switch to Claude's default model // Switch to Claude's default model
onModelSelect('sonnet'); onModelSelect('sonnet');
@@ -234,8 +270,42 @@ export function ModelSelector({
CLI CLI
</span> </span>
</div> </div>
{/* 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
type="button"
onClick={() => fetchCodexModels(true)}
className="text-xs text-red-400 underline hover:no-underline"
>
Retry
</button>
</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"> <div className="flex flex-col gap-2">
{CODEX_MODELS.map((option) => { {dynamicCodexModels.map((option) => {
const isSelected = selectedModel === option.id; const isSelected = selectedModel === option.id;
return ( return (
<button <button
@@ -252,6 +322,20 @@ export function ModelSelector({
data-testid={`${testIdPrefix}-${option.id}`} data-testid={`${testIdPrefix}-${option.id}`}
> >
<span>{option.label}</span> <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 && ( {option.badge && (
<Badge <Badge
variant="outline" variant="outline"
@@ -265,10 +349,12 @@ export function ModelSelector({
{option.badge} {option.badge}
</Badge> </Badge>
)} )}
</div>
</button> </button>
); );
})} })}
</div> </div>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -1,4 +1,4 @@
import * as React from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
import type { import type {
@@ -8,8 +8,6 @@ import type {
OpencodeModelId, OpencodeModelId,
GroupedModel, GroupedModel,
PhaseModelEntry, PhaseModelEntry,
ThinkingLevel,
ReasoningEffort,
} from '@automaker/types'; } from '@automaker/types';
import { import {
stripProviderPrefix, stripProviderPrefix,
@@ -17,13 +15,11 @@ import {
getModelGroup, getModelGroup,
isGroupSelected, isGroupSelected,
getSelectedVariant, getSelectedVariant,
isCursorModel,
codexModelHasThinking, codexModelHasThinking,
} from '@automaker/types'; } from '@automaker/types';
import { import {
CLAUDE_MODELS, CLAUDE_MODELS,
CURSOR_MODELS, CURSOR_MODELS,
CODEX_MODELS,
OPENCODE_MODELS, OPENCODE_MODELS,
THINKING_LEVELS, THINKING_LEVELS,
THINKING_LEVEL_LABELS, THINKING_LEVEL_LABELS,
@@ -73,23 +69,39 @@ export function PhaseModelSelector({
align = 'end', align = 'end',
disabled = false, disabled = false,
}: PhaseModelSelectorProps) { }: PhaseModelSelectorProps) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = useState(false);
const [expandedGroup, setExpandedGroup] = React.useState<string | null>(null); const [expandedGroup, setExpandedGroup] = useState<string | null>(null);
const [expandedClaudeModel, setExpandedClaudeModel] = React.useState<ModelAlias | null>(null); const [expandedClaudeModel, setExpandedClaudeModel] = useState<ModelAlias | null>(null);
const [expandedCodexModel, setExpandedCodexModel] = React.useState<CodexModelId | null>(null); const [expandedCodexModel, setExpandedCodexModel] = useState<CodexModelId | null>(null);
const commandListRef = React.useRef<HTMLDivElement>(null); const commandListRef = useRef<HTMLDivElement>(null);
const expandedTriggerRef = React.useRef<HTMLDivElement>(null); const expandedTriggerRef = useRef<HTMLDivElement>(null);
const expandedClaudeTriggerRef = React.useRef<HTMLDivElement>(null); const expandedClaudeTriggerRef = useRef<HTMLDivElement>(null);
const expandedCodexTriggerRef = React.useRef<HTMLDivElement>(null); const expandedCodexTriggerRef = useRef<HTMLDivElement>(null);
const { enabledCursorModels, favoriteModels, toggleFavoriteModel } = useAppStore(); const {
enabledCursorModels,
favoriteModels,
toggleFavoriteModel,
codexModels,
codexModelsLoading,
fetchCodexModels,
} = useAppStore();
// Extract model and thinking/reasoning levels from value // Extract model and thinking/reasoning levels from value
const selectedModel = value.model; const selectedModel = value.model;
const selectedThinkingLevel = value.thinkingLevel || 'none'; const selectedThinkingLevel = value.thinkingLevel || 'none';
const selectedReasoningEffort = value.reasoningEffort || '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 // Close expanded group when trigger scrolls out of view
React.useEffect(() => { useEffect(() => {
const triggerElement = expandedTriggerRef.current; const triggerElement = expandedTriggerRef.current;
const listElement = commandListRef.current; const listElement = commandListRef.current;
if (!triggerElement || !listElement || !expandedGroup) return; if (!triggerElement || !listElement || !expandedGroup) return;
@@ -112,7 +124,7 @@ export function PhaseModelSelector({
}, [expandedGroup]); }, [expandedGroup]);
// Close expanded Claude model popover when trigger scrolls out of view // Close expanded Claude model popover when trigger scrolls out of view
React.useEffect(() => { useEffect(() => {
const triggerElement = expandedClaudeTriggerRef.current; const triggerElement = expandedClaudeTriggerRef.current;
const listElement = commandListRef.current; const listElement = commandListRef.current;
if (!triggerElement || !listElement || !expandedClaudeModel) return; if (!triggerElement || !listElement || !expandedClaudeModel) return;
@@ -135,7 +147,7 @@ export function PhaseModelSelector({
}, [expandedClaudeModel]); }, [expandedClaudeModel]);
// Close expanded Codex model popover when trigger scrolls out of view // Close expanded Codex model popover when trigger scrolls out of view
React.useEffect(() => { useEffect(() => {
const triggerElement = expandedCodexTriggerRef.current; const triggerElement = expandedCodexTriggerRef.current;
const listElement = commandListRef.current; const listElement = commandListRef.current;
if (!triggerElement || !listElement || !expandedCodexModel) return; if (!triggerElement || !listElement || !expandedCodexModel) return;
@@ -157,6 +169,17 @@ export function PhaseModelSelector({
return () => observer.disconnect(); return () => observer.disconnect();
}, [expandedCodexModel]); }, [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 // Filter Cursor models to only show enabled ones
const availableCursorModels = CURSOR_MODELS.filter((model) => { const availableCursorModels = CURSOR_MODELS.filter((model) => {
const cursorId = stripProviderPrefix(model.id) as CursorModelId; const cursorId = stripProviderPrefix(model.id) as CursorModelId;
@@ -164,7 +187,7 @@ export function PhaseModelSelector({
}); });
// Helper to find current selected model details // Helper to find current selected model details
const currentModel = React.useMemo(() => { const currentModel = useMemo(() => {
const claudeModel = CLAUDE_MODELS.find((m) => m.id === selectedModel); const claudeModel = CLAUDE_MODELS.find((m) => m.id === selectedModel);
if (claudeModel) { if (claudeModel) {
// Add thinking level to label if not 'none' // Add thinking level to label if not 'none'
@@ -198,7 +221,7 @@ export function PhaseModelSelector({
} }
// Check Codex models // 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 }; if (codexModel) return { ...codexModel, icon: OpenAIIcon };
// Check OpenCode models // Check OpenCode models
@@ -206,10 +229,10 @@ export function PhaseModelSelector({
if (opencodeModel) return { ...opencodeModel, icon: OpenCodeIcon }; if (opencodeModel) return { ...opencodeModel, icon: OpenCodeIcon };
return null; return null;
}, [selectedModel, selectedThinkingLevel, availableCursorModels]); }, [selectedModel, selectedThinkingLevel, availableCursorModels, transformedCodexModels]);
// Compute grouped vs standalone Cursor models // Compute grouped vs standalone Cursor models
const { groupedModels, standaloneCursorModels } = React.useMemo(() => { const { groupedModels, standaloneCursorModels } = useMemo(() => {
const grouped: GroupedModel[] = []; const grouped: GroupedModel[] = [];
const standalone: typeof CURSOR_MODELS = []; const standalone: typeof CURSOR_MODELS = [];
const seenGroups = new Set<string>(); const seenGroups = new Set<string>();
@@ -242,11 +265,11 @@ export function PhaseModelSelector({
}, [availableCursorModels, enabledCursorModels]); }, [availableCursorModels, enabledCursorModels]);
// Group models // Group models
const { favorites, claude, cursor, codex, opencode } = React.useMemo(() => { const { favorites, claude, cursor, codex, opencode } = useMemo(() => {
const favs: typeof CLAUDE_MODELS = []; const favs: typeof CLAUDE_MODELS = [];
const cModels: typeof CLAUDE_MODELS = []; const cModels: typeof CLAUDE_MODELS = [];
const curModels: typeof CURSOR_MODELS = []; const curModels: typeof CURSOR_MODELS = [];
const codModels: typeof CODEX_MODELS = []; const codModels: typeof transformedCodexModels = [];
const ocModels: typeof OPENCODE_MODELS = []; const ocModels: typeof OPENCODE_MODELS = [];
// Process Claude Models // Process Claude Models
@@ -268,7 +291,7 @@ export function PhaseModelSelector({
}); });
// Process Codex Models // Process Codex Models
CODEX_MODELS.forEach((model) => { transformedCodexModels.forEach((model) => {
if (favoriteModels.includes(model.id)) { if (favoriteModels.includes(model.id)) {
favs.push(model); favs.push(model);
} else { } else {
@@ -292,10 +315,10 @@ export function PhaseModelSelector({
codex: codModels, codex: codModels,
opencode: ocModels, opencode: ocModels,
}; };
}, [favoriteModels, availableCursorModels]); }, [favoriteModels, availableCursorModels, transformedCodexModels]);
// Render Codex model item with secondary popover for reasoning effort (only for models that support it) // 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 isSelected = selectedModel === model.id;
const isFavorite = favoriteModels.includes(model.id); const isFavorite = favoriteModels.includes(model.id);
const hasReasoning = codexModelHasThinking(model.id as CodexModelId); const hasReasoning = codexModelHasThinking(model.id as CodexModelId);
@@ -919,7 +942,7 @@ export function PhaseModelSelector({
} }
// Codex model // Codex model
if (model.provider === 'codex') { if (model.provider === 'codex') {
return renderCodexModelItem(model); return renderCodexModelItem(model as (typeof transformedCodexModels)[0]);
} }
// OpenCode model // OpenCode model
if (model.provider === 'opencode') { if (model.provider === 'opencode') {

View File

@@ -727,6 +727,20 @@ export interface ElectronAPI {
ideation?: IdeationAPI; ideation?: IdeationAPI;
codex?: { codex?: {
getUsage: () => Promise<CodexUsageResponse>; 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?: { settings?: {
getStatus: () => Promise<{ getStatus: () => Promise<{

View File

@@ -2056,6 +2056,25 @@ export class HttpApiClient implements ElectronAPI {
// Codex API // Codex API
codex = { codex = {
getUsage: (): Promise<CodexUsageResponse> => this.get('/api/codex/usage'), 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 // Context API

View File

@@ -68,8 +68,9 @@ function RootLayoutContent() {
getEffectiveTheme, getEffectiveTheme,
skipSandboxWarning, skipSandboxWarning,
setSkipSandboxWarning, setSkipSandboxWarning,
fetchCodexModels,
} = useAppStore(); } = useAppStore();
const { setupComplete } = useSetupStore(); const { setupComplete, codexCliStatus } = useSetupStore();
const navigate = useNavigate(); const navigate = useNavigate();
const [isMounted, setIsMounted] = useState(false); const [isMounted, setIsMounted] = useState(false);
const [streamerPanelOpen, setStreamerPanelOpen] = useState(false); const [streamerPanelOpen, setStreamerPanelOpen] = useState(false);
@@ -431,6 +432,20 @@ function RootLayoutContent() {
} }
}, [isMounted, currentProject, location.pathname, navigate]); }, [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 // Apply theme class to document - use deferred value to avoid blocking UI
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;

View File

@@ -1,6 +1,7 @@
import { create } from 'zustand'; import { create } from 'zustand';
// Note: persist middleware removed - settings now sync via API (use-settings-sync.ts) // Note: persist middleware removed - settings now sync via API (use-settings-sync.ts)
import type { Project, TrashedProject } from '@/lib/electron'; import type { Project, TrashedProject } from '@/lib/electron';
import { getElectronAPI } from '@/lib/electron';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import { setItem, getItem } from '@/lib/storage'; import { setItem, getItem } from '@/lib/storage';
import type { import type {
@@ -652,6 +653,20 @@ export interface AppState {
codexUsage: CodexUsage | null; codexUsage: CodexUsage | null;
codexUsageLastUpdated: number | 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) // Pipeline Configuration (per-project, keyed by project path)
pipelineConfigByProject: Record<string, PipelineConfig>; pipelineConfigByProject: Record<string, PipelineConfig>;
@@ -1093,6 +1108,20 @@ export interface AppActions {
// Codex Usage Tracking actions // Codex Usage Tracking actions
setCodexUsage: (usage: CodexUsage | null) => void; 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
reset: () => void; reset: () => void;
} }
@@ -1233,6 +1262,10 @@ const initialState: AppState = {
claudeUsageLastUpdated: null, claudeUsageLastUpdated: null,
codexUsage: null, codexUsage: null,
codexUsageLastUpdated: null, codexUsageLastUpdated: null,
codexModels: [],
codexModelsLoading: false,
codexModelsError: null,
codexModelsLastFetched: null,
pipelineConfigByProject: {}, pipelineConfigByProject: {},
// UI State (previously in localStorage, now synced via API) // UI State (previously in localStorage, now synced via API)
worktreePanelCollapsed: false, worktreePanelCollapsed: false,
@@ -3016,6 +3049,53 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
codexUsageLastUpdated: usage ? Date.now() : null, 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 // Pipeline actions
setPipelineConfig: (projectPath, config) => { setPipelineConfig: (projectPath, config) => {
set({ set({

View File

@@ -0,0 +1,94 @@
/**
* 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;
credits?: AppServerCredits;
planType?: string;
}
export interface AppServerRateLimitWindow {
usedPercent: number;
windowDurationMins: number;
resetsAt: number;
}
export interface AppServerCredits {
hasCredits: boolean;
unlimited: boolean;
balance: string;
}
/**
* 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;
};
}

View File

@@ -30,6 +30,21 @@ export type {
} from './codex.js'; } from './codex.js';
export * from './codex-models.js'; export * from './codex-models.js';
// Codex App-Server JSON-RPC types
export type {
AppServerModelResponse,
AppServerModel,
AppServerReasoningEffort,
AppServerAccountResponse,
AppServerAccount,
AppServerRateLimitsResponse,
AppServerRateLimits,
AppServerRateLimitWindow,
AppServerCredits,
JsonRpcRequest,
JsonRpcResponse,
} from './codex-app-server.js';
// Feature types // Feature types
export type { export type {
Feature, Feature,

103
package-lock.json generated
View File

@@ -29,7 +29,7 @@
}, },
"apps/server": { "apps/server": {
"name": "@automaker/server", "name": "@automaker/server",
"version": "0.8.0", "version": "0.9.0",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "0.1.76", "@anthropic-ai/claude-agent-sdk": "0.1.76",
@@ -80,7 +80,7 @@
}, },
"apps/ui": { "apps/ui": {
"name": "@automaker/ui", "name": "@automaker/ui",
"version": "0.8.0", "version": "0.9.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
@@ -677,6 +677,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@@ -1260,6 +1261,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz",
"integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==", "integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.5.0", "@codemirror/state": "^6.5.0",
"crelt": "^1.0.6", "crelt": "^1.0.6",
@@ -1302,6 +1304,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
@@ -2122,7 +2125,6 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"cross-dirname": "^0.1.0", "cross-dirname": "^0.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -2144,7 +2146,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
@@ -2161,7 +2162,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"universalify": "^2.0.0" "universalify": "^2.0.0"
}, },
@@ -2176,7 +2176,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
@@ -2944,7 +2943,6 @@
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -3069,7 +3067,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"funding": { "funding": {
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
@@ -3086,7 +3083,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"funding": { "funding": {
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
@@ -3103,7 +3099,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"funding": { "funding": {
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
@@ -3212,7 +3207,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
}, },
@@ -3235,7 +3229,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
}, },
@@ -3258,7 +3251,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "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", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@emnapi/runtime": "^1.7.0" "@emnapi/runtime": "^1.7.0"
}, },
@@ -3367,7 +3358,6 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
}, },
@@ -3387,7 +3377,6 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
}, },
@@ -3787,8 +3776,7 @@
"version": "16.0.10", "version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz",
"integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==", "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "16.0.10", "version": "16.0.10",
@@ -3802,7 +3790,6 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
@@ -3819,7 +3806,6 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
@@ -3836,7 +3822,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
@@ -3853,7 +3838,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
@@ -3870,7 +3854,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
@@ -3887,7 +3870,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
@@ -3904,7 +3886,6 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
@@ -3921,7 +3902,6 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
@@ -4021,6 +4001,7 @@
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"playwright": "1.57.0" "playwright": "1.57.0"
}, },
@@ -5461,7 +5442,6 @@
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
@@ -5795,6 +5775,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.6.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.6.tgz",
"integrity": "sha512-qWFxi2D6eGc1L03RzUuhyEOplZ7Q6q62YOl7Of9Y0q4YjwQwxRm4zxwDVtvUIoy4RLVCpqp5UoE+Nxv2PY9trg==", "integrity": "sha512-qWFxi2D6eGc1L03RzUuhyEOplZ7Q6q62YOl7Of9Y0q4YjwQwxRm4zxwDVtvUIoy4RLVCpqp5UoE+Nxv2PY9trg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@tanstack/history": "1.141.0", "@tanstack/history": "1.141.0",
"@tanstack/react-store": "^0.8.0", "@tanstack/react-store": "^0.8.0",
@@ -6221,6 +6202,7 @@
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/body-parser": "*", "@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0", "@types/express-serve-static-core": "^5.0.0",
@@ -6363,6 +6345,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -6373,6 +6356,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@@ -6478,6 +6462,7 @@
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/types": "8.50.0", "@typescript-eslint/types": "8.50.0",
@@ -6971,7 +6956,8 @@
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/@xyflow/react": { "node_modules/@xyflow/react": {
"version": "12.10.0", "version": "12.10.0",
@@ -7069,6 +7055,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -7129,6 +7116,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -7727,6 +7715,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -8258,8 +8247,7 @@
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/cliui": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
@@ -8564,8 +8552,7 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true
"peer": true
}, },
"node_modules/cross-env": { "node_modules/cross-env": {
"version": "10.1.0", "version": "10.1.0",
@@ -8662,6 +8649,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC", "license": "ISC",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@@ -8963,6 +8951,7 @@
"integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"app-builder-lib": "26.0.12", "app-builder-lib": "26.0.12",
"builder-util": "26.0.11", "builder-util": "26.0.11",
@@ -9289,7 +9278,6 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.1", "@electron/asar": "^3.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -9310,7 +9298,6 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0", "jsonfile": "^4.0.0",
@@ -9561,6 +9548,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -9875,6 +9863,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.1", "body-parser": "^2.2.1",
@@ -11542,7 +11531,6 @@
"os": [ "os": [
"android" "android"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11564,7 +11552,6 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11586,7 +11573,6 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11608,7 +11594,6 @@
"os": [ "os": [
"freebsd" "freebsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11630,7 +11615,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11652,7 +11636,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11674,7 +11657,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11696,7 +11678,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11718,7 +11699,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11740,7 +11720,6 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -11762,7 +11741,6 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@@ -14050,7 +14028,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.6", "nanoid": "^3.3.6",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
@@ -14067,7 +14044,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"commander": "^9.4.0" "commander": "^9.4.0"
}, },
@@ -14085,7 +14061,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
@@ -14274,6 +14249,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -14283,6 +14259,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -14641,7 +14618,6 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported", "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^7.1.3"
}, },
@@ -14830,6 +14806,7 @@
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.0.tgz", "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.0.tgz",
"integrity": "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==", "integrity": "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
@@ -14878,7 +14855,6 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@img/colour": "^1.0.0", "@img/colour": "^1.0.0",
"detect-libc": "^2.1.2", "detect-libc": "^2.1.2",
@@ -14929,7 +14905,6 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
}, },
@@ -14952,7 +14927,6 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
}, },
@@ -14975,7 +14949,6 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"funding": { "funding": {
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
@@ -14992,7 +14965,6 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"funding": { "funding": {
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
@@ -15009,7 +14981,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"funding": { "funding": {
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
@@ -15026,7 +14997,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"funding": { "funding": {
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
@@ -15043,7 +15013,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"funding": { "funding": {
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
@@ -15060,7 +15029,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"funding": { "funding": {
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
@@ -15077,7 +15045,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"funding": { "funding": {
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
@@ -15094,7 +15061,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
}, },
@@ -15117,7 +15083,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
}, },
@@ -15140,7 +15105,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
}, },
@@ -15163,7 +15127,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
}, },
@@ -15186,7 +15149,6 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
}, },
@@ -15209,7 +15171,6 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0" "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", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"client-only": "0.0.1" "client-only": "0.0.1"
}, },
@@ -15848,7 +15808,6 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"rimraf": "~2.6.2" "rimraf": "~2.6.2"
@@ -15912,7 +15871,6 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"minimist": "^1.2.6" "minimist": "^1.2.6"
}, },
@@ -16010,6 +15968,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -16214,6 +16173,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -16585,6 +16545,7 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.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", "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/vite/node_modules/fdir": { "node_modules/vite/node_modules/fdir": {
"version": "6.5.0", "version": "6.5.0",
@@ -16700,6 +16662,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -16742,6 +16705,7 @@
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vitest/expect": "4.0.16", "@vitest/expect": "4.0.16",
"@vitest/mocker": "4.0.16", "@vitest/mocker": "4.0.16",
@@ -17067,6 +17031,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }