Merge branch 'v0.9.0rc' into feat/subagents-skills

This commit is contained in:
webdevcody
2026-01-08 00:33:30 -05:00
218 changed files with 18537 additions and 4390 deletions

View File

@@ -12,5 +12,6 @@ export {
getAncestors,
formatAncestorContextForPrompt,
type DependencyResolutionResult,
type DependencySatisfactionOptions,
type AncestorContext,
} from './resolver.js';

View File

@@ -174,21 +174,40 @@ function detectCycles(features: Feature[], featureMap: Map<string, Feature>): st
return cycles;
}
export interface DependencySatisfactionOptions {
/** If true, only require dependencies to not be 'running' (ignore verification requirement) */
skipVerification?: boolean;
}
/**
* Checks if a feature's dependencies are satisfied (all complete or verified)
*
* @param feature - Feature to check
* @param allFeatures - All features in the project
* @param options - Optional configuration for dependency checking
* @returns true if all dependencies are satisfied, false otherwise
*/
export function areDependenciesSatisfied(feature: Feature, allFeatures: Feature[]): boolean {
export function areDependenciesSatisfied(
feature: Feature,
allFeatures: Feature[],
options?: DependencySatisfactionOptions
): boolean {
if (!feature.dependencies || feature.dependencies.length === 0) {
return true; // No dependencies = always ready
}
const skipVerification = options?.skipVerification ?? false;
return feature.dependencies.every((depId: string) => {
const dep = allFeatures.find((f) => f.id === depId);
return dep && (dep.status === 'completed' || dep.status === 'verified');
if (!dep) return false;
if (skipVerification) {
// When skipping verification, only block if dependency is currently running
return dep.status !== 'running';
}
// Default: require 'completed' or 'verified'
return dep.status === 'completed' || dep.status === 'verified';
});
}

View File

@@ -11,6 +11,7 @@
import {
CLAUDE_MODEL_MAP,
CURSOR_MODEL_MAP,
CODEX_MODEL_MAP,
DEFAULT_MODELS,
PROVIDER_PREFIXES,
isCursorModel,
@@ -19,6 +20,11 @@ import {
type ThinkingLevel,
} from '@automaker/types';
// Pattern definitions for Codex/OpenAI models
const CODEX_MODEL_PREFIXES = ['gpt-'];
const OPENAI_O_SERIES_PATTERN = /^o\d/;
const OPENAI_O_SERIES_ALLOWED_MODELS = new Set<string>();
/**
* Resolve a model key/alias to a full model string
*
@@ -56,16 +62,6 @@ export function resolveModelString(
return modelKey;
}
// Check if it's a bare Cursor model ID (e.g., "composer-1", "auto", "gpt-4o")
if (modelKey in CURSOR_MODEL_MAP) {
// Return with cursor- prefix so provider routing works correctly
const prefixedModel = `${PROVIDER_PREFIXES.cursor}${modelKey}`;
console.log(
`[ModelResolver] Detected bare Cursor model ID: "${modelKey}" -> "${prefixedModel}"`
);
return prefixedModel;
}
// Full Claude model string - pass through unchanged
if (modelKey.includes('claude-')) {
console.log(`[ModelResolver] Using full Claude model string: ${modelKey}`);
@@ -79,6 +75,27 @@ export function resolveModelString(
return resolved;
}
// OpenAI/Codex models - check BEFORE bare Cursor models since they overlap
// (Cursor supports gpt models, but bare "gpt-*" should route to Codex)
if (
CODEX_MODEL_PREFIXES.some((prefix) => modelKey.startsWith(prefix)) ||
(OPENAI_O_SERIES_PATTERN.test(modelKey) && OPENAI_O_SERIES_ALLOWED_MODELS.has(modelKey))
) {
console.log(`[ModelResolver] Using OpenAI/Codex model: ${modelKey}`);
return modelKey;
}
// Check if it's a bare Cursor model ID (e.g., "composer-1", "auto", "gpt-4o")
// Note: This is checked AFTER Codex check to prioritize Codex for bare gpt-* models
if (modelKey in CURSOR_MODEL_MAP) {
// Return with cursor- prefix so provider routing works correctly
const prefixedModel = `${PROVIDER_PREFIXES.cursor}${modelKey}`;
console.log(
`[ModelResolver] Detected bare Cursor model ID: "${modelKey}" -> "${prefixedModel}"`
);
return prefixedModel;
}
// Unknown model key - use default
console.warn(`[ModelResolver] Unknown model key "${modelKey}", using default: "${defaultModel}"`);
return defaultModel;

View File

@@ -180,7 +180,7 @@ describe('model-resolver', () => {
it('should use custom default for unknown model key', () => {
const customDefault = 'claude-opus-4-20241113';
const result = resolveModelString('gpt-4', customDefault);
const result = resolveModelString('truly-unknown-model', customDefault);
expect(result).toBe(customDefault);
});

View File

@@ -93,6 +93,9 @@ export {
getClaudeSettingsPath,
getClaudeStatsCachePath,
getClaudeProjectsDir,
getCodexCliPaths,
getCodexConfigDir,
getCodexAuthPath,
getShellPaths,
getExtendedPath,
// Node.js paths
@@ -120,6 +123,9 @@ export {
findClaudeCliPath,
getClaudeAuthIndicators,
type ClaudeAuthIndicators,
findCodexCliPath,
getCodexAuthIndicators,
type CodexAuthIndicators,
// Electron userData operations
setElectronUserDataPath,
getElectronUserDataPath,

View File

@@ -44,11 +44,15 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
console.log(`[SubprocessManager] Passing ${stdinData.length} bytes via stdin`);
}
// On Windows, .cmd files must be run through shell (cmd.exe)
const needsShell = process.platform === 'win32' && command.toLowerCase().endsWith('.cmd');
const childProcess: ChildProcess = spawn(command, args, {
cwd,
env: processEnv,
// Use 'pipe' for stdin when we need to write data, otherwise 'ignore'
stdio: [stdinData ? 'pipe' : 'ignore', 'pipe', 'pipe'],
shell: needsShell,
});
// Write stdin data if provided
@@ -194,10 +198,14 @@ export async function spawnProcess(options: SubprocessOptions): Promise<Subproce
};
return new Promise((resolve, reject) => {
// On Windows, .cmd files must be run through shell (cmd.exe)
const needsShell = process.platform === 'win32' && command.toLowerCase().endsWith('.cmd');
const childProcess = spawn(command, args, {
cwd,
env: processEnv,
stdio: ['ignore', 'pipe', 'pipe'],
shell: needsShell,
});
let stdout = '';

View File

@@ -71,6 +71,131 @@ export function getClaudeCliPaths(): string[] {
];
}
/**
* Get NVM-installed Node.js bin paths for CLI tools
*/
function getNvmBinPaths(): string[] {
const nvmDir = process.env.NVM_DIR || path.join(os.homedir(), '.nvm');
const versionsDir = path.join(nvmDir, 'versions', 'node');
try {
if (!fsSync.existsSync(versionsDir)) {
return [];
}
const versions = fsSync.readdirSync(versionsDir);
return versions.map((version) => path.join(versionsDir, version, 'bin'));
} catch {
return [];
}
}
/**
* Get fnm (Fast Node Manager) installed Node.js bin paths
*/
function getFnmBinPaths(): string[] {
const homeDir = os.homedir();
const possibleFnmDirs = [
path.join(homeDir, '.local', 'share', 'fnm', 'node-versions'),
path.join(homeDir, '.fnm', 'node-versions'),
// macOS
path.join(homeDir, 'Library', 'Application Support', 'fnm', 'node-versions'),
];
const binPaths: string[] = [];
for (const fnmDir of possibleFnmDirs) {
try {
if (!fsSync.existsSync(fnmDir)) {
continue;
}
const versions = fsSync.readdirSync(fnmDir);
for (const version of versions) {
binPaths.push(path.join(fnmDir, version, 'installation', 'bin'));
}
} catch {
// Ignore errors for this directory
}
}
return binPaths;
}
/**
* Get common paths where Codex CLI might be installed
*/
export function getCodexCliPaths(): string[] {
const isWindows = process.platform === 'win32';
const homeDir = os.homedir();
if (isWindows) {
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local');
return [
path.join(homeDir, '.local', 'bin', 'codex.exe'),
path.join(appData, 'npm', 'codex.cmd'),
path.join(appData, 'npm', 'codex'),
path.join(appData, '.npm-global', 'bin', 'codex.cmd'),
path.join(appData, '.npm-global', 'bin', 'codex'),
// Volta on Windows
path.join(homeDir, '.volta', 'bin', 'codex.exe'),
// pnpm on Windows
path.join(localAppData, 'pnpm', 'codex.cmd'),
path.join(localAppData, 'pnpm', 'codex'),
];
}
// Include NVM bin paths for codex installed via npm global under NVM
const nvmBinPaths = getNvmBinPaths().map((binPath) => path.join(binPath, 'codex'));
// Include fnm bin paths
const fnmBinPaths = getFnmBinPaths().map((binPath) => path.join(binPath, 'codex'));
// pnpm global bin path
const pnpmHome = process.env.PNPM_HOME || path.join(homeDir, '.local', 'share', 'pnpm');
return [
// Standard locations
path.join(homeDir, '.local', 'bin', 'codex'),
'/opt/homebrew/bin/codex',
'/usr/local/bin/codex',
'/usr/bin/codex',
path.join(homeDir, '.npm-global', 'bin', 'codex'),
// Linuxbrew
'/home/linuxbrew/.linuxbrew/bin/codex',
// Volta
path.join(homeDir, '.volta', 'bin', 'codex'),
// pnpm global
path.join(pnpmHome, 'codex'),
// Yarn global
path.join(homeDir, '.yarn', 'bin', 'codex'),
path.join(homeDir, '.config', 'yarn', 'global', 'node_modules', '.bin', 'codex'),
// Snap packages
'/snap/bin/codex',
// NVM paths
...nvmBinPaths,
// fnm paths
...fnmBinPaths,
];
}
const CODEX_CONFIG_DIR_NAME = '.codex';
const CODEX_AUTH_FILENAME = 'auth.json';
const CODEX_TOKENS_KEY = 'tokens';
/**
* Get the Codex configuration directory path
*/
export function getCodexConfigDir(): string {
return path.join(os.homedir(), CODEX_CONFIG_DIR_NAME);
}
/**
* Get path to Codex auth file
*/
export function getCodexAuthPath(): string {
return path.join(getCodexConfigDir(), CODEX_AUTH_FILENAME);
}
/**
* Get the Claude configuration directory path
*/
@@ -413,6 +538,11 @@ function getAllAllowedSystemPaths(): string[] {
getClaudeSettingsPath(),
getClaudeStatsCachePath(),
getClaudeProjectsDir(),
// Codex CLI paths
...getCodexCliPaths(),
// Codex config directory and files
getCodexConfigDir(),
getCodexAuthPath(),
// Shell paths
...getShellPaths(),
// Node.js system paths
@@ -432,6 +562,8 @@ function getAllAllowedSystemDirs(): string[] {
// Claude config
getClaudeConfigDir(),
getClaudeProjectsDir(),
// Codex config
getCodexConfigDir(),
// Version managers (need recursive access for version directories)
...getNvmPaths(),
...getFnmPaths(),
@@ -740,6 +872,10 @@ export async function findClaudeCliPath(): Promise<string | null> {
return findFirstExistingPath(getClaudeCliPaths());
}
export async function findCodexCliPath(): Promise<string | null> {
return findFirstExistingPath(getCodexCliPaths());
}
/**
* Get Claude authentication status by checking various indicators
*/
@@ -800,8 +936,14 @@ export async function getClaudeAuthIndicators(): Promise<ClaudeAuthIndicators> {
const content = await systemPathReadFile(credPath);
const credentials = JSON.parse(content);
result.hasCredentialsFile = true;
// Support multiple credential formats:
// 1. Claude Code CLI format: { claudeAiOauth: { accessToken, refreshToken } }
// 2. Legacy format: { oauth_token } or { access_token }
// 3. API key format: { api_key }
const hasClaudeOauth = !!credentials.claudeAiOauth?.accessToken;
const hasLegacyOauth = !!(credentials.oauth_token || credentials.access_token);
result.credentials = {
hasOAuthToken: !!(credentials.oauth_token || credentials.access_token),
hasOAuthToken: hasClaudeOauth || hasLegacyOauth,
hasApiKey: !!credentials.api_key,
};
break;
@@ -812,3 +954,56 @@ export async function getClaudeAuthIndicators(): Promise<ClaudeAuthIndicators> {
return result;
}
export interface CodexAuthIndicators {
hasAuthFile: boolean;
hasOAuthToken: boolean;
hasApiKey: boolean;
}
const CODEX_OAUTH_KEYS = ['access_token', 'oauth_token'] as const;
const CODEX_API_KEY_KEYS = ['api_key', 'OPENAI_API_KEY'] as const;
function hasNonEmptyStringField(record: Record<string, unknown>, keys: readonly string[]): boolean {
return keys.some((key) => typeof record[key] === 'string' && record[key]);
}
function getNestedTokens(record: Record<string, unknown>): Record<string, unknown> | null {
const tokens = record[CODEX_TOKENS_KEY];
if (tokens && typeof tokens === 'object' && !Array.isArray(tokens)) {
return tokens as Record<string, unknown>;
}
return null;
}
export async function getCodexAuthIndicators(): Promise<CodexAuthIndicators> {
const result: CodexAuthIndicators = {
hasAuthFile: false,
hasOAuthToken: false,
hasApiKey: false,
};
try {
const authContent = await systemPathReadFile(getCodexAuthPath());
result.hasAuthFile = true;
try {
const authJson = JSON.parse(authContent) as Record<string, unknown>;
result.hasOAuthToken = hasNonEmptyStringField(authJson, CODEX_OAUTH_KEYS);
result.hasApiKey = hasNonEmptyStringField(authJson, CODEX_API_KEY_KEYS);
const nestedTokens = getNestedTokens(authJson);
if (nestedTokens) {
result.hasOAuthToken =
result.hasOAuthToken || hasNonEmptyStringField(nestedTokens, CODEX_OAUTH_KEYS);
result.hasApiKey =
result.hasApiKey || hasNonEmptyStringField(nestedTokens, CODEX_API_KEY_KEYS);
}
} catch {
// Ignore parse errors; file exists but contents are unreadable
}
} catch {
// Auth file not found or inaccessible
}
return result;
}

View File

@@ -284,11 +284,15 @@ describe('subprocess.ts', () => {
const generator = spawnJSONLProcess(options);
await collectAsyncGenerator(generator);
expect(cp.spawn).toHaveBeenCalledWith('my-command', ['--flag', 'value'], {
cwd: '/work/dir',
env: expect.objectContaining({ CUSTOM_VAR: 'test' }),
stdio: ['ignore', 'pipe', 'pipe'],
});
expect(cp.spawn).toHaveBeenCalledWith(
'my-command',
['--flag', 'value'],
expect.objectContaining({
cwd: '/work/dir',
env: expect.objectContaining({ CUSTOM_VAR: 'test' }),
stdio: ['ignore', 'pipe', 'pipe'],
})
);
});
it('should merge env with process.env', async () => {
@@ -473,11 +477,15 @@ describe('subprocess.ts', () => {
await spawnProcess(options);
expect(cp.spawn).toHaveBeenCalledWith('my-cmd', ['--verbose'], {
cwd: '/my/dir',
env: expect.objectContaining({ MY_VAR: 'value' }),
stdio: ['ignore', 'pipe', 'pipe'],
});
expect(cp.spawn).toHaveBeenCalledWith(
'my-cmd',
['--verbose'],
expect.objectContaining({
cwd: '/my/dir',
env: expect.objectContaining({ MY_VAR: 'value' }),
stdio: ['ignore', 'pipe', 'pipe'],
})
);
});
it('should handle empty stdout and stderr', async () => {

View File

@@ -0,0 +1,100 @@
/**
* Codex CLI Model IDs
* Based on OpenAI Codex CLI official models
* Reference: https://developers.openai.com/codex/models/
*/
export type CodexModelId =
| 'gpt-5.2-codex' // Most advanced agentic coding model for complex software engineering
| 'gpt-5-codex' // Purpose-built for Codex CLI with versatile tool use
| 'gpt-5-codex-mini' // Faster workflows optimized for low-latency code Q&A and editing
| 'codex-1' // Version of o3 optimized for software engineering
| 'codex-mini-latest' // Version of o4-mini for Codex, optimized for faster workflows
| 'gpt-5'; // GPT-5 base flagship model
/**
* Codex model metadata
*/
export interface CodexModelConfig {
id: CodexModelId;
label: string;
description: string;
hasThinking: boolean;
/** Whether the model supports vision/image inputs */
supportsVision: boolean;
}
/**
* Complete model map for Codex CLI
*/
export const CODEX_MODEL_CONFIG_MAP: Record<CodexModelId, CodexModelConfig> = {
'gpt-5.2-codex': {
id: 'gpt-5.2-codex',
label: 'GPT-5.2-Codex',
description: 'Most advanced agentic coding model for complex software engineering',
hasThinking: true,
supportsVision: true, // GPT-5 supports vision
},
'gpt-5-codex': {
id: 'gpt-5-codex',
label: 'GPT-5-Codex',
description: 'Purpose-built for Codex CLI with versatile tool use',
hasThinking: true,
supportsVision: true,
},
'gpt-5-codex-mini': {
id: 'gpt-5-codex-mini',
label: 'GPT-5-Codex-Mini',
description: 'Faster workflows optimized for low-latency code Q&A and editing',
hasThinking: false,
supportsVision: true,
},
'codex-1': {
id: 'codex-1',
label: 'Codex-1',
description: 'Version of o3 optimized for software engineering',
hasThinking: true,
supportsVision: true,
},
'codex-mini-latest': {
id: 'codex-mini-latest',
label: 'Codex-Mini-Latest',
description: 'Version of o4-mini for Codex, optimized for faster workflows',
hasThinking: false,
supportsVision: true,
},
'gpt-5': {
id: 'gpt-5',
label: 'GPT-5',
description: 'GPT-5 base flagship model',
hasThinking: true,
supportsVision: true,
},
};
/**
* Helper: Check if model has thinking capability
*/
export function codexModelHasThinking(modelId: CodexModelId): boolean {
return CODEX_MODEL_CONFIG_MAP[modelId]?.hasThinking ?? false;
}
/**
* Helper: Get display name for model
*/
export function getCodexModelLabel(modelId: CodexModelId): string {
return CODEX_MODEL_CONFIG_MAP[modelId]?.label ?? modelId;
}
/**
* Helper: Get all Codex model IDs
*/
export function getAllCodexModelIds(): CodexModelId[] {
return Object.keys(CODEX_MODEL_CONFIG_MAP) as CodexModelId[];
}
/**
* Helper: Check if Codex model supports vision
*/
export function codexModelSupportsVision(modelId: CodexModelId): boolean {
return CODEX_MODEL_CONFIG_MAP[modelId]?.supportsVision ?? true;
}

52
libs/types/src/codex.ts Normal file
View File

@@ -0,0 +1,52 @@
/** Sandbox modes for Codex CLI command execution */
export type CodexSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
/** Approval policies for Codex CLI tool execution */
export type CodexApprovalPolicy = 'untrusted' | 'on-failure' | 'on-request' | 'never';
/** Codex event types emitted by CLI */
export type CodexEventType =
| 'thread.started'
| 'turn.started'
| 'turn.completed'
| 'turn.failed'
| 'item.completed'
| 'error';
/** Codex item types in CLI events */
export type CodexItemType =
| 'agent_message'
| 'reasoning'
| 'command_execution'
| 'file_change'
| 'mcp_tool_call'
| 'web_search'
| 'plan_update';
/** Codex CLI event structure */
export interface CodexEvent {
type: CodexEventType;
thread_id?: string;
item?: {
type: CodexItemType;
content?: string;
[key: string]: unknown;
};
[key: string]: unknown;
}
/** Codex CLI configuration (stored in .automaker/codex-config.json) */
export interface CodexCliConfig {
/** Default model to use when not specified */
defaultModel?: string;
/** List of enabled models */
models?: string[];
}
/** Codex authentication status */
export interface CodexAuthStatus {
authenticated: boolean;
method: 'oauth' | 'api_key' | 'none';
hasCredentialsFile?: boolean;
error?: string;
}

View File

@@ -217,6 +217,7 @@ export interface CursorAuthStatus {
authenticated: boolean;
method: 'login' | 'api_key' | 'none';
hasCredentialsFile?: boolean;
error?: string;
}
/**

View File

@@ -4,6 +4,16 @@
import type { PlanningMode, ThinkingLevel } from './settings.js';
/**
* A single entry in the description history
*/
export interface DescriptionHistoryEntry {
description: string;
timestamp: string; // ISO date string
source: 'initial' | 'enhance' | 'edit'; // What triggered this version
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance'; // Only for 'enhance' source
}
export interface FeatureImagePath {
id: string;
path: string;
@@ -54,6 +64,7 @@ export interface Feature {
error?: string;
summary?: string;
startedAt?: string;
descriptionHistory?: DescriptionHistoryEntry[]; // History of description changes
[key: string]: unknown; // Keep catch-all for extensibility
}

View File

@@ -18,10 +18,26 @@ export type {
McpSSEServerConfig,
McpHttpServerConfig,
AgentDefinition,
ReasoningEffort,
} from './provider.js';
// Codex CLI types
export type {
CodexSandboxMode,
CodexApprovalPolicy,
CodexCliConfig,
CodexAuthStatus,
} from './codex.js';
export * from './codex-models.js';
// Feature types
export type { Feature, FeatureImagePath, FeatureTextFilePath, FeatureStatus } from './feature.js';
export type {
Feature,
FeatureImagePath,
FeatureTextFilePath,
FeatureStatus,
DescriptionHistoryEntry,
} from './feature.js';
// Session types
export type {
@@ -38,7 +54,18 @@ export type { ErrorType, ErrorInfo } from './error.js';
export type { ImageData, ImageContentBlock } from './image.js';
// Model types and constants
export { CLAUDE_MODEL_MAP, DEFAULT_MODELS, type ModelAlias } from './model.js';
export {
CLAUDE_MODEL_MAP,
CODEX_MODEL_MAP,
CODEX_MODEL_IDS,
REASONING_CAPABLE_MODELS,
supportsReasoningEffort,
getAllCodexModelIds,
DEFAULT_MODELS,
type ModelAlias,
type CodexModelId,
type AgentModel,
} from './model.js';
// Event types
export type { EventType, EventCallback } from './event.js';
@@ -104,11 +131,13 @@ export {
} from './settings.js';
// Model display constants
export type { ModelOption, ThinkingLevelOption } from './model-display.js';
export type { ModelOption, ThinkingLevelOption, ReasoningEffortOption } from './model-display.js';
export {
CLAUDE_MODELS,
THINKING_LEVELS,
THINKING_LEVEL_LABELS,
REASONING_EFFORT_LEVELS,
REASONING_EFFORT_LABELS,
getModelDisplayName,
} from './model-display.js';
@@ -151,6 +180,7 @@ export {
PROVIDER_PREFIXES,
isCursorModel,
isClaudeModel,
isCodexModel,
getModelProvider,
stripProviderPrefix,
addProviderPrefix,

View File

@@ -6,7 +6,10 @@
*/
import type { ModelAlias, ThinkingLevel, ModelProvider } from './settings.js';
import type { ReasoningEffort } from './provider.js';
import type { CursorModelId } from './cursor-models.js';
import type { AgentModel, CodexModelId } from './model.js';
import { CODEX_MODEL_MAP } from './model.js';
/**
* ModelOption - Display metadata for a model option in the UI
@@ -63,6 +66,61 @@ export const CLAUDE_MODELS: ModelOption[] = [
},
];
/**
* Codex model options with full metadata for UI display
* Official models from https://developers.openai.com/codex/models/
*/
export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
{
id: CODEX_MODEL_MAP.gpt52Codex,
label: 'GPT-5.2-Codex',
description: 'Most advanced agentic coding model (default for ChatGPT users).',
badge: 'Premium',
provider: 'codex',
hasReasoning: true,
},
{
id: CODEX_MODEL_MAP.gpt5Codex,
label: 'GPT-5-Codex',
description: 'Purpose-built for Codex CLI (default for CLI users).',
badge: 'Balanced',
provider: 'codex',
hasReasoning: true,
},
{
id: CODEX_MODEL_MAP.gpt5CodexMini,
label: 'GPT-5-Codex-Mini',
description: 'Faster workflows for code Q&A and editing.',
badge: 'Speed',
provider: 'codex',
hasReasoning: false,
},
{
id: CODEX_MODEL_MAP.codex1,
label: 'Codex-1',
description: 'o3-based model optimized for software engineering.',
badge: 'Premium',
provider: 'codex',
hasReasoning: true,
},
{
id: CODEX_MODEL_MAP.codexMiniLatest,
label: 'Codex-Mini-Latest',
description: 'o4-mini-based model for faster workflows.',
badge: 'Balanced',
provider: 'codex',
hasReasoning: false,
},
{
id: CODEX_MODEL_MAP.gpt5,
label: 'GPT-5',
description: 'GPT-5 base flagship model.',
badge: 'Balanced',
provider: 'codex',
hasReasoning: true,
},
];
/**
* Thinking level options with display labels
*
@@ -89,6 +147,43 @@ export const THINKING_LEVEL_LABELS: Record<ThinkingLevel, string> = {
ultrathink: 'Ultra',
};
/**
* ReasoningEffortOption - Display metadata for reasoning effort selection (Codex/OpenAI)
*/
export interface ReasoningEffortOption {
/** Reasoning effort identifier */
id: ReasoningEffort;
/** Display label */
label: string;
/** Description of what this level does */
description: string;
}
/**
* Reasoning effort options for Codex/OpenAI models
* All models support reasoning effort levels
*/
export const REASONING_EFFORT_LEVELS: ReasoningEffortOption[] = [
{ id: 'none', label: 'None', description: 'No reasoning tokens (GPT-5.1 models only)' },
{ id: 'minimal', label: 'Minimal', description: 'Very quick reasoning' },
{ id: 'low', label: 'Low', description: 'Quick responses for simpler queries' },
{ id: 'medium', label: 'Medium', description: 'Balance between depth and speed (default)' },
{ id: 'high', label: 'High', description: 'Maximizes reasoning depth for critical tasks' },
{ id: 'xhigh', label: 'XHigh', description: 'Highest level for gpt-5.1-codex-max and newer' },
];
/**
* Map of reasoning effort levels to short display labels
*/
export const REASONING_EFFORT_LABELS: Record<ReasoningEffort, string> = {
none: 'None',
minimal: 'Min',
low: 'Low',
medium: 'Med',
high: 'High',
xhigh: 'XHigh',
};
/**
* Get display name for a model
*
@@ -107,6 +202,12 @@ export function getModelDisplayName(model: ModelAlias | string): string {
haiku: 'Claude Haiku',
sonnet: 'Claude Sonnet',
opus: 'Claude Opus',
[CODEX_MODEL_MAP.gpt52Codex]: 'GPT-5.2-Codex',
[CODEX_MODEL_MAP.gpt5Codex]: 'GPT-5-Codex',
[CODEX_MODEL_MAP.gpt5CodexMini]: 'GPT-5-Codex-Mini',
[CODEX_MODEL_MAP.codex1]: 'Codex-1',
[CODEX_MODEL_MAP.codexMiniLatest]: 'Codex-Mini-Latest',
[CODEX_MODEL_MAP.gpt5]: 'GPT-5',
};
return displayNames[model] || model;
}

View File

@@ -7,12 +7,70 @@ export const CLAUDE_MODEL_MAP: Record<string, string> = {
opus: 'claude-opus-4-5-20251101',
} as const;
/**
* Codex/OpenAI model identifiers
* Based on OpenAI Codex CLI official models
* See: https://developers.openai.com/codex/models/
*/
export const CODEX_MODEL_MAP = {
// Codex-specific models
/** Most advanced agentic coding model for complex software engineering (default for ChatGPT users) */
gpt52Codex: 'gpt-5.2-codex',
/** Purpose-built for Codex CLI with versatile tool use (default for CLI users) */
gpt5Codex: 'gpt-5-codex',
/** Faster workflows optimized for low-latency code Q&A and editing */
gpt5CodexMini: 'gpt-5-codex-mini',
/** Version of o3 optimized for software engineering */
codex1: 'codex-1',
/** Version of o4-mini for Codex, optimized for faster workflows */
codexMiniLatest: 'codex-mini-latest',
// Base GPT-5 model (also available in Codex)
/** GPT-5 base flagship model */
gpt5: 'gpt-5',
} as const;
export const CODEX_MODEL_IDS = Object.values(CODEX_MODEL_MAP);
/**
* Models that support reasoning effort configuration
* These models can use reasoning.effort parameter
*/
export const REASONING_CAPABLE_MODELS = new Set([
CODEX_MODEL_MAP.gpt52Codex,
CODEX_MODEL_MAP.gpt5Codex,
CODEX_MODEL_MAP.gpt5,
CODEX_MODEL_MAP.codex1, // o3-based model
]);
/**
* Check if a model supports reasoning effort configuration
*/
export function supportsReasoningEffort(modelId: string): boolean {
return REASONING_CAPABLE_MODELS.has(modelId as any);
}
/**
* Get all Codex model IDs as an array
*/
export function getAllCodexModelIds(): CodexModelId[] {
return CODEX_MODEL_IDS as CodexModelId[];
}
/**
* Default models per provider
*/
export const DEFAULT_MODELS = {
claude: 'claude-opus-4-5-20251101',
cursor: 'auto', // Cursor's recommended default
codex: CODEX_MODEL_MAP.gpt52Codex, // GPT-5.2-Codex is the most advanced agentic coding model
} as const;
export type ModelAlias = keyof typeof CLAUDE_MODEL_MAP;
export type CodexModelId = (typeof CODEX_MODEL_MAP)[keyof typeof CODEX_MODEL_MAP];
/**
* AgentModel - Alias for ModelAlias for backward compatibility
* Represents available models across providers
*/
export type AgentModel = ModelAlias | CodexModelId;

View File

@@ -8,11 +8,12 @@
import type { ModelProvider } from './settings.js';
import { CURSOR_MODEL_MAP, type CursorModelId } from './cursor-models.js';
import { CLAUDE_MODEL_MAP } from './model.js';
import { CLAUDE_MODEL_MAP, CODEX_MODEL_MAP, type CodexModelId } from './model.js';
/** Provider prefix constants */
export const PROVIDER_PREFIXES = {
cursor: 'cursor-',
codex: 'codex-',
// Add new provider prefixes here
} as const;
@@ -52,6 +53,35 @@ export function isClaudeModel(model: string | undefined | null): boolean {
return model.includes('claude-');
}
/**
* Check if a model string represents a Codex/OpenAI model
*
* @param model - Model string to check (e.g., "gpt-5.2", "o1", "codex-gpt-5.2")
* @returns true if the model is a Codex model
*/
export function isCodexModel(model: string | undefined | null): boolean {
if (!model || typeof model !== 'string') return false;
// Check for explicit codex- prefix
if (model.startsWith(PROVIDER_PREFIXES.codex)) {
return true;
}
// Check if it's a gpt- model
if (model.startsWith('gpt-')) {
return true;
}
// Check if it's an o-series model (o1, o3, etc.)
if (/^o\d/.test(model)) {
return true;
}
// Check if it's in the CODEX_MODEL_MAP
const modelValues = Object.values(CODEX_MODEL_MAP);
return modelValues.includes(model as CodexModelId);
}
/**
* Get the provider for a model string
*
@@ -59,6 +89,11 @@ export function isClaudeModel(model: string | undefined | null): boolean {
* @returns The provider type, defaults to 'claude' for unknown models
*/
export function getModelProvider(model: string | undefined | null): ModelProvider {
// Check Codex first before Cursor, since Cursor also supports gpt models
// but bare gpt-* should route to Codex
if (isCodexModel(model)) {
return 'codex';
}
if (isCursorModel(model)) {
return 'cursor';
}
@@ -96,6 +131,7 @@ export function stripProviderPrefix(model: string): string {
* @example
* addProviderPrefix('composer-1', 'cursor') // 'cursor-composer-1'
* addProviderPrefix('cursor-composer-1', 'cursor') // 'cursor-composer-1' (no change)
* addProviderPrefix('gpt-5.2', 'codex') // 'codex-gpt-5.2'
* addProviderPrefix('sonnet', 'claude') // 'sonnet' (Claude doesn't use prefix)
*/
export function addProviderPrefix(model: string, provider: ModelProvider): string {
@@ -105,6 +141,10 @@ export function addProviderPrefix(model: string, provider: ModelProvider): strin
if (!model.startsWith(PROVIDER_PREFIXES.cursor)) {
return `${PROVIDER_PREFIXES.cursor}${model}`;
}
} else if (provider === 'codex') {
if (!model.startsWith(PROVIDER_PREFIXES.codex)) {
return `${PROVIDER_PREFIXES.codex}${model}`;
}
}
// Claude models don't use prefixes
return model;
@@ -123,6 +163,7 @@ export function getBareModelId(model: string): string {
/**
* Normalize a model string to its canonical form
* - For Cursor: adds cursor- prefix if missing
* - For Codex: can add codex- prefix (but bare gpt-* is also valid)
* - For Claude: returns as-is
*
* @param model - Model string to normalize
@@ -136,5 +177,19 @@ export function normalizeModelString(model: string | undefined | null): string {
return `${PROVIDER_PREFIXES.cursor}${model}`;
}
// For Codex, bare gpt-* and o-series models are valid canonical forms
// Only add prefix if it's in CODEX_MODEL_MAP but doesn't have gpt-/o prefix
const codexModelValues = Object.values(CODEX_MODEL_MAP);
if (codexModelValues.includes(model as CodexModelId)) {
// If it already starts with gpt- or o, it's canonical
if (model.startsWith('gpt-') || /^o\d/.test(model)) {
return model;
}
// Otherwise, it might need a prefix (though this is unlikely)
if (!model.startsWith(PROVIDER_PREFIXES.codex)) {
return `${PROVIDER_PREFIXES.codex}${model}`;
}
}
return model;
}

View File

@@ -3,6 +3,20 @@
*/
import type { ThinkingLevel } from './settings.js';
import type { CodexSandboxMode, CodexApprovalPolicy } from './codex.js';
/**
* Reasoning effort levels for Codex/OpenAI models
* Controls the computational intensity and reasoning tokens used.
* Based on OpenAI API documentation:
* - 'none': No reasoning (GPT-5.1 models only)
* - 'minimal': Very quick reasoning
* - 'low': Quick responses for simpler queries
* - 'medium': Balance between depth and speed (default)
* - 'high': Maximizes reasoning depth for critical tasks
* - 'xhigh': Highest level, supported by gpt-5.1-codex-max and newer
*/
export type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
/**
* Configuration for a provider instance
@@ -87,11 +101,14 @@ export interface ExecuteOptions {
maxTurns?: number;
allowedTools?: string[];
mcpServers?: Record<string, McpServerConfig>;
/** If true, allows all MCP tools unrestricted (no approval needed). Default: false */
mcpUnrestrictedTools?: boolean;
/** If true, automatically approves all MCP tool calls. Default: undefined (uses approval policy) */
mcpAutoApproveTools?: boolean;
abortController?: AbortController;
conversationHistory?: ConversationMessage[]; // Previous messages for context
sdkSessionId?: string; // Claude SDK session ID for resuming conversations
settingSources?: Array<'user' | 'project' | 'local'>; // Sources for CLAUDE.md loading
sandbox?: { enabled: boolean; autoAllowBashIfSandboxed?: boolean }; // Sandbox configuration
/**
* If true, the provider should run in read-only mode (no file modifications).
* For Cursor CLI, this omits the --force flag, making it suggest-only.
@@ -109,6 +126,31 @@ export interface ExecuteOptions {
* Key: agent name, Value: agent definition
*/
agents?: Record<string, AgentDefinition>;
/**
* Reasoning effort for Codex/OpenAI models with reasoning capabilities.
* Controls how many reasoning tokens the model generates before responding.
* Supported values: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'
* - none: No reasoning tokens (fastest)
* - minimal/low: Quick reasoning for simple tasks
* - medium: Balanced reasoning (default)
* - high: Extended reasoning for complex tasks
* - xhigh: Maximum reasoning for quality-critical tasks
* Only applies to models that support reasoning (gpt-5.1-codex-max+, o3-mini, o4-mini)
*/
reasoningEffort?: ReasoningEffort;
codexSettings?: {
autoLoadAgents?: boolean;
sandboxMode?: CodexSandboxMode;
approvalPolicy?: CodexApprovalPolicy;
enableWebSearch?: boolean;
enableImages?: boolean;
additionalDirs?: string[];
threadId?: string;
};
outputFormat?: {
type: 'json_schema';
schema: Record<string, unknown>;
};
}
/**
@@ -185,4 +227,5 @@ export interface ModelDefinition {
supportsTools?: boolean;
tier?: 'basic' | 'standard' | 'premium';
default?: boolean;
hasReasoning?: boolean;
}

View File

@@ -6,10 +6,11 @@
* (for file I/O via SettingsService) and the UI (for state management and sync).
*/
import type { ModelAlias } from './model.js';
import type { ModelAlias, AgentModel, CodexModelId } from './model.js';
import type { CursorModelId } from './cursor-models.js';
import { CURSOR_MODEL_MAP, getAllCursorModelIds } from './cursor-models.js';
import type { PromptCustomization } from './prompts.js';
import type { CodexSandboxMode, CodexApprovalPolicy } from './codex.js';
// Re-export ModelAlias for convenience
export type { ModelAlias };
@@ -95,7 +96,14 @@ export function getThinkingTokenBudget(level: ThinkingLevel | undefined): number
}
/** ModelProvider - AI model provider for credentials and API key management */
export type ModelProvider = 'claude' | 'cursor';
export type ModelProvider = 'claude' | 'cursor' | 'codex';
const DEFAULT_CODEX_AUTO_LOAD_AGENTS = false;
const DEFAULT_CODEX_SANDBOX_MODE: CodexSandboxMode = 'workspace-write';
const DEFAULT_CODEX_APPROVAL_POLICY: CodexApprovalPolicy = 'on-request';
const DEFAULT_CODEX_ENABLE_WEB_SEARCH = false;
const DEFAULT_CODEX_ENABLE_IMAGES = true;
const DEFAULT_CODEX_ADDITIONAL_DIRS: string[] = [];
/**
* PhaseModelEntry - Configuration for a single phase model
@@ -227,7 +235,7 @@ export interface AIProfile {
name: string;
/** User-friendly description */
description: string;
/** Provider selection: 'claude' or 'cursor' */
/** Provider selection: 'claude', 'cursor', or 'codex' */
provider: ModelProvider;
/** Whether this is a built-in default profile */
isBuiltIn: boolean;
@@ -245,6 +253,10 @@ export interface AIProfile {
* Note: For Cursor, thinking is embedded in the model ID (e.g., 'claude-sonnet-4-thinking')
*/
cursorModel?: CursorModelId;
// Codex-specific settings
/** Which Codex/GPT model to use - only for Codex provider */
codexModel?: CodexModelId;
}
/**
@@ -262,6 +274,12 @@ export function profileHasThinking(profile: AIProfile): boolean {
return modelConfig?.hasThinking ?? false;
}
if (profile.provider === 'codex') {
// Codex models handle thinking internally (o-series models)
const model = profile.codexModel || 'gpt-5.2';
return model.startsWith('o');
}
return false;
}
@@ -273,6 +291,10 @@ export function getProfileModelString(profile: AIProfile): string {
return `cursor:${profile.cursorModel || 'auto'}`;
}
if (profile.provider === 'codex') {
return `codex:${profile.codexModel || 'gpt-5.2'}`;
}
// Claude
return profile.model || 'sonnet';
}
@@ -387,6 +409,18 @@ export interface GlobalSettings {
/** Version number for schema migration */
version: number;
// Migration Tracking
/** Whether localStorage settings have been migrated to API storage (prevents re-migration) */
localStorageMigrated?: boolean;
// Onboarding / Setup Wizard
/** Whether the initial setup wizard has been completed */
setupComplete: boolean;
/** Whether this is the first run experience (used by UI onboarding) */
isFirstRun: boolean;
/** Whether Claude setup was skipped during onboarding */
skipClaudeSetup: boolean;
// Theme Configuration
/** Currently selected theme */
theme: ThemeMode;
@@ -406,6 +440,8 @@ export interface GlobalSettings {
defaultSkipTests: boolean;
/** Default: enable dependency blocking */
enableDependencyBlocking: boolean;
/** Skip verification requirement in auto-mode (treat 'completed' same as 'verified') */
skipVerificationInAutoMode: boolean;
/** Default: use git worktrees for feature branches */
useWorktrees: boolean;
/** Default: only show AI profiles (hide other settings) */
@@ -450,6 +486,8 @@ export interface GlobalSettings {
projects: ProjectRef[];
/** Projects in trash/recycle bin */
trashedProjects: TrashedProjectRef[];
/** ID of the currently open project (null if none) */
currentProjectId: string | null;
/** History of recently opened project IDs */
projectHistory: string[];
/** Current position in project history for navigation */
@@ -474,11 +512,25 @@ export interface GlobalSettings {
// Claude Agent SDK Settings
/** Auto-load CLAUDE.md files using SDK's settingSources option */
autoLoadClaudeMd?: boolean;
/** Enable sandbox mode for bash commands (default: false, enable for additional security) */
enableSandboxMode?: boolean;
/** Skip showing the sandbox risk warning dialog */
/** Skip the sandbox environment warning dialog on startup */
skipSandboxWarning?: boolean;
// Codex CLI Settings
/** Auto-load .codex/AGENTS.md instructions into Codex prompts */
codexAutoLoadAgents?: boolean;
/** Sandbox mode for Codex CLI command execution */
codexSandboxMode?: CodexSandboxMode;
/** Approval policy for Codex CLI tool execution */
codexApprovalPolicy?: CodexApprovalPolicy;
/** Enable web search capability for Codex CLI (--search flag) */
codexEnableWebSearch?: boolean;
/** Enable image attachment support for Codex CLI (-i flag) */
codexEnableImages?: boolean;
/** Additional directories with write access (--add-dir flags) */
codexAdditionalDirs?: string[];
/** Last thread ID for session resumption */
codexThreadId?: string;
// MCP Server Configuration
/** List of configured MCP servers for agent use */
mcpServers: MCPServerConfig[];
@@ -656,7 +708,7 @@ export const DEFAULT_PHASE_MODELS: PhaseModelConfig = {
};
/** Current version of the global settings schema */
export const SETTINGS_VERSION = 3;
export const SETTINGS_VERSION = 4;
/** Current version of the credentials schema */
export const CREDENTIALS_VERSION = 1;
/** Current version of the project settings schema */
@@ -689,6 +741,9 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
/** Default global settings used when no settings file exists */
export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
version: SETTINGS_VERSION,
setupComplete: false,
isFirstRun: true,
skipClaudeSetup: false,
theme: 'dark',
sidebarOpen: true,
chatHistoryOpen: false,
@@ -696,6 +751,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
maxConcurrency: 3,
defaultSkipTests: true,
enableDependencyBlocking: true,
skipVerificationInAutoMode: false,
useWorktrees: false,
showProfilesOnly: false,
defaultPlanningMode: 'skip',
@@ -711,6 +767,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
aiProfiles: [],
projects: [],
trashedProjects: [],
currentProjectId: null,
projectHistory: [],
projectHistoryIndex: -1,
lastProjectDir: undefined,
@@ -718,8 +775,14 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
worktreePanelCollapsed: false,
lastSelectedSessionByProject: {},
autoLoadClaudeMd: false,
enableSandboxMode: false,
skipSandboxWarning: false,
codexAutoLoadAgents: DEFAULT_CODEX_AUTO_LOAD_AGENTS,
codexSandboxMode: DEFAULT_CODEX_SANDBOX_MODE,
codexApprovalPolicy: DEFAULT_CODEX_APPROVAL_POLICY,
codexEnableWebSearch: DEFAULT_CODEX_ENABLE_WEB_SEARCH,
codexEnableImages: DEFAULT_CODEX_ENABLE_IMAGES,
codexAdditionalDirs: DEFAULT_CODEX_ADDITIONAL_DIRS,
codexThreadId: undefined,
mcpServers: [],
enableSkills: true,
skillsSources: ['user', 'project'],