Merge remote-tracking branch 'upstream/v0.15.0rc' into refactor/auto-mode-service-gsxdsm

This commit is contained in:
gsxdsm
2026-02-15 10:20:53 -08:00
42 changed files with 1363 additions and 153 deletions

2
.gitignore vendored
View File

@@ -98,3 +98,5 @@ data/
# GSD planning docs (local-only) # GSD planning docs (local-only)
.planning/ .planning/
.mcp.json
.planning

View File

@@ -161,7 +161,7 @@ Use `resolveModelString()` from `@automaker/model-resolver` to convert model ali
- `haiku``claude-haiku-4-5` - `haiku``claude-haiku-4-5`
- `sonnet``claude-sonnet-4-20250514` - `sonnet``claude-sonnet-4-20250514`
- `opus``claude-opus-4-5-20251101` - `opus``claude-opus-4-6`
## Environment Variables ## Environment Variables

View File

@@ -118,6 +118,7 @@ RUN curl -fsSL https://opencode.ai/install | bash && \
echo "=== Checking OpenCode CLI installation ===" && \ echo "=== Checking OpenCode CLI installation ===" && \
ls -la /home/automaker/.local/bin/ && \ ls -la /home/automaker/.local/bin/ && \
(which opencode && opencode --version) || echo "opencode installed (may need auth setup)" (which opencode && opencode --version) || echo "opencode installed (may need auth setup)"
USER root USER root
# Add PATH to profile so it's available in all interactive shells (for login shells) # Add PATH to profile so it's available in all interactive shells (for login shells)
@@ -147,6 +148,15 @@ COPY --from=server-builder /app/apps/server/package*.json ./apps/server/
# Copy node_modules (includes symlinks to libs) # Copy node_modules (includes symlinks to libs)
COPY --from=server-builder /app/node_modules ./node_modules COPY --from=server-builder /app/node_modules ./node_modules
# Install Playwright Chromium browser for AI agent verification tests
# This adds ~300MB to the image but enables automated testing mode out of the box
# Using the locally installed playwright ensures we use the pinned version from package-lock.json
USER automaker
RUN ./node_modules/.bin/playwright install chromium && \
echo "=== Playwright Chromium installed ===" && \
ls -la /home/automaker/.cache/ms-playwright/
USER root
# Create data and projects directories # Create data and projects directories
RUN mkdir -p /data /projects && chown automaker:automaker /data /projects RUN mkdir -p /data /projects && chown automaker:automaker /data /projects

View File

@@ -367,6 +367,42 @@ services:
The Docker image supports both AMD64 and ARM64 architectures. The GitHub CLI and Claude CLI are automatically downloaded for the correct architecture during build. The Docker image supports both AMD64 and ARM64 architectures. The GitHub CLI and Claude CLI are automatically downloaded for the correct architecture during build.
##### Playwright for Automated Testing
The Docker image includes **Playwright Chromium pre-installed** for AI agent verification tests. When agents implement features in automated testing mode, they use Playwright to verify the implementation works correctly.
**No additional setup required** - Playwright verification works out of the box.
#### Optional: Persist browsers for manual updates
By default, Playwright Chromium is pre-installed in the Docker image. If you need to manually update browsers or want to persist browser installations across container restarts (not image rebuilds), you can mount a volume.
**Important:** When you first add this volume mount to an existing setup, the empty volume will override the pre-installed browsers. You must re-install them:
```bash
# After adding the volume mount for the first time
docker exec --user automaker -w /app automaker-server npx playwright install chromium
```
Add this to your `docker-compose.override.yml`:
```yaml
services:
server:
volumes:
- playwright-cache:/home/automaker/.cache/ms-playwright
volumes:
playwright-cache:
name: automaker-playwright-cache
```
**Updating browsers manually:**
```bash
docker exec --user automaker -w /app automaker-server npx playwright install chromium
```
### Testing ### Testing
#### End-to-End Tests (Playwright) #### End-to-End Tests (Playwright)

View File

@@ -24,7 +24,7 @@
"test:unit": "vitest run tests/unit" "test:unit": "vitest run tests/unit"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "0.1.76", "@anthropic-ai/claude-agent-sdk": "0.2.32",
"@automaker/dependency-resolver": "1.0.0", "@automaker/dependency-resolver": "1.0.0",
"@automaker/git-utils": "1.0.0", "@automaker/git-utils": "1.0.0",
"@automaker/model-resolver": "1.0.0", "@automaker/model-resolver": "1.0.0",
@@ -34,7 +34,7 @@
"@automaker/utils": "1.0.0", "@automaker/utils": "1.0.0",
"@github/copilot-sdk": "^0.1.16", "@github/copilot-sdk": "^0.1.16",
"@modelcontextprotocol/sdk": "1.25.2", "@modelcontextprotocol/sdk": "1.25.2",
"@openai/codex-sdk": "^0.77.0", "@openai/codex-sdk": "^0.98.0",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"cors": "2.8.5", "cors": "2.8.5",
"dotenv": "17.2.3", "dotenv": "17.2.3",
@@ -45,6 +45,7 @@
"yaml": "2.7.0" "yaml": "2.7.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.57.0",
"@types/cookie": "0.6.0", "@types/cookie": "0.6.0",
"@types/cookie-parser": "1.4.10", "@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19", "@types/cors": "2.8.19",

View File

@@ -121,21 +121,57 @@ const BOX_CONTENT_WIDTH = 67;
// The Claude Agent SDK can use either ANTHROPIC_API_KEY or Claude Code CLI authentication // The Claude Agent SDK can use either ANTHROPIC_API_KEY or Claude Code CLI authentication
(async () => { (async () => {
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY; const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
const hasEnvOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
logger.debug('[CREDENTIAL_CHECK] Starting credential detection...');
logger.debug('[CREDENTIAL_CHECK] Environment variables:', {
hasAnthropicKey,
hasEnvOAuthToken,
});
if (hasAnthropicKey) { if (hasAnthropicKey) {
logger.info('✓ ANTHROPIC_API_KEY detected'); logger.info('✓ ANTHROPIC_API_KEY detected');
return; return;
} }
if (hasEnvOAuthToken) {
logger.info('✓ CLAUDE_CODE_OAUTH_TOKEN detected');
return;
}
// Check for Claude Code CLI authentication // Check for Claude Code CLI authentication
// Store indicators outside the try block so we can use them in the warning message
let cliAuthIndicators: Awaited<ReturnType<typeof getClaudeAuthIndicators>> | null = null;
try { try {
const indicators = await getClaudeAuthIndicators(); cliAuthIndicators = await getClaudeAuthIndicators();
const indicators = cliAuthIndicators;
// Log detailed credential detection results
const { checks, ...indicatorSummary } = indicators;
logger.debug('[CREDENTIAL_CHECK] Claude CLI auth indicators:', indicatorSummary);
logger.debug('[CREDENTIAL_CHECK] File check details:', checks);
const hasCliAuth = const hasCliAuth =
indicators.hasStatsCacheWithActivity || indicators.hasStatsCacheWithActivity ||
(indicators.hasSettingsFile && indicators.hasProjectsSessions) || (indicators.hasSettingsFile && indicators.hasProjectsSessions) ||
(indicators.hasCredentialsFile && (indicators.hasCredentialsFile &&
(indicators.credentials?.hasOAuthToken || indicators.credentials?.hasApiKey)); (indicators.credentials?.hasOAuthToken || indicators.credentials?.hasApiKey));
logger.debug('[CREDENTIAL_CHECK] Auth determination:', {
hasCliAuth,
reason: hasCliAuth
? indicators.hasStatsCacheWithActivity
? 'stats cache with activity'
: indicators.hasSettingsFile && indicators.hasProjectsSessions
? 'settings file + project sessions'
: indicators.credentials?.hasOAuthToken
? 'credentials file with OAuth token'
: 'credentials file with API key'
: 'no valid credentials found',
});
if (hasCliAuth) { if (hasCliAuth) {
logger.info('✓ Claude Code CLI authentication detected'); logger.info('✓ Claude Code CLI authentication detected');
return; return;
@@ -145,7 +181,7 @@ const BOX_CONTENT_WIDTH = 67;
logger.warn('Error checking for Claude Code CLI authentication:', error); logger.warn('Error checking for Claude Code CLI authentication:', error);
} }
// No authentication found - show warning // No authentication found - show warning with paths that were checked
const wHeader = '⚠️ WARNING: No Claude authentication configured'.padEnd(BOX_CONTENT_WIDTH); const wHeader = '⚠️ WARNING: No Claude authentication configured'.padEnd(BOX_CONTENT_WIDTH);
const w1 = 'The Claude Agent SDK requires authentication to function.'.padEnd(BOX_CONTENT_WIDTH); const w1 = 'The Claude Agent SDK requires authentication to function.'.padEnd(BOX_CONTENT_WIDTH);
const w2 = 'Options:'.padEnd(BOX_CONTENT_WIDTH); const w2 = 'Options:'.padEnd(BOX_CONTENT_WIDTH);
@@ -158,6 +194,33 @@ const BOX_CONTENT_WIDTH = 67;
BOX_CONTENT_WIDTH BOX_CONTENT_WIDTH
); );
// Build paths checked summary from the indicators (if available)
let pathsCheckedInfo = '';
if (cliAuthIndicators) {
const pathsChecked: string[] = [];
// Collect paths that were checked (paths are always populated strings)
pathsChecked.push(`Settings: ${cliAuthIndicators.checks.settingsFile.path}`);
pathsChecked.push(`Stats cache: ${cliAuthIndicators.checks.statsCache.path}`);
pathsChecked.push(`Projects dir: ${cliAuthIndicators.checks.projectsDir.path}`);
for (const credFile of cliAuthIndicators.checks.credentialFiles) {
pathsChecked.push(`Credentials: ${credFile.path}`);
}
if (pathsChecked.length > 0) {
pathsCheckedInfo = `
║ ║
${'Paths checked:'.padEnd(BOX_CONTENT_WIDTH)}
${pathsChecked
.map((p) => {
const maxLen = BOX_CONTENT_WIDTH - 4;
const display = p.length > maxLen ? '...' + p.slice(-(maxLen - 3)) : p;
return `${display.padEnd(maxLen)}`;
})
.join('\n')}`;
}
}
logger.warn(` logger.warn(`
╔═════════════════════════════════════════════════════════════════════╗ ╔═════════════════════════════════════════════════════════════════════╗
${wHeader} ${wHeader}
@@ -169,7 +232,7 @@ const BOX_CONTENT_WIDTH = 67;
${w3} ${w3}
${w4} ${w4}
${w5} ${w5}
${w6} ${w6}${pathsCheckedInfo}
║ ║ ║ ║
╚═════════════════════════════════════════════════════════════════════╝ ╚═════════════════════════════════════════════════════════════════════╝
`); `);

View File

@@ -253,11 +253,27 @@ function buildMcpOptions(config: CreateSdkOptionsConfig): McpOptions {
/** /**
* Build thinking options for SDK configuration. * Build thinking options for SDK configuration.
* Converts ThinkingLevel to maxThinkingTokens for the Claude SDK. * Converts ThinkingLevel to maxThinkingTokens for the Claude SDK.
* For adaptive thinking (Opus 4.6), omits maxThinkingTokens to let the model
* decide its own reasoning depth.
* *
* @param thinkingLevel - The thinking level to convert * @param thinkingLevel - The thinking level to convert
* @returns Object with maxThinkingTokens if thinking is enabled * @returns Object with maxThinkingTokens if thinking is enabled with a budget
*/ */
function buildThinkingOptions(thinkingLevel?: ThinkingLevel): Partial<Options> { function buildThinkingOptions(thinkingLevel?: ThinkingLevel): Partial<Options> {
if (!thinkingLevel || thinkingLevel === 'none') {
return {};
}
// Adaptive thinking (Opus 4.6): don't set maxThinkingTokens
// The model will use adaptive thinking by default
if (thinkingLevel === 'adaptive') {
logger.debug(
`buildThinkingOptions: thinkingLevel="adaptive" -> no maxThinkingTokens (model decides)`
);
return {};
}
// Manual budget-based thinking for Haiku/Sonnet
const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel); const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel);
logger.debug( logger.debug(
`buildThinkingOptions: thinkingLevel="${thinkingLevel}" -> maxThinkingTokens=${maxThinkingTokens}` `buildThinkingOptions: thinkingLevel="${thinkingLevel}" -> maxThinkingTokens=${maxThinkingTokens}`

View File

@@ -219,8 +219,11 @@ export class ClaudeProvider extends BaseProvider {
// claudeCompatibleProvider takes precedence over claudeApiProfile // claudeCompatibleProvider takes precedence over claudeApiProfile
const providerConfig = claudeCompatibleProvider || claudeApiProfile; const providerConfig = claudeCompatibleProvider || claudeApiProfile;
// Convert thinking level to token budget // Build thinking configuration
const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel); // Adaptive thinking (Opus 4.6): don't set maxThinkingTokens, model uses adaptive by default
// Manual thinking (Haiku/Sonnet): use budget_tokens
const maxThinkingTokens =
thinkingLevel === 'adaptive' ? undefined : getThinkingTokenBudget(thinkingLevel);
// Build Claude SDK options // Build Claude SDK options
const sdkOptions: Options = { const sdkOptions: Options = {
@@ -349,13 +352,13 @@ export class ClaudeProvider extends BaseProvider {
getAvailableModels(): ModelDefinition[] { getAvailableModels(): ModelDefinition[] {
const models = [ const models = [
{ {
id: 'claude-opus-4-5-20251101', id: 'claude-opus-4-6',
name: 'Claude Opus 4.5', name: 'Claude Opus 4.6',
modelString: 'claude-opus-4-5-20251101', modelString: 'claude-opus-4-6',
provider: 'anthropic', provider: 'anthropic',
description: 'Most capable Claude model', description: 'Most capable Claude model with adaptive thinking',
contextWindow: 200000, contextWindow: 200000,
maxOutputTokens: 16000, maxOutputTokens: 128000,
supportsVision: true, supportsVision: true,
supportsTools: true, supportsTools: true,
tier: 'premium' as const, tier: 'premium' as const,

View File

@@ -19,12 +19,11 @@ const MAX_OUTPUT_16K = 16000;
export const CODEX_MODELS: ModelDefinition[] = [ export const CODEX_MODELS: ModelDefinition[] = [
// ========== Recommended Codex Models ========== // ========== Recommended Codex Models ==========
{ {
id: CODEX_MODEL_MAP.gpt52Codex, id: CODEX_MODEL_MAP.gpt53Codex,
name: 'GPT-5.2-Codex', name: 'GPT-5.3-Codex',
modelString: CODEX_MODEL_MAP.gpt52Codex, modelString: CODEX_MODEL_MAP.gpt53Codex,
provider: 'openai', provider: 'openai',
description: description: 'Latest frontier agentic coding model.',
'Most advanced agentic coding model for complex software engineering (default for ChatGPT users).',
contextWindow: CONTEXT_WINDOW_256K, contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K, maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true, supportsVision: true,
@@ -33,12 +32,25 @@ export const CODEX_MODELS: ModelDefinition[] = [
default: true, default: true,
hasReasoning: true, hasReasoning: true,
}, },
{
id: CODEX_MODEL_MAP.gpt52Codex,
name: 'GPT-5.2-Codex',
modelString: CODEX_MODEL_MAP.gpt52Codex,
provider: 'openai',
description: 'Frontier agentic coding model.',
contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
supportsTools: true,
tier: 'premium' as const,
hasReasoning: true,
},
{ {
id: CODEX_MODEL_MAP.gpt51CodexMax, id: CODEX_MODEL_MAP.gpt51CodexMax,
name: 'GPT-5.1-Codex-Max', name: 'GPT-5.1-Codex-Max',
modelString: CODEX_MODEL_MAP.gpt51CodexMax, modelString: CODEX_MODEL_MAP.gpt51CodexMax,
provider: 'openai', provider: 'openai',
description: 'Optimized for long-horizon, agentic coding tasks in Codex.', description: 'Codex-optimized flagship for deep and fast reasoning.',
contextWindow: CONTEXT_WINDOW_256K, contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K, maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true, supportsVision: true,
@@ -51,7 +63,7 @@ export const CODEX_MODELS: ModelDefinition[] = [
name: 'GPT-5.1-Codex-Mini', name: 'GPT-5.1-Codex-Mini',
modelString: CODEX_MODEL_MAP.gpt51CodexMini, modelString: CODEX_MODEL_MAP.gpt51CodexMini,
provider: 'openai', provider: 'openai',
description: 'Smaller, more cost-effective version for faster workflows.', description: 'Optimized for codex. Cheaper, faster, but less capable.',
contextWindow: CONTEXT_WINDOW_128K, contextWindow: CONTEXT_WINDOW_128K,
maxOutputTokens: MAX_OUTPUT_16K, maxOutputTokens: MAX_OUTPUT_16K,
supportsVision: true, supportsVision: true,
@@ -66,7 +78,7 @@ export const CODEX_MODELS: ModelDefinition[] = [
name: 'GPT-5.2', name: 'GPT-5.2',
modelString: CODEX_MODEL_MAP.gpt52, modelString: CODEX_MODEL_MAP.gpt52,
provider: 'openai', provider: 'openai',
description: 'Best general agentic model for tasks across industries and domains.', description: 'Latest frontier model with improvements across knowledge, reasoning and coding.',
contextWindow: CONTEXT_WINDOW_256K, contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K, maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true, supportsVision: true,

View File

@@ -103,7 +103,7 @@ export class ProviderFactory {
/** /**
* Get the appropriate provider for a given model ID * Get the appropriate provider for a given model ID
* *
* @param modelId Model identifier (e.g., "claude-opus-4-5-20251101", "cursor-gpt-4o", "cursor-auto") * @param modelId Model identifier (e.g., "claude-opus-4-6", "cursor-gpt-4o", "cursor-auto")
* @param options Optional settings * @param options Optional settings
* @param options.throwOnDisconnected Throw error if provider is disconnected (default: true) * @param options.throwOnDisconnected Throw error if provider is disconnected (default: true)
* @returns Provider instance for the model * @returns Provider instance for the model

View File

@@ -6,6 +6,7 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk'; import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import { getClaudeAuthIndicators } from '@automaker/platform';
import { getApiKey } from '../common.js'; import { getApiKey } from '../common.js';
import { import {
createSecureAuthEnv, createSecureAuthEnv,
@@ -320,9 +321,28 @@ export function createVerifyClaudeAuthHandler() {
authMethod, authMethod,
}); });
// Determine specific auth type for success messages
const effectiveAuthMethod = authMethod ?? 'api_key';
let authType: 'oauth' | 'api_key' | 'cli' | undefined;
if (authenticated) {
if (effectiveAuthMethod === 'api_key') {
authType = 'api_key';
} else if (effectiveAuthMethod === 'cli') {
// Check if CLI auth is via OAuth (Claude Code subscription) or generic CLI
try {
const indicators = await getClaudeAuthIndicators();
authType = indicators.credentials?.hasOAuthToken ? 'oauth' : 'cli';
} catch {
// Fall back to generic CLI if credential check fails
authType = 'cli';
}
}
}
res.json({ res.json({
success: true, success: true,
authenticated, authenticated,
authType,
error: errorMessage || undefined, error: errorMessage || undefined,
}); });
} catch (error) { } catch (error) {

View File

@@ -35,7 +35,7 @@ describe('model-resolver.ts', () => {
it("should resolve 'opus' alias to full model string", () => { it("should resolve 'opus' alias to full model string", () => {
const result = resolveModelString('opus'); const result = resolveModelString('opus');
expect(result).toBe('claude-opus-4-5-20251101'); expect(result).toBe('claude-opus-4-6');
expect(consoleSpy.log).toHaveBeenCalledWith( expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining('Migrated legacy ID: "opus" -> "claude-opus"') expect.stringContaining('Migrated legacy ID: "opus" -> "claude-opus"')
); );
@@ -117,7 +117,7 @@ describe('model-resolver.ts', () => {
describe('getEffectiveModel', () => { describe('getEffectiveModel', () => {
it('should prioritize explicit model over session and default', () => { it('should prioritize explicit model over session and default', () => {
const result = getEffectiveModel('opus', 'haiku', 'gpt-5.2'); const result = getEffectiveModel('opus', 'haiku', 'gpt-5.2');
expect(result).toBe('claude-opus-4-5-20251101'); expect(result).toBe('claude-opus-4-6');
}); });
it('should use session model when explicit is not provided', () => { it('should use session model when explicit is not provided', () => {

View File

@@ -491,5 +491,29 @@ describe('sdk-options.ts', () => {
expect(options.maxThinkingTokens).toBeUndefined(); expect(options.maxThinkingTokens).toBeUndefined();
}); });
}); });
describe('adaptive thinking for Opus 4.6', () => {
it('should not set maxThinkingTokens for adaptive thinking (model decides)', async () => {
const { createAutoModeOptions } = await import('@/lib/sdk-options.js');
const options = createAutoModeOptions({
cwd: '/test/path',
thinkingLevel: 'adaptive',
});
expect(options.maxThinkingTokens).toBeUndefined();
});
it('should not include maxThinkingTokens when thinkingLevel is "none"', async () => {
const { createAutoModeOptions } = await import('@/lib/sdk-options.js');
const options = createAutoModeOptions({
cwd: '/test/path',
thinkingLevel: 'none',
});
expect(options.maxThinkingTokens).toBeUndefined();
});
});
}); });
}); });

View File

@@ -39,7 +39,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({ const generator = provider.executeQuery({
prompt: 'Hello', prompt: 'Hello',
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/test', cwd: '/test',
}); });
@@ -59,7 +59,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({ const generator = provider.executeQuery({
prompt: 'Test prompt', prompt: 'Test prompt',
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/test/dir', cwd: '/test/dir',
systemPrompt: 'You are helpful', systemPrompt: 'You are helpful',
maxTurns: 10, maxTurns: 10,
@@ -71,7 +71,7 @@ describe('claude-provider.ts', () => {
expect(sdk.query).toHaveBeenCalledWith({ expect(sdk.query).toHaveBeenCalledWith({
prompt: 'Test prompt', prompt: 'Test prompt',
options: expect.objectContaining({ options: expect.objectContaining({
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
systemPrompt: 'You are helpful', systemPrompt: 'You are helpful',
maxTurns: 10, maxTurns: 10,
cwd: '/test/dir', cwd: '/test/dir',
@@ -91,7 +91,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({ const generator = provider.executeQuery({
prompt: 'Test', prompt: 'Test',
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/test', cwd: '/test',
}); });
@@ -116,7 +116,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({ const generator = provider.executeQuery({
prompt: 'Test', prompt: 'Test',
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/test', cwd: '/test',
abortController, abortController,
}); });
@@ -145,7 +145,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({ const generator = provider.executeQuery({
prompt: 'Current message', prompt: 'Current message',
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/test', cwd: '/test',
conversationHistory, conversationHistory,
sdkSessionId: 'test-session-id', sdkSessionId: 'test-session-id',
@@ -176,7 +176,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({ const generator = provider.executeQuery({
prompt: arrayPrompt as any, prompt: arrayPrompt as any,
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/test', cwd: '/test',
}); });
@@ -196,7 +196,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({ const generator = provider.executeQuery({
prompt: 'Test', prompt: 'Test',
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/test', cwd: '/test',
}); });
@@ -222,7 +222,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({ const generator = provider.executeQuery({
prompt: 'Test', prompt: 'Test',
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/test', cwd: '/test',
}); });
@@ -286,7 +286,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({ const generator = provider.executeQuery({
prompt: 'Test', prompt: 'Test',
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/test', cwd: '/test',
}); });
@@ -313,7 +313,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({ const generator = provider.executeQuery({
prompt: 'Test', prompt: 'Test',
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/test', cwd: '/test',
}); });
@@ -341,7 +341,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({ const generator = provider.executeQuery({
prompt: 'Test', prompt: 'Test',
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/test', cwd: '/test',
}); });
@@ -366,12 +366,12 @@ describe('claude-provider.ts', () => {
expect(models).toHaveLength(4); expect(models).toHaveLength(4);
}); });
it('should include Claude Opus 4.5', () => { it('should include Claude Opus 4.6', () => {
const models = provider.getAvailableModels(); const models = provider.getAvailableModels();
const opus = models.find((m) => m.id === 'claude-opus-4-5-20251101'); const opus = models.find((m) => m.id === 'claude-opus-4-6');
expect(opus).toBeDefined(); expect(opus).toBeDefined();
expect(opus?.name).toBe('Claude Opus 4.5'); expect(opus?.name).toBe('Claude Opus 4.6');
expect(opus?.provider).toBe('anthropic'); expect(opus?.provider).toBe('anthropic');
}); });
@@ -400,7 +400,7 @@ describe('claude-provider.ts', () => {
it('should mark Opus as default', () => { it('should mark Opus as default', () => {
const models = provider.getAvailableModels(); const models = provider.getAvailableModels();
const opus = models.find((m) => m.id === 'claude-opus-4-5-20251101'); const opus = models.find((m) => m.id === 'claude-opus-4-6');
expect(opus?.default).toBe(true); expect(opus?.default).toBe(true);
}); });

View File

@@ -54,8 +54,8 @@ describe('provider-factory.ts', () => {
describe('getProviderForModel', () => { describe('getProviderForModel', () => {
describe('Claude models (claude-* prefix)', () => { describe('Claude models (claude-* prefix)', () => {
it('should return ClaudeProvider for claude-opus-4-5-20251101', () => { it('should return ClaudeProvider for claude-opus-4-6', () => {
const provider = ProviderFactory.getProviderForModel('claude-opus-4-5-20251101'); const provider = ProviderFactory.getProviderForModel('claude-opus-4-6');
expect(provider).toBeInstanceOf(ClaudeProvider); expect(provider).toBeInstanceOf(ClaudeProvider);
}); });
@@ -70,7 +70,7 @@ describe('provider-factory.ts', () => {
}); });
it('should be case-insensitive for claude models', () => { it('should be case-insensitive for claude models', () => {
const provider = ProviderFactory.getProviderForModel('CLAUDE-OPUS-4-5-20251101'); const provider = ProviderFactory.getProviderForModel('CLAUDE-OPUS-4-6');
expect(provider).toBeInstanceOf(ClaudeProvider); expect(provider).toBeInstanceOf(ClaudeProvider);
}); });
}); });

View File

@@ -199,7 +199,7 @@ The agent is configured with:
```javascript ```javascript
{ {
model: "claude-opus-4-5-20251101", model: "claude-opus-4-6",
maxTurns: 20, maxTurns: 20,
cwd: workingDirectory, cwd: workingDirectory,
allowedTools: [ allowedTools: [

View File

@@ -69,6 +69,29 @@ export function SandboxRiskDialog({ open, onConfirm, onDeny }: SandboxRiskDialog
For safer operation, consider running Automaker in Docker. See the README for For safer operation, consider running Automaker in Docker. See the README for
instructions. instructions.
</p> </p>
<div className="bg-muted/50 border border-border rounded-lg p-4 space-y-2">
<p className="text-sm font-medium text-foreground">
Already running in Docker? Try these troubleshooting steps:
</p>
<ul className="text-sm text-muted-foreground list-disc list-inside space-y-1">
<li>
Ensure <code className="bg-muted px-1 rounded">IS_CONTAINERIZED=true</code> is
set in your docker-compose environment
</li>
<li>
Verify the server container has the environment variable:{' '}
<code className="bg-muted px-1 rounded">
docker exec automaker-server printenv IS_CONTAINERIZED
</code>
</li>
<li>Rebuild and restart containers if you recently changed the configuration</li>
<li>
Check the server logs for startup messages:{' '}
<code className="bg-muted px-1 rounded">docker-compose logs server</code>
</li>
</ul>
</div>
</div> </div>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>

View File

@@ -28,7 +28,7 @@ import { cn } from '@/lib/utils';
import { modelSupportsThinking } from '@/lib/utils'; import { modelSupportsThinking } from '@/lib/utils';
import { useAppStore, ThinkingLevel, FeatureImage, PlanningMode, Feature } from '@/store/app-store'; import { useAppStore, ThinkingLevel, FeatureImage, PlanningMode, Feature } from '@/store/app-store';
import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types'; import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types';
import { supportsReasoningEffort } from '@automaker/types'; import { supportsReasoningEffort, isAdaptiveThinkingModel } from '@automaker/types';
import { import {
PrioritySelector, PrioritySelector,
WorkModeSelector, WorkModeSelector,
@@ -264,7 +264,20 @@ export function AddFeatureDialog({
}, [planningMode]); }, [planningMode]);
const handleModelChange = (entry: PhaseModelEntry) => { const handleModelChange = (entry: PhaseModelEntry) => {
setModelEntry(entry); // Normalize thinking level when switching between adaptive and non-adaptive models
const isNewModelAdaptive =
typeof entry.model === 'string' && isAdaptiveThinkingModel(entry.model);
const currentLevel = entry.thinkingLevel || 'none';
if (isNewModelAdaptive && currentLevel !== 'none' && currentLevel !== 'adaptive') {
// Switching TO Opus 4.6 with a manual level -> auto-switch to 'adaptive'
setModelEntry({ ...entry, thinkingLevel: 'adaptive' });
} else if (!isNewModelAdaptive && currentLevel === 'adaptive') {
// Switching FROM Opus 4.6 with adaptive -> auto-switch to 'high'
setModelEntry({ ...entry, thinkingLevel: 'high' });
} else {
setModelEntry(entry);
}
}; };
const buildFeatureData = (): FeatureData | null => { const buildFeatureData = (): FeatureData | null => {

View File

@@ -167,7 +167,14 @@ export const ALL_MODELS: ModelOption[] = [
...COPILOT_MODELS, ...COPILOT_MODELS,
]; ];
export const THINKING_LEVELS: ThinkingLevel[] = ['none', 'low', 'medium', 'high', 'ultrathink']; export const THINKING_LEVELS: ThinkingLevel[] = [
'none',
'low',
'medium',
'high',
'ultrathink',
'adaptive',
];
export const THINKING_LEVEL_LABELS: Record<ThinkingLevel, string> = { export const THINKING_LEVEL_LABELS: Record<ThinkingLevel, string> = {
none: 'None', none: 'None',
@@ -175,6 +182,7 @@ export const THINKING_LEVEL_LABELS: Record<ThinkingLevel, string> = {
medium: 'Med', medium: 'Med',
high: 'High', high: 'High',
ultrathink: 'Ultra', ultrathink: 'Ultra',
adaptive: 'Adaptive',
}; };
/** /**

View File

@@ -2,19 +2,26 @@ import { Label } from '@/components/ui/label';
import { Brain } from 'lucide-react'; import { Brain } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { ThinkingLevel } from '@/store/app-store'; import { ThinkingLevel } from '@/store/app-store';
import { THINKING_LEVELS, THINKING_LEVEL_LABELS } from './model-constants'; import { THINKING_LEVEL_LABELS } from './model-constants';
import { getThinkingLevelsForModel } from '@automaker/types';
interface ThinkingLevelSelectorProps { interface ThinkingLevelSelectorProps {
selectedLevel: ThinkingLevel; selectedLevel: ThinkingLevel;
onLevelSelect: (level: ThinkingLevel) => void; onLevelSelect: (level: ThinkingLevel) => void;
testIdPrefix?: string; testIdPrefix?: string;
/** Model ID is required for correct thinking level filtering.
* Without it, adaptive thinking won't be available for Opus 4.6. */
model?: string;
} }
export function ThinkingLevelSelector({ export function ThinkingLevelSelector({
selectedLevel, selectedLevel,
onLevelSelect, onLevelSelect,
testIdPrefix = 'thinking-level', testIdPrefix = 'thinking-level',
model,
}: ThinkingLevelSelectorProps) { }: ThinkingLevelSelectorProps) {
const levels = getThinkingLevelsForModel(model || '');
return ( return (
<div className="space-y-2 pt-2 border-t border-border"> <div className="space-y-2 pt-2 border-t border-border">
<Label className="flex items-center gap-2 text-sm"> <Label className="flex items-center gap-2 text-sm">
@@ -22,7 +29,7 @@ export function ThinkingLevelSelector({
Thinking Level Thinking Level
</Label> </Label>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{THINKING_LEVELS.map((level) => ( {levels.map((level) => (
<button <button
key={level} key={level}
type="button" type="button"
@@ -40,7 +47,9 @@ export function ThinkingLevelSelector({
))} ))}
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Higher levels give more time to reason through complex problems. {levels.includes('adaptive')
? 'Adaptive thinking lets the model decide how much reasoning to use.'
: 'Higher levels give more time to reason through complex problems.'}
</p> </p>
</div> </div>
); );

View File

@@ -21,6 +21,7 @@ import {
isGroupSelected, isGroupSelected,
getSelectedVariant, getSelectedVariant,
codexModelHasThinking, codexModelHasThinking,
getThinkingLevelsForModel,
} from '@automaker/types'; } from '@automaker/types';
import { import {
CLAUDE_MODELS, CLAUDE_MODELS,
@@ -28,7 +29,6 @@ import {
OPENCODE_MODELS, OPENCODE_MODELS,
GEMINI_MODELS, GEMINI_MODELS,
COPILOT_MODELS, COPILOT_MODELS,
THINKING_LEVELS,
THINKING_LEVEL_LABELS, THINKING_LEVEL_LABELS,
REASONING_EFFORT_LEVELS, REASONING_EFFORT_LEVELS,
REASONING_EFFORT_LABELS, REASONING_EFFORT_LABELS,
@@ -1296,7 +1296,9 @@ export function PhaseModelSelector({
<div className="px-2 py-1 text-xs font-medium text-muted-foreground"> <div className="px-2 py-1 text-xs font-medium text-muted-foreground">
Thinking Level Thinking Level
</div> </div>
{THINKING_LEVELS.map((level) => ( {getThinkingLevelsForModel(
model.mapsToClaudeModel === 'opus' ? 'claude-opus' : model.id || ''
).map((level) => (
<button <button
key={level} key={level}
onClick={() => { onClick={() => {
@@ -1322,6 +1324,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'} {level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'} {level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'} {level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span> </span>
</div> </div>
{isSelected && currentThinking === level && ( {isSelected && currentThinking === level && (
@@ -1402,7 +1405,9 @@ export function PhaseModelSelector({
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground border-b border-border/50 mb-1"> <div className="px-2 py-1.5 text-xs font-medium text-muted-foreground border-b border-border/50 mb-1">
Thinking Level Thinking Level
</div> </div>
{THINKING_LEVELS.map((level) => ( {getThinkingLevelsForModel(
model.mapsToClaudeModel === 'opus' ? 'claude-opus' : model.id || ''
).map((level) => (
<button <button
key={level} key={level}
onClick={() => { onClick={() => {
@@ -1428,6 +1433,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'} {level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'} {level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'} {level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span> </span>
</div> </div>
{isSelected && currentThinking === level && ( {isSelected && currentThinking === level && (
@@ -1564,7 +1570,7 @@ export function PhaseModelSelector({
<div className="px-2 py-1 text-xs font-medium text-muted-foreground"> <div className="px-2 py-1 text-xs font-medium text-muted-foreground">
Thinking Level Thinking Level
</div> </div>
{THINKING_LEVELS.map((level) => ( {getThinkingLevelsForModel(model.id).map((level) => (
<button <button
key={level} key={level}
onClick={() => { onClick={() => {
@@ -1589,6 +1595,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'} {level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'} {level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'} {level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span> </span>
</div> </div>
{isSelected && currentThinking === level && ( {isSelected && currentThinking === level && (
@@ -1685,7 +1692,7 @@ export function PhaseModelSelector({
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground border-b border-border/50 mb-1"> <div className="px-2 py-1.5 text-xs font-medium text-muted-foreground border-b border-border/50 mb-1">
Thinking Level Thinking Level
</div> </div>
{THINKING_LEVELS.map((level) => ( {getThinkingLevelsForModel(model.id).map((level) => (
<button <button
key={level} key={level}
onClick={() => { onClick={() => {
@@ -1710,6 +1717,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'} {level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'} {level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'} {level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span> </span>
</div> </div>
{isSelected && currentThinking === level && ( {isSelected && currentThinking === level && (

View File

@@ -9,7 +9,7 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { CodexModelId } from '@automaker/types'; import { supportsReasoningEffort, type CodexModelId } from '@automaker/types';
import { OpenAIIcon } from '@/components/ui/provider-icon'; import { OpenAIIcon } from '@/components/ui/provider-icon';
interface CodexModelConfigurationProps { interface CodexModelConfigurationProps {
@@ -27,25 +27,30 @@ interface CodexModelInfo {
} }
const CODEX_MODEL_INFO: Record<CodexModelId, CodexModelInfo> = { const CODEX_MODEL_INFO: Record<CodexModelId, CodexModelInfo> = {
'codex-gpt-5.3-codex': {
id: 'codex-gpt-5.3-codex',
label: 'GPT-5.3-Codex',
description: 'Latest frontier agentic coding model',
},
'codex-gpt-5.2-codex': { 'codex-gpt-5.2-codex': {
id: 'codex-gpt-5.2-codex', id: 'codex-gpt-5.2-codex',
label: 'GPT-5.2-Codex', label: 'GPT-5.2-Codex',
description: 'Most advanced agentic coding model for complex software engineering', description: 'Frontier agentic coding model',
}, },
'codex-gpt-5.1-codex-max': { 'codex-gpt-5.1-codex-max': {
id: 'codex-gpt-5.1-codex-max', id: 'codex-gpt-5.1-codex-max',
label: 'GPT-5.1-Codex-Max', label: 'GPT-5.1-Codex-Max',
description: 'Optimized for long-horizon, agentic coding tasks in Codex', description: 'Codex-optimized flagship for deep and fast reasoning',
}, },
'codex-gpt-5.1-codex-mini': { 'codex-gpt-5.1-codex-mini': {
id: 'codex-gpt-5.1-codex-mini', id: 'codex-gpt-5.1-codex-mini',
label: 'GPT-5.1-Codex-Mini', label: 'GPT-5.1-Codex-Mini',
description: 'Smaller, more cost-effective version for faster workflows', description: 'Optimized for codex. Cheaper, faster, but less capable',
}, },
'codex-gpt-5.2': { 'codex-gpt-5.2': {
id: 'codex-gpt-5.2', id: 'codex-gpt-5.2',
label: 'GPT-5.2', label: 'GPT-5.2',
description: 'Best general agentic model for tasks across industries and domains', description: 'Latest frontier model with improvements across knowledge, reasoning and coding',
}, },
'codex-gpt-5.1': { 'codex-gpt-5.1': {
id: 'codex-gpt-5.1', id: 'codex-gpt-5.1',
@@ -157,13 +162,3 @@ export function CodexModelConfiguration({
</div> </div>
); );
} }
function supportsReasoningEffort(modelId: string): boolean {
const reasoningModels = [
'codex-gpt-5.2-codex',
'codex-gpt-5.1-codex-max',
'codex-gpt-5.2',
'codex-gpt-5.1',
];
return reasoningModels.includes(modelId);
}

View File

@@ -59,6 +59,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
// CLI Verification state // CLI Verification state
const [cliVerificationStatus, setCliVerificationStatus] = useState<VerificationStatus>('idle'); const [cliVerificationStatus, setCliVerificationStatus] = useState<VerificationStatus>('idle');
const [cliVerificationError, setCliVerificationError] = useState<string | null>(null); const [cliVerificationError, setCliVerificationError] = useState<string | null>(null);
const [cliAuthType, setCliAuthType] = useState<'oauth' | 'cli' | null>(null);
// API Key Verification state // API Key Verification state
const [apiKeyVerificationStatus, setApiKeyVerificationStatus] = const [apiKeyVerificationStatus, setApiKeyVerificationStatus] =
@@ -119,6 +120,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
const verifyCliAuth = useCallback(async () => { const verifyCliAuth = useCallback(async () => {
setCliVerificationStatus('verifying'); setCliVerificationStatus('verifying');
setCliVerificationError(null); setCliVerificationError(null);
setCliAuthType(null);
try { try {
const api = getElectronAPI(); const api = getElectronAPI();
@@ -138,12 +140,21 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
if (result.authenticated && !hasLimitReachedError) { if (result.authenticated && !hasLimitReachedError) {
setCliVerificationStatus('verified'); setCliVerificationStatus('verified');
// Store the auth type for displaying specific success message
const authType = result.authType === 'oauth' ? 'oauth' : 'cli';
setCliAuthType(authType);
setClaudeAuthStatus({ setClaudeAuthStatus({
authenticated: true, authenticated: true,
method: 'cli_authenticated', method: authType === 'oauth' ? 'oauth_token' : 'cli_authenticated',
hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false, hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false,
oauthTokenValid: authType === 'oauth',
}); });
toast.success('Claude CLI authentication verified!'); // Show specific success message based on auth type
if (authType === 'oauth') {
toast.success('Claude Code subscription detected and verified!');
} else {
toast.success('Claude CLI authentication verified!');
}
} else { } else {
setCliVerificationStatus('error'); setCliVerificationStatus('error');
setCliVerificationError( setCliVerificationError(
@@ -436,9 +447,15 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20"> <div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" /> <CheckCircle2 className="w-5 h-5 text-green-500" />
<div> <div>
<p className="font-medium text-foreground">CLI Authentication verified!</p> <p className="font-medium text-foreground">
{cliAuthType === 'oauth'
? 'Claude Code subscription verified!'
: 'CLI Authentication verified!'}
</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Your Claude CLI is working correctly. {cliAuthType === 'oauth'
? 'Your Claude Code subscription is active and ready to use.'
: 'Your Claude CLI is working correctly.'}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -27,18 +27,20 @@ export interface AgentTaskInfo {
/** /**
* Default model used by the feature executor * Default model used by the feature executor
*/ */
export const DEFAULT_MODEL = 'claude-opus-4-5-20251101'; export const DEFAULT_MODEL = 'claude-opus-4-6';
/** /**
* Formats a model name for display * Formats a model name for display
*/ */
export function formatModelName(model: string): string { export function formatModelName(model: string): string {
// Claude models // Claude models
if (model.includes('opus-4-6') || model === 'claude-opus') return 'Opus 4.6';
if (model.includes('opus')) return 'Opus 4.5'; if (model.includes('opus')) return 'Opus 4.5';
if (model.includes('sonnet')) return 'Sonnet 4.5'; if (model.includes('sonnet')) return 'Sonnet 4.5';
if (model.includes('haiku')) return 'Haiku 4.5'; if (model.includes('haiku')) return 'Haiku 4.5';
// Codex/GPT models - specific formatting // Codex/GPT models - specific formatting
if (model === 'codex-gpt-5.3-codex') return 'GPT-5.3 Codex';
if (model === 'codex-gpt-5.2-codex') return 'GPT-5.2 Codex'; if (model === 'codex-gpt-5.2-codex') return 'GPT-5.2 Codex';
if (model === 'codex-gpt-5.2') return 'GPT-5.2'; if (model === 'codex-gpt-5.2') return 'GPT-5.2';
if (model === 'codex-gpt-5.1-codex-max') return 'GPT-5.1 Max'; if (model === 'codex-gpt-5.1-codex-max') return 'GPT-5.1 Max';

View File

@@ -1442,6 +1442,7 @@ interface SetupAPI {
verifyClaudeAuth: (authMethod?: 'cli' | 'api_key') => Promise<{ verifyClaudeAuth: (authMethod?: 'cli' | 'api_key') => Promise<{
success: boolean; success: boolean;
authenticated: boolean; authenticated: boolean;
authType?: 'oauth' | 'api_key' | 'cli';
error?: string; error?: string;
}>; }>;
getGhStatus?: () => Promise<{ getGhStatus?: () => Promise<{

View File

@@ -1350,6 +1350,7 @@ export class HttpApiClient implements ElectronAPI {
): Promise<{ ): Promise<{
success: boolean; success: boolean;
authenticated: boolean; authenticated: boolean;
authType?: 'oauth' | 'api_key' | 'cli';
error?: string; error?: string;
}> => this.post('/api/setup/verify-claude-auth', { authMethod, apiKey }), }> => this.post('/api/setup/verify-claude-auth', { authMethod, apiKey }),

View File

@@ -21,9 +21,13 @@ services:
# - ~/.local/share/opencode:/home/automaker/.local/share/opencode # - ~/.local/share/opencode:/home/automaker/.local/share/opencode
# - ~/.config/opencode:/home/automaker/.config/opencode # - ~/.config/opencode:/home/automaker/.config/opencode
# Playwright browser cache - persists installed browsers across container restarts # ===== Playwright Browser Cache (Optional) =====
# Run 'npx playwright install --with-deps chromium' once, and it will persist # Playwright Chromium is PRE-INSTALLED in the Docker image for automated testing.
# Uncomment below to persist browser cache across container rebuilds (saves ~300MB download):
# - playwright-cache:/home/automaker/.cache/ms-playwright # - playwright-cache:/home/automaker/.cache/ms-playwright
#
# To update Playwright browsers manually:
# docker exec --user automaker -w /app automaker-server npx playwright install chromium
environment: environment:
# Set root directory for all projects and file operations # Set root directory for all projects and file operations
# Users can only create/open projects within this directory # Users can only create/open projects within this directory
@@ -37,6 +41,7 @@ services:
# - CURSOR_API_KEY=${CURSOR_API_KEY:-} # - CURSOR_API_KEY=${CURSOR_API_KEY:-}
volumes: volumes:
# Playwright cache volume (persists Chromium installs) # Playwright cache volume - optional, persists browser updates across container rebuilds
# Uncomment if you mounted the playwright-cache volume above
# playwright-cache: # playwright-cache:
# name: automaker-playwright-cache # name: automaker-playwright-cache

View File

@@ -142,7 +142,7 @@ const modelId = resolveModelString('sonnet'); // → 'claude-sonnet-4-20250514'
- `haiku``claude-haiku-4-5` (fast, simple tasks) - `haiku``claude-haiku-4-5` (fast, simple tasks)
- `sonnet``claude-sonnet-4-20250514` (balanced, recommended) - `sonnet``claude-sonnet-4-20250514` (balanced, recommended)
- `opus``claude-opus-4-5-20251101` (maximum capability) - `opus``claude-opus-4-6` (maximum capability)
### @automaker/dependency-resolver ### @automaker/dependency-resolver

View File

@@ -175,7 +175,7 @@ Uses `@anthropic-ai/claude-agent-sdk` for direct SDK integration.
Routes models that: Routes models that:
- Start with `"claude-"` (e.g., `"claude-opus-4-5-20251101"`) - Start with `"claude-"` (e.g., `"claude-opus-4-6"`)
- Are Claude aliases: `"opus"`, `"sonnet"`, `"haiku"` - Are Claude aliases: `"opus"`, `"sonnet"`, `"haiku"`
#### Authentication #### Authentication
@@ -191,7 +191,7 @@ const provider = new ClaudeProvider();
const stream = provider.executeQuery({ const stream = provider.executeQuery({
prompt: 'What is 2+2?', prompt: 'What is 2+2?',
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
cwd: '/project/path', cwd: '/project/path',
systemPrompt: 'You are a helpful assistant.', systemPrompt: 'You are a helpful assistant.',
maxTurns: 20, maxTurns: 20,
@@ -701,7 +701,7 @@ Test provider interaction with services:
```typescript ```typescript
describe('Provider Integration', () => { describe('Provider Integration', () => {
it('should work with AgentService', async () => { it('should work with AgentService', async () => {
const provider = ProviderFactory.getProviderForModel('claude-opus-4-5-20251101'); const provider = ProviderFactory.getProviderForModel('claude-opus-4-6');
// Test full workflow // Test full workflow
}); });

View File

@@ -213,7 +213,7 @@ Model alias mapping for Claude models.
export const CLAUDE_MODEL_MAP: Record<string, string> = { export const CLAUDE_MODEL_MAP: Record<string, string> = {
haiku: 'claude-haiku-4-5', haiku: 'claude-haiku-4-5',
sonnet: 'claude-sonnet-4-20250514', sonnet: 'claude-sonnet-4-20250514',
opus: 'claude-opus-4-5-20251101', opus: 'claude-opus-4-6',
} as const; } as const;
``` ```
@@ -223,7 +223,7 @@ Default models per provider.
```typescript ```typescript
export const DEFAULT_MODELS = { export const DEFAULT_MODELS = {
claude: 'claude-opus-4-5-20251101', claude: 'claude-opus-4-6',
openai: 'gpt-5.2', openai: 'gpt-5.2',
} as const; } as const;
``` ```
@@ -248,8 +248,8 @@ Resolve a model key/alias to a full model string.
import { resolveModelString, DEFAULT_MODELS } from '../lib/model-resolver.js'; import { resolveModelString, DEFAULT_MODELS } from '../lib/model-resolver.js';
resolveModelString('opus'); resolveModelString('opus');
// Returns: "claude-opus-4-5-20251101" // Returns: "claude-opus-4-6"
// Logs: "[ModelResolver] Resolved model alias: "opus" -> "claude-opus-4-5-20251101"" // Logs: "[ModelResolver] Resolved model alias: "opus" -> "claude-opus-4-6""
resolveModelString('gpt-5.2'); resolveModelString('gpt-5.2');
// Returns: "gpt-5.2" // Returns: "gpt-5.2"
@@ -260,8 +260,8 @@ resolveModelString('claude-sonnet-4-20250514');
// Logs: "[ModelResolver] Using full Claude model string: claude-sonnet-4-20250514" // Logs: "[ModelResolver] Using full Claude model string: claude-sonnet-4-20250514"
resolveModelString('invalid-model'); resolveModelString('invalid-model');
// Returns: "claude-opus-4-5-20251101" // Returns: "claude-opus-4-6"
// Logs: "[ModelResolver] Unknown model key "invalid-model", using default: "claude-opus-4-5-20251101"" // Logs: "[ModelResolver] Unknown model key "invalid-model", using default: "claude-opus-4-6""
``` ```
--- ---

View File

@@ -30,15 +30,15 @@ const model2 = resolveModelString('haiku');
// Returns: 'claude-haiku-4-5' // Returns: 'claude-haiku-4-5'
const model3 = resolveModelString('opus'); const model3 = resolveModelString('opus');
// Returns: 'claude-opus-4-5-20251101' // Returns: 'claude-opus-4-6'
// Use with custom default // Use with custom default
const model4 = resolveModelString(undefined, 'claude-sonnet-4-20250514'); const model4 = resolveModelString(undefined, 'claude-sonnet-4-20250514');
// Returns: 'claude-sonnet-4-20250514' (default) // Returns: 'claude-sonnet-4-20250514' (default)
// Direct model ID passthrough // Direct model ID passthrough
const model5 = resolveModelString('claude-opus-4-5-20251101'); const model5 = resolveModelString('claude-opus-4-6');
// Returns: 'claude-opus-4-5-20251101' (unchanged) // Returns: 'claude-opus-4-6' (unchanged)
``` ```
### Get Effective Model ### Get Effective Model
@@ -72,7 +72,7 @@ console.log(DEFAULT_MODELS.chat); // 'claude-sonnet-4-20250514'
// Model alias mappings // Model alias mappings
console.log(CLAUDE_MODEL_MAP.haiku); // 'claude-haiku-4-5' console.log(CLAUDE_MODEL_MAP.haiku); // 'claude-haiku-4-5'
console.log(CLAUDE_MODEL_MAP.sonnet); // 'claude-sonnet-4-20250514' console.log(CLAUDE_MODEL_MAP.sonnet); // 'claude-sonnet-4-20250514'
console.log(CLAUDE_MODEL_MAP.opus); // 'claude-opus-4-5-20251101' console.log(CLAUDE_MODEL_MAP.opus); // 'claude-opus-4-6'
``` ```
## Usage Example ## Usage Example
@@ -103,7 +103,7 @@ const feature: Feature = {
}; };
prepareFeatureExecution(feature); prepareFeatureExecution(feature);
// Output: Executing feature with model: claude-opus-4-5-20251101 // Output: Executing feature with model: claude-opus-4-6
``` ```
## Supported Models ## Supported Models
@@ -112,7 +112,7 @@ prepareFeatureExecution(feature);
- `haiku``claude-haiku-4-5` - `haiku``claude-haiku-4-5`
- `sonnet``claude-sonnet-4-20250514` - `sonnet``claude-sonnet-4-20250514`
- `opus``claude-opus-4-5-20251101` - `opus``claude-opus-4-6`
### Model Selection Guide ### Model Selection Guide

View File

@@ -484,12 +484,12 @@ describe('model-resolver', () => {
it('should handle full Claude model string in entry', () => { it('should handle full Claude model string in entry', () => {
const entry: PhaseModelEntry = { const entry: PhaseModelEntry = {
model: 'claude-opus-4-5-20251101', model: 'claude-opus-4-6',
thinkingLevel: 'high', thinkingLevel: 'high',
}; };
const result = resolvePhaseModel(entry); const result = resolvePhaseModel(entry);
expect(result.model).toBe('claude-opus-4-5-20251101'); expect(result.model).toBe('claude-opus-4-6');
expect(result.thinkingLevel).toBe('high'); expect(result.thinkingLevel).toBe('high');
}); });
}); });

View File

@@ -134,6 +134,8 @@ export {
findClaudeCliPath, findClaudeCliPath,
getClaudeAuthIndicators, getClaudeAuthIndicators,
type ClaudeAuthIndicators, type ClaudeAuthIndicators,
type FileCheckResult,
type DirectoryCheckResult,
findCodexCliPath, findCodexCliPath,
getCodexAuthIndicators, getCodexAuthIndicators,
type CodexAuthIndicators, type CodexAuthIndicators,

View File

@@ -25,6 +25,16 @@ import fs from 'fs/promises';
// System Tool Path Definitions // System Tool Path Definitions
// ============================================================================= // =============================================================================
/**
* Get NVM for Windows (nvm4w) symlink paths for a given CLI tool.
* Reused across getClaudeCliPaths, getCodexCliPaths, and getOpenCodeCliPaths.
*/
function getNvmWindowsCliPaths(cliName: string): string[] {
const nvmSymlink = process.env.NVM_SYMLINK;
if (!nvmSymlink) return [];
return [path.join(nvmSymlink, `${cliName}.cmd`), path.join(nvmSymlink, cliName)];
}
/** /**
* Get common paths where GitHub CLI might be installed * Get common paths where GitHub CLI might be installed
*/ */
@@ -60,6 +70,7 @@ export function getClaudeCliPaths(): string[] {
path.join(appData, 'npm', 'claude'), path.join(appData, 'npm', 'claude'),
path.join(appData, '.npm-global', 'bin', 'claude.cmd'), path.join(appData, '.npm-global', 'bin', 'claude.cmd'),
path.join(appData, '.npm-global', 'bin', 'claude'), path.join(appData, '.npm-global', 'bin', 'claude'),
...getNvmWindowsCliPaths('claude'),
]; ];
} }
@@ -141,6 +152,7 @@ export function getCodexCliPaths(): string[] {
// pnpm on Windows // pnpm on Windows
path.join(localAppData, 'pnpm', 'codex.cmd'), path.join(localAppData, 'pnpm', 'codex.cmd'),
path.join(localAppData, 'pnpm', 'codex'), path.join(localAppData, 'pnpm', 'codex'),
...getNvmWindowsCliPaths('codex'),
]; ];
} }
@@ -976,6 +988,27 @@ export async function findGitBashPath(): Promise<string | null> {
return findFirstExistingPath(getGitBashPaths()); return findFirstExistingPath(getGitBashPaths());
} }
/**
* Details about a file check performed during auth detection
*/
export interface FileCheckResult {
path: string;
exists: boolean;
readable: boolean;
error?: string;
}
/**
* Details about a directory check performed during auth detection
*/
export interface DirectoryCheckResult {
path: string;
exists: boolean;
readable: boolean;
entryCount: number;
error?: string;
}
/** /**
* Get Claude authentication status by checking various indicators * Get Claude authentication status by checking various indicators
*/ */
@@ -988,67 +1021,165 @@ export interface ClaudeAuthIndicators {
hasOAuthToken: boolean; hasOAuthToken: boolean;
hasApiKey: boolean; hasApiKey: boolean;
} | null; } | null;
/** Detailed information about what was checked */
checks: {
settingsFile: FileCheckResult;
statsCache: FileCheckResult & { hasDailyActivity?: boolean };
projectsDir: DirectoryCheckResult;
credentialFiles: FileCheckResult[];
};
} }
export async function getClaudeAuthIndicators(): Promise<ClaudeAuthIndicators> { export async function getClaudeAuthIndicators(): Promise<ClaudeAuthIndicators> {
const settingsPath = getClaudeSettingsPath();
const statsCachePath = getClaudeStatsCachePath();
const projectsDir = getClaudeProjectsDir();
const credentialPaths = getClaudeCredentialPaths();
// Initialize checks with paths
const settingsFileCheck: FileCheckResult = {
path: settingsPath,
exists: false,
readable: false,
};
const statsCacheCheck: FileCheckResult & { hasDailyActivity?: boolean } = {
path: statsCachePath,
exists: false,
readable: false,
};
const projectsDirCheck: DirectoryCheckResult = {
path: projectsDir,
exists: false,
readable: false,
entryCount: 0,
};
const credentialFileChecks: FileCheckResult[] = credentialPaths.map((p) => ({
path: p,
exists: false,
readable: false,
}));
const result: ClaudeAuthIndicators = { const result: ClaudeAuthIndicators = {
hasCredentialsFile: false, hasCredentialsFile: false,
hasSettingsFile: false, hasSettingsFile: false,
hasStatsCacheWithActivity: false, hasStatsCacheWithActivity: false,
hasProjectsSessions: false, hasProjectsSessions: false,
credentials: null, credentials: null,
checks: {
settingsFile: settingsFileCheck,
statsCache: statsCacheCheck,
projectsDir: projectsDirCheck,
credentialFiles: credentialFileChecks,
},
}; };
// Check settings file // Check settings file
// First check existence, then try to read to confirm it's actually readable
try { try {
if (await systemPathAccess(getClaudeSettingsPath())) { if (await systemPathAccess(settingsPath)) {
result.hasSettingsFile = true; settingsFileCheck.exists = true;
// Try to actually read the file to confirm read permissions
try {
await systemPathReadFile(settingsPath);
settingsFileCheck.readable = true;
result.hasSettingsFile = true;
} catch (readErr) {
// File exists but cannot be read (permission denied, etc.)
settingsFileCheck.readable = false;
settingsFileCheck.error = `Cannot read: ${readErr instanceof Error ? readErr.message : String(readErr)}`;
}
} }
} catch { } catch (err) {
// Ignore errors settingsFileCheck.error = err instanceof Error ? err.message : String(err);
} }
// Check stats cache for recent activity // Check stats cache for recent activity
try { try {
const statsContent = await systemPathReadFile(getClaudeStatsCachePath()); const statsContent = await systemPathReadFile(statsCachePath);
const stats = JSON.parse(statsContent); statsCacheCheck.exists = true;
if (stats.dailyActivity && stats.dailyActivity.length > 0) { statsCacheCheck.readable = true;
result.hasStatsCacheWithActivity = true; try {
const stats = JSON.parse(statsContent);
if (stats.dailyActivity && stats.dailyActivity.length > 0) {
statsCacheCheck.hasDailyActivity = true;
result.hasStatsCacheWithActivity = true;
} else {
statsCacheCheck.hasDailyActivity = false;
}
} catch (parseErr) {
statsCacheCheck.error = `JSON parse error: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`;
}
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
statsCacheCheck.exists = false;
} else {
statsCacheCheck.error = err instanceof Error ? err.message : String(err);
} }
} catch {
// Ignore errors
} }
// Check for sessions in projects directory // Check for sessions in projects directory
try { try {
const sessions = await systemPathReaddir(getClaudeProjectsDir()); const sessions = await systemPathReaddir(projectsDir);
projectsDirCheck.exists = true;
projectsDirCheck.readable = true;
projectsDirCheck.entryCount = sessions.length;
if (sessions.length > 0) { if (sessions.length > 0) {
result.hasProjectsSessions = true; result.hasProjectsSessions = true;
} }
} catch { } catch (err) {
// Ignore errors if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
projectsDirCheck.exists = false;
} else {
projectsDirCheck.error = err instanceof Error ? err.message : String(err);
}
} }
// Check credentials files // Check credentials files
const credentialPaths = getClaudeCredentialPaths(); // We iterate through all credential paths and only stop when we find a file
for (const credPath of credentialPaths) { // that contains actual credentials (OAuth tokens or API keys). An empty or
// token-less file should not prevent checking subsequent credential paths.
for (let i = 0; i < credentialPaths.length; i++) {
const credPath = credentialPaths[i];
const credCheck = credentialFileChecks[i];
try { try {
const content = await systemPathReadFile(credPath); const content = await systemPathReadFile(credPath);
const credentials = JSON.parse(content); credCheck.exists = true;
result.hasCredentialsFile = true; credCheck.readable = true;
// Support multiple credential formats: try {
// 1. Claude Code CLI format: { claudeAiOauth: { accessToken, refreshToken } } const credentials = JSON.parse(content);
// 2. Legacy format: { oauth_token } or { access_token } // Support multiple credential formats:
// 3. API key format: { api_key } // 1. Claude Code CLI format: { claudeAiOauth: { accessToken, refreshToken } }
const hasClaudeOauth = !!credentials.claudeAiOauth?.accessToken; // 2. Legacy format: { oauth_token } or { access_token }
const hasLegacyOauth = !!(credentials.oauth_token || credentials.access_token); // 3. API key format: { api_key }
result.credentials = { const hasClaudeOauth = !!credentials.claudeAiOauth?.accessToken;
hasOAuthToken: hasClaudeOauth || hasLegacyOauth, const hasLegacyOauth = !!(credentials.oauth_token || credentials.access_token);
hasApiKey: !!credentials.api_key, const hasOAuthToken = hasClaudeOauth || hasLegacyOauth;
}; const hasApiKey = !!credentials.api_key;
break;
} catch { // Only consider this a valid credentials file if it actually contains tokens
// Continue to next path // An empty JSON file ({}) or file without tokens should not stop us from
// checking subsequent credential paths
if (hasOAuthToken || hasApiKey) {
result.hasCredentialsFile = true;
result.credentials = {
hasOAuthToken,
hasApiKey,
};
break; // Found valid credentials, stop searching
}
// File exists and is valid JSON but contains no tokens - continue checking other paths
} catch (parseErr) {
credCheck.error = `JSON parse error: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`;
}
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
credCheck.exists = false;
} else {
credCheck.error = err instanceof Error ? err.message : String(err);
}
} }
} }
@@ -1142,6 +1273,7 @@ export function getOpenCodeCliPaths(): string[] {
// Go installation (if OpenCode is a Go binary) // Go installation (if OpenCode is a Go binary)
path.join(homeDir, 'go', 'bin', 'opencode.exe'), path.join(homeDir, 'go', 'bin', 'opencode.exe'),
path.join(process.env.GOPATH || path.join(homeDir, 'go'), 'bin', 'opencode.exe'), path.join(process.env.GOPATH || path.join(homeDir, 'go'), 'bin', 'opencode.exe'),
...getNvmWindowsCliPaths('opencode'),
]; ];
} }

View File

@@ -0,0 +1,761 @@
/**
* Unit tests for OAuth credential detection scenarios
*
* Tests the various Claude credential detection formats including:
* - Claude Code CLI OAuth format (claudeAiOauth)
* - Legacy OAuth token format (oauth_token, access_token)
* - API key format (api_key)
* - Invalid/malformed credential files
*
* These tests use real temp directories to avoid complex fs mocking issues.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
describe('OAuth Credential Detection', () => {
let tempDir: string;
let originalHomedir: () => string;
let mockClaudeDir: string;
let mockCodexDir: string;
let mockOpenCodeDir: string;
beforeEach(async () => {
// Reset modules to get fresh state
vi.resetModules();
// Create a temporary directory
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oauth-detection-test-'));
// Create mock home directory structure
mockClaudeDir = path.join(tempDir, '.claude');
mockCodexDir = path.join(tempDir, '.codex');
mockOpenCodeDir = path.join(tempDir, '.local', 'share', 'opencode');
await fs.mkdir(mockClaudeDir, { recursive: true });
await fs.mkdir(mockCodexDir, { recursive: true });
await fs.mkdir(mockOpenCodeDir, { recursive: true });
// Mock os.homedir to return our temp directory
originalHomedir = os.homedir;
vi.spyOn(os, 'homedir').mockReturnValue(tempDir);
});
afterEach(async () => {
vi.restoreAllMocks();
// Clean up temp directory
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
describe('getClaudeAuthIndicators', () => {
it('should detect Claude Code CLI OAuth format (claudeAiOauth)', async () => {
const credentialsContent = JSON.stringify({
claudeAiOauth: {
accessToken: 'oauth-access-token-12345',
refreshToken: 'oauth-refresh-token-67890',
expiresAt: Date.now() + 3600000,
},
});
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), credentialsContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasCredentialsFile).toBe(true);
expect(indicators.credentials).not.toBeNull();
expect(indicators.credentials?.hasOAuthToken).toBe(true);
expect(indicators.credentials?.hasApiKey).toBe(false);
});
it('should detect legacy OAuth token format (oauth_token)', async () => {
const credentialsContent = JSON.stringify({
oauth_token: 'legacy-oauth-token-abcdef',
});
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), credentialsContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasCredentialsFile).toBe(true);
expect(indicators.credentials?.hasOAuthToken).toBe(true);
expect(indicators.credentials?.hasApiKey).toBe(false);
});
it('should detect legacy access_token format', async () => {
const credentialsContent = JSON.stringify({
access_token: 'legacy-access-token-xyz',
});
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), credentialsContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasCredentialsFile).toBe(true);
expect(indicators.credentials?.hasOAuthToken).toBe(true);
expect(indicators.credentials?.hasApiKey).toBe(false);
});
it('should detect API key format', async () => {
const credentialsContent = JSON.stringify({
api_key: 'sk-ant-api03-xxxxxxxxxxxx',
});
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), credentialsContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasCredentialsFile).toBe(true);
expect(indicators.credentials?.hasOAuthToken).toBe(false);
expect(indicators.credentials?.hasApiKey).toBe(true);
});
it('should detect both OAuth and API key when present', async () => {
const credentialsContent = JSON.stringify({
claudeAiOauth: {
accessToken: 'oauth-token',
refreshToken: 'refresh-token',
},
api_key: 'sk-ant-api03-xxxxxxxxxxxx',
});
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), credentialsContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasCredentialsFile).toBe(true);
expect(indicators.credentials?.hasOAuthToken).toBe(true);
expect(indicators.credentials?.hasApiKey).toBe(true);
});
it('should handle missing credentials file gracefully', async () => {
// No credentials file created
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasCredentialsFile).toBe(false);
expect(indicators.credentials).toBeNull();
expect(indicators.checks.credentialFiles).toBeDefined();
expect(indicators.checks.credentialFiles.length).toBeGreaterThan(0);
expect(indicators.checks.credentialFiles[0].exists).toBe(false);
});
it('should handle malformed JSON in credentials file', async () => {
const malformedContent = '{ invalid json }';
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), malformedContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// File exists but parsing fails
expect(indicators.hasCredentialsFile).toBe(false);
expect(indicators.credentials).toBeNull();
expect(indicators.checks.credentialFiles[0].exists).toBe(true);
expect(indicators.checks.credentialFiles[0].error).toContain('JSON parse error');
});
it('should handle empty credentials file', async () => {
const emptyContent = JSON.stringify({});
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), emptyContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// Empty credentials file ({}) should NOT be treated as having credentials
// because it contains no actual tokens. This allows the system to continue
// checking subsequent credential paths that might have valid tokens.
expect(indicators.hasCredentialsFile).toBe(false);
expect(indicators.credentials).toBeNull();
// But the file should still show as existing and readable in the checks
expect(indicators.checks.credentialFiles[0].exists).toBe(true);
expect(indicators.checks.credentialFiles[0].readable).toBe(true);
});
it('should handle credentials file with null values', async () => {
const nullContent = JSON.stringify({
claudeAiOauth: null,
api_key: null,
oauth_token: null,
});
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), nullContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// File with all null values should NOT be treated as having credentials
// because null values are not valid tokens
expect(indicators.hasCredentialsFile).toBe(false);
expect(indicators.credentials).toBeNull();
});
it('should handle credentials with empty string values', async () => {
const emptyStrings = JSON.stringify({
claudeAiOauth: {
accessToken: '',
refreshToken: '',
},
api_key: '',
});
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), emptyStrings);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// Empty strings should NOT be treated as having credentials
// This allows checking subsequent credential paths for valid tokens
expect(indicators.hasCredentialsFile).toBe(false);
expect(indicators.credentials).toBeNull();
});
it('should detect settings file presence', async () => {
await fs.writeFile(
path.join(mockClaudeDir, 'settings.json'),
JSON.stringify({ theme: 'dark' })
);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasSettingsFile).toBe(true);
expect(indicators.checks.settingsFile.exists).toBe(true);
expect(indicators.checks.settingsFile.readable).toBe(true);
});
it('should detect stats cache with activity', async () => {
const statsContent = JSON.stringify({
dailyActivity: [
{ date: '2025-01-15', messagesCount: 10 },
{ date: '2025-01-16', messagesCount: 5 },
],
});
await fs.writeFile(path.join(mockClaudeDir, 'stats-cache.json'), statsContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasStatsCacheWithActivity).toBe(true);
expect(indicators.checks.statsCache.exists).toBe(true);
expect(indicators.checks.statsCache.hasDailyActivity).toBe(true);
});
it('should detect stats cache without activity', async () => {
const statsContent = JSON.stringify({
dailyActivity: [],
});
await fs.writeFile(path.join(mockClaudeDir, 'stats-cache.json'), statsContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasStatsCacheWithActivity).toBe(false);
expect(indicators.checks.statsCache.exists).toBe(true);
expect(indicators.checks.statsCache.hasDailyActivity).toBe(false);
});
it('should detect project sessions', async () => {
const projectsDir = path.join(mockClaudeDir, 'projects');
await fs.mkdir(projectsDir, { recursive: true });
await fs.mkdir(path.join(projectsDir, 'session-1'));
await fs.mkdir(path.join(projectsDir, 'session-2'));
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasProjectsSessions).toBe(true);
expect(indicators.checks.projectsDir.exists).toBe(true);
expect(indicators.checks.projectsDir.entryCount).toBe(2);
});
it('should return comprehensive check details', async () => {
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// Verify all check detail objects are present
expect(indicators.checks).toBeDefined();
expect(indicators.checks.settingsFile).toBeDefined();
expect(indicators.checks.settingsFile.path).toContain('settings.json');
expect(indicators.checks.statsCache).toBeDefined();
expect(indicators.checks.statsCache.path).toContain('stats-cache.json');
expect(indicators.checks.projectsDir).toBeDefined();
expect(indicators.checks.projectsDir.path).toContain('projects');
expect(indicators.checks.credentialFiles).toBeDefined();
expect(Array.isArray(indicators.checks.credentialFiles)).toBe(true);
});
it('should try both .credentials.json and credentials.json paths', async () => {
// Write to credentials.json (without leading dot)
const credentialsContent = JSON.stringify({
api_key: 'sk-test-key',
});
await fs.writeFile(path.join(mockClaudeDir, 'credentials.json'), credentialsContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// Should find credentials in the second path
expect(indicators.hasCredentialsFile).toBe(true);
expect(indicators.credentials?.hasApiKey).toBe(true);
});
it('should prefer first credentials file if both exist', async () => {
// Write OAuth to .credentials.json (first path checked)
await fs.writeFile(
path.join(mockClaudeDir, '.credentials.json'),
JSON.stringify({
claudeAiOauth: {
accessToken: 'oauth-token',
refreshToken: 'refresh-token',
},
})
);
// Write API key to credentials.json (second path)
await fs.writeFile(
path.join(mockClaudeDir, 'credentials.json'),
JSON.stringify({
api_key: 'sk-test-key',
})
);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// Should use first file (.credentials.json) which has OAuth
expect(indicators.hasCredentialsFile).toBe(true);
expect(indicators.credentials?.hasOAuthToken).toBe(true);
expect(indicators.credentials?.hasApiKey).toBe(false);
});
it('should check second credentials file if first file has no tokens', async () => {
// Write empty/token-less content to .credentials.json (first path checked)
// This tests the bug fix: previously, an empty JSON file would stop the search
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), JSON.stringify({}));
// Write actual credentials to credentials.json (second path)
await fs.writeFile(
path.join(mockClaudeDir, 'credentials.json'),
JSON.stringify({
api_key: 'sk-test-key-from-second-file',
})
);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// Should find credentials in second file since first file has no tokens
expect(indicators.hasCredentialsFile).toBe(true);
expect(indicators.credentials?.hasApiKey).toBe(true);
});
});
describe('getCodexAuthIndicators', () => {
it('should detect OAuth token in Codex auth file', async () => {
const authContent = JSON.stringify({
access_token: 'codex-oauth-token-12345',
});
await fs.writeFile(path.join(mockCodexDir, 'auth.json'), authContent);
const { getCodexAuthIndicators } = await import('../src/system-paths');
const indicators = await getCodexAuthIndicators();
expect(indicators.hasAuthFile).toBe(true);
expect(indicators.hasOAuthToken).toBe(true);
expect(indicators.hasApiKey).toBe(false);
});
it('should detect API key in Codex auth file', async () => {
const authContent = JSON.stringify({
OPENAI_API_KEY: 'sk-xxxxxxxxxxxxxxxx',
});
await fs.writeFile(path.join(mockCodexDir, 'auth.json'), authContent);
const { getCodexAuthIndicators } = await import('../src/system-paths');
const indicators = await getCodexAuthIndicators();
expect(indicators.hasAuthFile).toBe(true);
expect(indicators.hasOAuthToken).toBe(false);
expect(indicators.hasApiKey).toBe(true);
});
it('should detect nested tokens in Codex auth file', async () => {
const authContent = JSON.stringify({
tokens: {
oauth_token: 'nested-oauth-token',
},
});
await fs.writeFile(path.join(mockCodexDir, 'auth.json'), authContent);
const { getCodexAuthIndicators } = await import('../src/system-paths');
const indicators = await getCodexAuthIndicators();
expect(indicators.hasAuthFile).toBe(true);
expect(indicators.hasOAuthToken).toBe(true);
});
it('should handle missing Codex auth file', async () => {
// No auth file created
const { getCodexAuthIndicators } = await import('../src/system-paths');
const indicators = await getCodexAuthIndicators();
expect(indicators.hasAuthFile).toBe(false);
expect(indicators.hasOAuthToken).toBe(false);
expect(indicators.hasApiKey).toBe(false);
});
it('should detect api_key field in Codex auth', async () => {
const authContent = JSON.stringify({
api_key: 'sk-api-key-value',
});
await fs.writeFile(path.join(mockCodexDir, 'auth.json'), authContent);
const { getCodexAuthIndicators } = await import('../src/system-paths');
const indicators = await getCodexAuthIndicators();
expect(indicators.hasAuthFile).toBe(true);
expect(indicators.hasApiKey).toBe(true);
});
});
describe('getOpenCodeAuthIndicators', () => {
it('should detect provider-specific OAuth credentials', async () => {
const authContent = JSON.stringify({
anthropic: {
type: 'oauth',
access: 'oauth-access-token',
refresh: 'oauth-refresh-token',
},
});
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
const indicators = await getOpenCodeAuthIndicators();
expect(indicators.hasAuthFile).toBe(true);
expect(indicators.hasOAuthToken).toBe(true);
expect(indicators.hasApiKey).toBe(false);
});
it('should detect GitHub Copilot refresh token as OAuth', async () => {
const authContent = JSON.stringify({
'github-copilot': {
type: 'oauth',
access: '', // Empty access token
refresh: 'gh-refresh-token', // But has refresh token
},
});
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
const indicators = await getOpenCodeAuthIndicators();
expect(indicators.hasAuthFile).toBe(true);
expect(indicators.hasOAuthToken).toBe(true);
});
it('should detect provider-specific API key credentials', async () => {
const authContent = JSON.stringify({
openai: {
type: 'api_key',
key: 'sk-xxxxxxxxxxxx',
},
});
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
const indicators = await getOpenCodeAuthIndicators();
expect(indicators.hasAuthFile).toBe(true);
expect(indicators.hasOAuthToken).toBe(false);
expect(indicators.hasApiKey).toBe(true);
});
it('should detect multiple providers', async () => {
const authContent = JSON.stringify({
anthropic: {
type: 'oauth',
access: 'anthropic-token',
refresh: 'refresh-token',
},
openai: {
type: 'api_key',
key: 'sk-xxxxxxxxxxxx',
},
});
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
const indicators = await getOpenCodeAuthIndicators();
expect(indicators.hasAuthFile).toBe(true);
expect(indicators.hasOAuthToken).toBe(true);
expect(indicators.hasApiKey).toBe(true);
});
it('should handle missing OpenCode auth file', async () => {
// No auth file created
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
const indicators = await getOpenCodeAuthIndicators();
expect(indicators.hasAuthFile).toBe(false);
expect(indicators.hasOAuthToken).toBe(false);
expect(indicators.hasApiKey).toBe(false);
});
it('should handle legacy top-level OAuth keys', async () => {
const authContent = JSON.stringify({
access_token: 'legacy-access-token',
});
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
const indicators = await getOpenCodeAuthIndicators();
expect(indicators.hasAuthFile).toBe(true);
expect(indicators.hasOAuthToken).toBe(true);
});
it('should detect copilot provider OAuth', async () => {
const authContent = JSON.stringify({
copilot: {
type: 'oauth',
access: 'copilot-access-token',
refresh: 'copilot-refresh-token',
},
});
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
const indicators = await getOpenCodeAuthIndicators();
expect(indicators.hasAuthFile).toBe(true);
expect(indicators.hasOAuthToken).toBe(true);
});
});
describe('Credential path helpers', () => {
it('should return correct Claude credential paths', async () => {
const { getClaudeCredentialPaths, getClaudeConfigDir } = await import('../src/system-paths');
const configDir = getClaudeConfigDir();
expect(configDir).toContain('.claude');
const credPaths = getClaudeCredentialPaths();
expect(credPaths.length).toBeGreaterThan(0);
expect(credPaths.some((p) => p.includes('.credentials.json'))).toBe(true);
expect(credPaths.some((p) => p.includes('credentials.json'))).toBe(true);
});
it('should return correct Codex auth path', async () => {
const { getCodexAuthPath, getCodexConfigDir } = await import('../src/system-paths');
const configDir = getCodexConfigDir();
expect(configDir).toContain('.codex');
const authPath = getCodexAuthPath();
expect(authPath).toContain('.codex');
expect(authPath).toContain('auth.json');
});
it('should return correct OpenCode auth path', async () => {
const { getOpenCodeAuthPath, getOpenCodeConfigDir } = await import('../src/system-paths');
const configDir = getOpenCodeConfigDir();
expect(configDir).toContain('opencode');
const authPath = getOpenCodeAuthPath();
expect(authPath).toContain('opencode');
expect(authPath).toContain('auth.json');
});
});
describe('Edge cases for credential detection', () => {
it('should handle credentials file with unexpected structure', async () => {
const unexpectedContent = JSON.stringify({
someUnexpectedKey: 'value',
nested: {
deeply: {
unexpected: true,
},
},
});
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), unexpectedContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// File with unexpected structure but no valid tokens should NOT be treated as having credentials
expect(indicators.hasCredentialsFile).toBe(false);
expect(indicators.credentials).toBeNull();
});
it('should handle array instead of object in credentials', async () => {
const arrayContent = JSON.stringify(['token1', 'token2']);
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), arrayContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// Array is valid JSON but wrong structure - no valid tokens, so not treated as credentials file
expect(indicators.hasCredentialsFile).toBe(false);
expect(indicators.credentials).toBeNull();
});
it('should handle numeric values in credential fields', async () => {
const numericContent = JSON.stringify({
api_key: 12345,
oauth_token: 67890,
});
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), numericContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// Note: Current implementation uses JavaScript truthiness which accepts numbers
// This documents the actual behavior - ideally would validate string type
expect(indicators.hasCredentialsFile).toBe(true);
// The implementation checks truthiness, not strict string type
expect(indicators.credentials?.hasOAuthToken).toBe(true);
expect(indicators.credentials?.hasApiKey).toBe(true);
});
it('should handle boolean values in credential fields', async () => {
const booleanContent = JSON.stringify({
api_key: true,
oauth_token: false,
});
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), booleanContent);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
// Note: Current implementation uses JavaScript truthiness
// api_key: true is truthy, oauth_token: false is falsy
expect(indicators.hasCredentialsFile).toBe(true);
expect(indicators.credentials?.hasOAuthToken).toBe(false); // false is falsy
expect(indicators.credentials?.hasApiKey).toBe(true); // true is truthy
});
it('should handle malformed stats-cache.json gracefully', async () => {
await fs.writeFile(path.join(mockClaudeDir, 'stats-cache.json'), '{ invalid json }');
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasStatsCacheWithActivity).toBe(false);
expect(indicators.checks.statsCache.exists).toBe(true);
expect(indicators.checks.statsCache.error).toBeDefined();
});
it('should handle empty projects directory', async () => {
const projectsDir = path.join(mockClaudeDir, 'projects');
await fs.mkdir(projectsDir, { recursive: true });
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasProjectsSessions).toBe(false);
expect(indicators.checks.projectsDir.exists).toBe(true);
expect(indicators.checks.projectsDir.entryCount).toBe(0);
});
});
describe('Combined authentication scenarios', () => {
it('should detect CLI authenticated state with settings + sessions', async () => {
// Create settings file
await fs.writeFile(
path.join(mockClaudeDir, 'settings.json'),
JSON.stringify({ theme: 'dark' })
);
// Create projects directory with sessions
const projectsDir = path.join(mockClaudeDir, 'projects');
await fs.mkdir(projectsDir, { recursive: true });
await fs.mkdir(path.join(projectsDir, 'session-1'));
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasSettingsFile).toBe(true);
expect(indicators.hasProjectsSessions).toBe(true);
});
it('should detect recent activity indicating working auth', async () => {
// Create stats cache with recent activity
await fs.writeFile(
path.join(mockClaudeDir, 'stats-cache.json'),
JSON.stringify({
dailyActivity: [{ date: new Date().toISOString().split('T')[0], messagesCount: 10 }],
})
);
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasStatsCacheWithActivity).toBe(true);
});
it('should handle complete auth setup', async () => {
// Create all auth indicators
await fs.writeFile(
path.join(mockClaudeDir, '.credentials.json'),
JSON.stringify({
claudeAiOauth: {
accessToken: 'token',
refreshToken: 'refresh',
},
})
);
await fs.writeFile(
path.join(mockClaudeDir, 'settings.json'),
JSON.stringify({ theme: 'dark' })
);
await fs.writeFile(
path.join(mockClaudeDir, 'stats-cache.json'),
JSON.stringify({ dailyActivity: [{ date: '2025-01-15', messagesCount: 5 }] })
);
const projectsDir = path.join(mockClaudeDir, 'projects');
await fs.mkdir(projectsDir, { recursive: true });
await fs.mkdir(path.join(projectsDir, 'session-1'));
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
expect(indicators.hasCredentialsFile).toBe(true);
expect(indicators.hasSettingsFile).toBe(true);
expect(indicators.hasStatsCacheWithActivity).toBe(true);
expect(indicators.hasProjectsSessions).toBe(true);
expect(indicators.credentials?.hasOAuthToken).toBe(true);
});
});
});

View File

@@ -6,6 +6,7 @@
* IMPORTANT: All Codex models use 'codex-' prefix to distinguish from Cursor CLI models * IMPORTANT: All Codex models use 'codex-' prefix to distinguish from Cursor CLI models
*/ */
export type CodexModelId = export type CodexModelId =
| 'codex-gpt-5.3-codex'
| 'codex-gpt-5.2-codex' | 'codex-gpt-5.2-codex'
| 'codex-gpt-5.1-codex-max' | 'codex-gpt-5.1-codex-max'
| 'codex-gpt-5.1-codex-mini' | 'codex-gpt-5.1-codex-mini'
@@ -29,31 +30,38 @@ export interface CodexModelConfig {
* All keys use 'codex-' prefix to distinguish from Cursor CLI models * All keys use 'codex-' prefix to distinguish from Cursor CLI models
*/ */
export const CODEX_MODEL_CONFIG_MAP: Record<CodexModelId, CodexModelConfig> = { export const CODEX_MODEL_CONFIG_MAP: Record<CodexModelId, CodexModelConfig> = {
'codex-gpt-5.3-codex': {
id: 'codex-gpt-5.3-codex',
label: 'GPT-5.3-Codex',
description: 'Latest frontier agentic coding model',
hasThinking: true,
supportsVision: true,
},
'codex-gpt-5.2-codex': { 'codex-gpt-5.2-codex': {
id: 'codex-gpt-5.2-codex', id: 'codex-gpt-5.2-codex',
label: 'GPT-5.2-Codex', label: 'GPT-5.2-Codex',
description: 'Most advanced agentic coding model for complex software engineering', description: 'Frontier agentic coding model',
hasThinking: true, hasThinking: true,
supportsVision: true, supportsVision: true,
}, },
'codex-gpt-5.1-codex-max': { 'codex-gpt-5.1-codex-max': {
id: 'codex-gpt-5.1-codex-max', id: 'codex-gpt-5.1-codex-max',
label: 'GPT-5.1-Codex-Max', label: 'GPT-5.1-Codex-Max',
description: 'Optimized for long-horizon, agentic coding tasks in Codex', description: 'Codex-optimized flagship for deep and fast reasoning',
hasThinking: true, hasThinking: true,
supportsVision: true, supportsVision: true,
}, },
'codex-gpt-5.1-codex-mini': { 'codex-gpt-5.1-codex-mini': {
id: 'codex-gpt-5.1-codex-mini', id: 'codex-gpt-5.1-codex-mini',
label: 'GPT-5.1-Codex-Mini', label: 'GPT-5.1-Codex-Mini',
description: 'Smaller, more cost-effective version for faster workflows', description: 'Optimized for codex. Cheaper, faster, but less capable',
hasThinking: false, hasThinking: false,
supportsVision: true, supportsVision: true,
}, },
'codex-gpt-5.2': { 'codex-gpt-5.2': {
id: 'codex-gpt-5.2', id: 'codex-gpt-5.2',
label: 'GPT-5.2 (Codex)', label: 'GPT-5.2 (Codex)',
description: 'Best general agentic model for tasks across industries and domains via Codex', description: 'Latest frontier model with improvements across knowledge, reasoning and coding',
hasThinking: true, hasThinking: true,
supportsVision: true, supportsVision: true,
}, },

View File

@@ -46,6 +46,7 @@ export type EventType =
| 'dev-server:started' | 'dev-server:started'
| 'dev-server:output' | 'dev-server:output'
| 'dev-server:stopped' | 'dev-server:stopped'
| 'dev-server:url-detected'
| 'test-runner:started' | 'test-runner:started'
| 'test-runner:progress' | 'test-runner:progress'
| 'test-runner:output' | 'test-runner:output'

View File

@@ -196,6 +196,8 @@ export {
PROJECT_SETTINGS_VERSION, PROJECT_SETTINGS_VERSION,
THINKING_TOKEN_BUDGET, THINKING_TOKEN_BUDGET,
getThinkingTokenBudget, getThinkingTokenBudget,
isAdaptiveThinkingModel,
getThinkingLevelsForModel,
// Event hook constants // Event hook constants
EVENT_HOOK_TRIGGER_LABELS, EVENT_HOOK_TRIGGER_LABELS,
// Claude-compatible provider templates (new) // Claude-compatible provider templates (new)

View File

@@ -72,10 +72,18 @@ export const CLAUDE_MODELS: ModelOption[] = [
* Official models from https://developers.openai.com/codex/models/ * Official models from https://developers.openai.com/codex/models/
*/ */
export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [ export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
{
id: CODEX_MODEL_MAP.gpt53Codex,
label: 'GPT-5.3-Codex',
description: 'Latest frontier agentic coding model.',
badge: 'Premium',
provider: 'codex',
hasReasoning: true,
},
{ {
id: CODEX_MODEL_MAP.gpt52Codex, id: CODEX_MODEL_MAP.gpt52Codex,
label: 'GPT-5.2-Codex', label: 'GPT-5.2-Codex',
description: 'Most advanced agentic coding model for complex software engineering.', description: 'Frontier agentic coding model.',
badge: 'Premium', badge: 'Premium',
provider: 'codex', provider: 'codex',
hasReasoning: true, hasReasoning: true,
@@ -83,7 +91,7 @@ export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
{ {
id: CODEX_MODEL_MAP.gpt51CodexMax, id: CODEX_MODEL_MAP.gpt51CodexMax,
label: 'GPT-5.1-Codex-Max', label: 'GPT-5.1-Codex-Max',
description: 'Optimized for long-horizon, agentic coding tasks in Codex.', description: 'Codex-optimized flagship for deep and fast reasoning.',
badge: 'Premium', badge: 'Premium',
provider: 'codex', provider: 'codex',
hasReasoning: true, hasReasoning: true,
@@ -91,7 +99,7 @@ export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
{ {
id: CODEX_MODEL_MAP.gpt51CodexMini, id: CODEX_MODEL_MAP.gpt51CodexMini,
label: 'GPT-5.1-Codex-Mini', label: 'GPT-5.1-Codex-Mini',
description: 'Smaller, more cost-effective version for faster workflows.', description: 'Optimized for codex. Cheaper, faster, but less capable.',
badge: 'Speed', badge: 'Speed',
provider: 'codex', provider: 'codex',
hasReasoning: false, hasReasoning: false,
@@ -99,7 +107,7 @@ export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
{ {
id: CODEX_MODEL_MAP.gpt52, id: CODEX_MODEL_MAP.gpt52,
label: 'GPT-5.2', label: 'GPT-5.2',
description: 'Best general agentic model for tasks across industries and domains.', description: 'Latest frontier model with improvements across knowledge, reasoning and coding.',
badge: 'Balanced', badge: 'Balanced',
provider: 'codex', provider: 'codex',
hasReasoning: true, hasReasoning: true,
@@ -141,6 +149,7 @@ export const THINKING_LEVELS: ThinkingLevelOption[] = [
{ id: 'medium', label: 'Medium' }, { id: 'medium', label: 'Medium' },
{ id: 'high', label: 'High' }, { id: 'high', label: 'High' },
{ id: 'ultrathink', label: 'Ultrathink' }, { id: 'ultrathink', label: 'Ultrathink' },
{ id: 'adaptive', label: 'Adaptive' },
]; ];
/** /**
@@ -154,6 +163,7 @@ export const THINKING_LEVEL_LABELS: Record<ThinkingLevel, string> = {
medium: 'Med', medium: 'Med',
high: 'High', high: 'High',
ultrathink: 'Ultra', ultrathink: 'Ultra',
adaptive: 'Adaptive',
}; };
/** /**
@@ -211,6 +221,7 @@ export function getModelDisplayName(model: ModelAlias | string): string {
haiku: 'Claude Haiku', haiku: 'Claude Haiku',
sonnet: 'Claude Sonnet', sonnet: 'Claude Sonnet',
opus: 'Claude Opus', opus: 'Claude Opus',
[CODEX_MODEL_MAP.gpt53Codex]: 'GPT-5.3-Codex',
[CODEX_MODEL_MAP.gpt52Codex]: 'GPT-5.2-Codex', [CODEX_MODEL_MAP.gpt52Codex]: 'GPT-5.2-Codex',
[CODEX_MODEL_MAP.gpt51CodexMax]: 'GPT-5.1-Codex-Max', [CODEX_MODEL_MAP.gpt51CodexMax]: 'GPT-5.1-Codex-Max',
[CODEX_MODEL_MAP.gpt51CodexMini]: 'GPT-5.1-Codex-Mini', [CODEX_MODEL_MAP.gpt51CodexMini]: 'GPT-5.1-Codex-Mini',

View File

@@ -18,7 +18,7 @@ export type ClaudeCanonicalId = 'claude-haiku' | 'claude-sonnet' | 'claude-opus'
export const CLAUDE_CANONICAL_MAP: Record<ClaudeCanonicalId, string> = { export const CLAUDE_CANONICAL_MAP: Record<ClaudeCanonicalId, string> = {
'claude-haiku': 'claude-haiku-4-5-20251001', 'claude-haiku': 'claude-haiku-4-5-20251001',
'claude-sonnet': 'claude-sonnet-4-5-20250929', 'claude-sonnet': 'claude-sonnet-4-5-20250929',
'claude-opus': 'claude-opus-4-5-20251101', 'claude-opus': 'claude-opus-4-6',
} as const; } as const;
/** /**
@@ -29,7 +29,7 @@ export const CLAUDE_CANONICAL_MAP: Record<ClaudeCanonicalId, string> = {
export const CLAUDE_MODEL_MAP: Record<string, string> = { export const CLAUDE_MODEL_MAP: Record<string, string> = {
haiku: 'claude-haiku-4-5-20251001', haiku: 'claude-haiku-4-5-20251001',
sonnet: 'claude-sonnet-4-5-20250929', sonnet: 'claude-sonnet-4-5-20250929',
opus: 'claude-opus-4-5-20251101', opus: 'claude-opus-4-6',
} as const; } as const;
/** /**
@@ -50,15 +50,17 @@ export const LEGACY_CLAUDE_ALIAS_MAP: Record<string, ClaudeCanonicalId> = {
*/ */
export const CODEX_MODEL_MAP = { export const CODEX_MODEL_MAP = {
// Recommended Codex-specific models // Recommended Codex-specific models
/** Most advanced agentic coding model for complex software engineering (default for ChatGPT users) */ /** Latest frontier agentic coding model */
gpt53Codex: 'codex-gpt-5.3-codex',
/** Frontier agentic coding model */
gpt52Codex: 'codex-gpt-5.2-codex', gpt52Codex: 'codex-gpt-5.2-codex',
/** Optimized for long-horizon, agentic coding tasks in Codex */ /** Codex-optimized flagship for deep and fast reasoning */
gpt51CodexMax: 'codex-gpt-5.1-codex-max', gpt51CodexMax: 'codex-gpt-5.1-codex-max',
/** Smaller, more cost-effective version for faster workflows */ /** Optimized for codex. Cheaper, faster, but less capable */
gpt51CodexMini: 'codex-gpt-5.1-codex-mini', gpt51CodexMini: 'codex-gpt-5.1-codex-mini',
// General-purpose GPT models (also available in Codex) // General-purpose GPT models (also available in Codex)
/** Best general agentic model for tasks across industries and domains */ /** Latest frontier model with improvements across knowledge, reasoning and coding */
gpt52: 'codex-gpt-5.2', gpt52: 'codex-gpt-5.2',
/** Great for coding and agentic tasks across domains */ /** Great for coding and agentic tasks across domains */
gpt51: 'codex-gpt-5.1', gpt51: 'codex-gpt-5.1',
@@ -71,6 +73,7 @@ export const CODEX_MODEL_IDS = Object.values(CODEX_MODEL_MAP);
* These models can use reasoning.effort parameter * These models can use reasoning.effort parameter
*/ */
export const REASONING_CAPABLE_MODELS = new Set([ export const REASONING_CAPABLE_MODELS = new Set([
CODEX_MODEL_MAP.gpt53Codex,
CODEX_MODEL_MAP.gpt52Codex, CODEX_MODEL_MAP.gpt52Codex,
CODEX_MODEL_MAP.gpt51CodexMax, CODEX_MODEL_MAP.gpt51CodexMax,
CODEX_MODEL_MAP.gpt52, CODEX_MODEL_MAP.gpt52,
@@ -96,9 +99,9 @@ export function getAllCodexModelIds(): CodexModelId[] {
* Uses canonical prefixed IDs for consistent routing. * Uses canonical prefixed IDs for consistent routing.
*/ */
export const DEFAULT_MODELS = { export const DEFAULT_MODELS = {
claude: 'claude-opus-4-5-20251101', claude: 'claude-opus-4-6',
cursor: 'cursor-auto', // Cursor's recommended default (with prefix) cursor: 'cursor-auto', // Cursor's recommended default (with prefix)
codex: CODEX_MODEL_MAP.gpt52Codex, // GPT-5.2-Codex is the most advanced agentic coding model codex: CODEX_MODEL_MAP.gpt53Codex, // GPT-5.3-Codex is the latest frontier agentic coding model
} as const; } as const;
export type ModelAlias = keyof typeof CLAUDE_MODEL_MAP; export type ModelAlias = keyof typeof CLAUDE_MODEL_MAP;

View File

@@ -213,7 +213,7 @@ export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
export type ServerLogLevel = 'error' | 'warn' | 'info' | 'debug'; export type ServerLogLevel = 'error' | 'warn' | 'info' | 'debug';
/** ThinkingLevel - Extended thinking levels for Claude models (reasoning intensity) */ /** ThinkingLevel - Extended thinking levels for Claude models (reasoning intensity) */
export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink' | 'adaptive';
/** /**
* SidebarStyle - Sidebar layout style options * SidebarStyle - Sidebar layout style options
@@ -237,6 +237,7 @@ export const THINKING_TOKEN_BUDGET: Record<ThinkingLevel, number | undefined> =
medium: 10000, // Light reasoning medium: 10000, // Light reasoning
high: 16000, // Complex tasks (recommended starting point) high: 16000, // Complex tasks (recommended starting point)
ultrathink: 32000, // Maximum safe (above this risks timeouts) ultrathink: 32000, // Maximum safe (above this risks timeouts)
adaptive: undefined, // Adaptive thinking (Opus 4.6) - SDK handles token allocation
}; };
/** /**
@@ -247,6 +248,26 @@ export function getThinkingTokenBudget(level: ThinkingLevel | undefined): number
return THINKING_TOKEN_BUDGET[level]; return THINKING_TOKEN_BUDGET[level];
} }
/**
* Check if a model uses adaptive thinking (Opus 4.6+)
* Adaptive thinking models let the SDK decide token allocation automatically.
*/
export function isAdaptiveThinkingModel(model: string): boolean {
return model.includes('opus-4-6') || model === 'claude-opus';
}
/**
* Get the available thinking levels for a given model.
* - Opus 4.6: Only 'none' and 'adaptive' (SDK handles token allocation)
* - Others: Full range of manual thinking levels
*/
export function getThinkingLevelsForModel(model: string): ThinkingLevel[] {
if (isAdaptiveThinkingModel(model)) {
return ['none', 'adaptive'];
}
return ['none', 'low', 'medium', 'high', 'ultrathink'];
}
/** ModelProvider - AI model provider for credentials and API key management */ /** ModelProvider - AI model provider for credentials and API key management */
export type ModelProvider = 'claude' | 'cursor' | 'codex' | 'opencode' | 'gemini' | 'copilot'; export type ModelProvider = 'claude' | 'cursor' | 'codex' | 'opencode' | 'gemini' | 'copilot';

18
package-lock.json generated
View File

@@ -35,7 +35,7 @@
"version": "0.13.0", "version": "0.13.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.2.32",
"@automaker/dependency-resolver": "1.0.0", "@automaker/dependency-resolver": "1.0.0",
"@automaker/git-utils": "1.0.0", "@automaker/git-utils": "1.0.0",
"@automaker/model-resolver": "1.0.0", "@automaker/model-resolver": "1.0.0",
@@ -45,7 +45,7 @@
"@automaker/utils": "1.0.0", "@automaker/utils": "1.0.0",
"@github/copilot-sdk": "^0.1.16", "@github/copilot-sdk": "^0.1.16",
"@modelcontextprotocol/sdk": "1.25.2", "@modelcontextprotocol/sdk": "1.25.2",
"@openai/codex-sdk": "^0.77.0", "@openai/codex-sdk": "^0.98.0",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"cors": "2.8.5", "cors": "2.8.5",
"dotenv": "17.2.3", "dotenv": "17.2.3",
@@ -657,9 +657,9 @@
} }
}, },
"node_modules/@anthropic-ai/claude-agent-sdk": { "node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.1.76", "version": "0.2.32",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.76.tgz", "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.32.tgz",
"integrity": "sha512-s7RvpXoFaLXLG7A1cJBAPD8ilwOhhc/12fb5mJXRuD561o4FmPtQ+WRfuy9akMmrFRfLsKv8Ornw3ClGAPL2fw==", "integrity": "sha512-8AtsSx/M9jxd0ihS08eqa7VireTEuwQy0i1+6ZJX93LECT6Svlf47dPJiAm7JB+BhVMmwTfQeS6x1akIcCfvbQ==",
"license": "SEE LICENSE IN README.md", "license": "SEE LICENSE IN README.md",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
@@ -675,7 +675,7 @@
"@img/sharp-win32-x64": "^0.33.5" "@img/sharp-win32-x64": "^0.33.5"
}, },
"peerDependencies": { "peerDependencies": {
"zod": "^3.24.1 || ^4.0.0" "zod": "^4.0.0"
} }
}, },
"node_modules/@automaker/dependency-resolver": { "node_modules/@automaker/dependency-resolver": {
@@ -3949,9 +3949,9 @@
} }
}, },
"node_modules/@openai/codex-sdk": { "node_modules/@openai/codex-sdk": {
"version": "0.77.0", "version": "0.98.0",
"resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.77.0.tgz", "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.98.0.tgz",
"integrity": "sha512-bvJQ4dASnZ7jgfxmseViQwdRupHxs0TwHSZFeYB0gpdOAXnWwDWdGJRCMyphLSHwExRp27JNOk7EBFVmZRBanQ==", "integrity": "sha512-TbPgrBpuSNMJyOXys0HNsh6UoP5VIHu1fVh2KDdACi5XyB0vuPtzBZC+qOsxHz7WXEQPFlomPLyxS6JnE5Okmg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=18" "node": ">=18"