From 45f6f17eb0b0e1e6f28a9a77069397255c3b6e8f Mon Sep 17 00:00:00 2001
From: Kacper
Date: Mon, 2 Feb 2026 15:47:18 +0100
Subject: [PATCH 01/14] fix(docker): Pre-install Playwright Chromium browsers
for automated testing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes #725
AI agents in automated testing mode require Playwright to verify implementations,
but Docker containers had only system dependencies installed, not browser binaries.
This caused verification failures with permissions errors.
Changes:
- Install Playwright Chromium in Dockerfile (~300MB increase)
- Update docker-compose.override.yml.example with clearer Playwright documentation
- Add "Playwright for Automated Testing" section to README
- Document optional volume mount for persisting browsers across rebuilds
Browsers are now pre-installed and work out of the box for Docker users.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5
---
Dockerfile | 6 ++++++
README.md | 27 +++++++++++++++++++++++++++
docker-compose.override.yml.example | 11 ++++++++---
3 files changed, 41 insertions(+), 3 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 03911b45..7d22858c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -118,6 +118,12 @@ RUN curl -fsSL https://opencode.ai/install | bash && \
echo "=== Checking OpenCode CLI installation ===" && \
ls -la /home/automaker/.local/bin/ && \
(which opencode && opencode --version) || echo "opencode installed (may need auth setup)"
+
+# Install Playwright Chromium browser for AI agent verification tests
+# This adds ~300MB to the image but enables automated testing mode out of the box
+RUN npx playwright install chromium && \
+ echo "=== Playwright Chromium installed ===" && \
+ ls -la /home/automaker/.cache/ms-playwright/ || echo "Playwright browsers installed"
USER root
# Add PATH to profile so it's available in all interactive shells (for login shells)
diff --git a/README.md b/README.md
index 75705673..645ba722 100644
--- a/README.md
+++ b/README.md
@@ -338,6 +338,33 @@ 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.
+##### 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 across container rebuilds**
+
+To avoid re-downloading browsers when rebuilding the Docker image, 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 automaker-server npx playwright install chromium
+```
+
### Testing
#### End-to-End Tests (Playwright)
diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example
index 3815c197..d1f0c216 100644
--- a/docker-compose.override.yml.example
+++ b/docker-compose.override.yml.example
@@ -21,9 +21,13 @@ services:
# - ~/.local/share/opencode:/home/automaker/.local/share/opencode
# - ~/.config/opencode:/home/automaker/.config/opencode
- # Playwright browser cache - persists installed browsers across container restarts
- # Run 'npx playwright install --with-deps chromium' once, and it will persist
+ # ===== Playwright Browser Cache (Optional) =====
+ # 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
+ #
+ # To update Playwright browsers manually:
+ # docker exec automaker-server npx playwright install chromium
environment:
# Set root directory for all projects and file operations
# Users can only create/open projects within this directory
@@ -37,6 +41,7 @@ services:
# - CURSOR_API_KEY=${CURSOR_API_KEY:-}
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:
# name: automaker-playwright-cache
From b37a287c9c63b6955e454e8497a16746a2231712 Mon Sep 17 00:00:00 2001
From: Kacper
Date: Mon, 2 Feb 2026 15:55:11 +0100
Subject: [PATCH 02/14] fix(docker): Address PR #745 review feedback
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Clean up npx cache after Playwright installation to reduce image size
- Clarify README: volume mounts persist cache across container lifecycles,
not image rebuilds
- Add first-use warning: empty volume overrides pre-installed browsers,
users must re-install with docker exec command
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
Dockerfile | 4 +++-
README.md | 13 +++++++++++--
2 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 7d22858c..f5c3511e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -121,9 +121,11 @@ RUN curl -fsSL https://opencode.ai/install | bash && \
# Install Playwright Chromium browser for AI agent verification tests
# This adds ~300MB to the image but enables automated testing mode out of the box
+# Clean up npx cache after installation to reduce image size
RUN npx playwright install chromium && \
echo "=== Playwright Chromium installed ===" && \
- ls -la /home/automaker/.cache/ms-playwright/ || echo "Playwright browsers installed"
+ ls -la /home/automaker/.cache/ms-playwright/ || echo "Playwright browsers installed" && \
+ rm -rf /home/automaker/.npm/_npx
USER root
# Add PATH to profile so it's available in all interactive shells (for login shells)
diff --git a/README.md b/README.md
index 645ba722..0c21245b 100644
--- a/README.md
+++ b/README.md
@@ -344,9 +344,18 @@ The Docker image includes **Playwright Chromium pre-installed** for AI agent ver
**No additional setup required** - Playwright verification works out of the box.
-**Optional: Persist browsers across container rebuilds**
+**Optional: Persist browsers for manual updates**
-To avoid re-downloading browsers when rebuilding the Docker image, add this to your `docker-compose.override.yml`:
+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 automaker-server npx playwright install chromium
+```
+
+Add this to your `docker-compose.override.yml`:
```yaml
services:
From 3ccea7a67beee54f06060ecc4cfb8ebb0c307673 Mon Sep 17 00:00:00 2001
From: Kacper
Date: Mon, 2 Feb 2026 16:07:53 +0100
Subject: [PATCH 03/14] fix(docker): Address remaining PR #745 review comments
- Move Playwright install after node_modules copy to use pinned version
- Use local playwright binary instead of npx to avoid registry fetch
- Add --user automaker -w /app flags to docker exec commands
- Change bold text to proper heading in README (MD036 lint fix)
Co-Authored-By: Claude Opus 4.5
---
Dockerfile | 16 +++++++++-------
README.md | 6 +++---
docker-compose.override.yml.example | 2 +-
3 files changed, 13 insertions(+), 11 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index f5c3511e..2e745e4c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -119,13 +119,6 @@ RUN curl -fsSL https://opencode.ai/install | bash && \
ls -la /home/automaker/.local/bin/ && \
(which opencode && opencode --version) || echo "opencode installed (may need auth setup)"
-# Install Playwright Chromium browser for AI agent verification tests
-# This adds ~300MB to the image but enables automated testing mode out of the box
-# Clean up npx cache after installation to reduce image size
-RUN npx playwright install chromium && \
- echo "=== Playwright Chromium installed ===" && \
- ls -la /home/automaker/.cache/ms-playwright/ || echo "Playwright browsers installed" && \
- rm -rf /home/automaker/.npm/_npx
USER root
# Add PATH to profile so it's available in all interactive shells (for login shells)
@@ -155,6 +148,15 @@ COPY --from=server-builder /app/apps/server/package*.json ./apps/server/
# Copy node_modules (includes symlinks to libs)
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/ || echo "Playwright browsers installed"
+USER root
+
# Create data and projects directories
RUN mkdir -p /data /projects && chown automaker:automaker /data /projects
diff --git a/README.md b/README.md
index 0c21245b..95beefe1 100644
--- a/README.md
+++ b/README.md
@@ -344,7 +344,7 @@ The Docker image includes **Playwright Chromium pre-installed** for AI agent ver
**No additional setup required** - Playwright verification works out of the box.
-**Optional: Persist browsers for manual updates**
+#### 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.
@@ -352,7 +352,7 @@ By default, Playwright Chromium is pre-installed in the Docker image. If you nee
```bash
# After adding the volume mount for the first time
-docker exec automaker-server npx playwright install chromium
+docker exec --user automaker -w /app automaker-server npx playwright install chromium
```
Add this to your `docker-compose.override.yml`:
@@ -371,7 +371,7 @@ volumes:
**Updating browsers manually:**
```bash
-docker exec automaker-server npx playwright install chromium
+docker exec --user automaker -w /app automaker-server npx playwright install chromium
```
### Testing
diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example
index d1f0c216..e92ce119 100644
--- a/docker-compose.override.yml.example
+++ b/docker-compose.override.yml.example
@@ -27,7 +27,7 @@ services:
# - playwright-cache:/home/automaker/.cache/ms-playwright
#
# To update Playwright browsers manually:
- # docker exec automaker-server npx playwright install chromium
+ # docker exec --user automaker -w /app automaker-server npx playwright install chromium
environment:
# Set root directory for all projects and file operations
# Users can only create/open projects within this directory
From aad3ff2cdf74b7f872dc57eb8aa8117dae3b6952 Mon Sep 17 00:00:00 2001
From: Kacper
Date: Mon, 2 Feb 2026 17:35:03 +0100
Subject: [PATCH 04/14] fix(auth): Improve OAuth credential detection and
startup warning
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Enhanced getClaudeAuthIndicators() to return detailed check information
including file paths checked and specific error details for debugging
- Added debug logging to server startup credential detection for easier
troubleshooting in Docker environments
- Show paths that were checked in the warning message to help users debug
mount issues
- Added support for CLAUDE_CODE_OAUTH_TOKEN environment variable
- Return authType in verify-claude-auth response to distinguish between
OAuth and CLI authentication methods
- Updated UI to show specific success messages for Claude Code subscription
vs generic CLI auth
- Added Docker troubleshooting tips to sandbox risk dialog
- Added comprehensive unit tests for OAuth credential detection scenarios
Closes #721
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
apps/server/src/index.ts | 101 ++-
.../routes/setup/routes/verify-claude-auth.ts | 19 +
.../dialogs/sandbox-risk-dialog.tsx | 23 +
.../setup-view/steps/claude-setup-step.tsx | 25 +-
apps/ui/src/lib/electron.ts | 1 +
apps/ui/src/lib/http-api-client.ts | 1 +
libs/platform/src/index.ts | 2 +
libs/platform/src/system-paths.ts | 156 +++-
.../tests/oauth-credential-detection.test.ts | 736 ++++++++++++++++++
9 files changed, 1028 insertions(+), 36 deletions(-)
create mode 100644 libs/platform/tests/oauth-credential-detection.test.ts
diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts
index 4bd496bc..c10702bb 100644
--- a/apps/server/src/index.ts
+++ b/apps/server/src/index.ts
@@ -121,21 +121,89 @@ const BOX_CONTENT_WIDTH = 67;
// The Claude Agent SDK can use either ANTHROPIC_API_KEY or Claude Code CLI authentication
(async () => {
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) {
logger.info('✓ ANTHROPIC_API_KEY detected');
return;
}
+ if (hasEnvOAuthToken) {
+ logger.info('✓ CLAUDE_CODE_OAUTH_TOKEN detected');
+ return;
+ }
+
// Check for Claude Code CLI authentication
+ // Store indicators outside the try block so we can use them in the warning message
+ let cliAuthIndicators: Awaited> | null = null;
+
try {
- const indicators = await getClaudeAuthIndicators();
+ cliAuthIndicators = await getClaudeAuthIndicators();
+ const indicators = cliAuthIndicators;
+
+ // Log detailed credential detection results
+ logger.debug('[CREDENTIAL_CHECK] Claude CLI auth indicators:', {
+ hasCredentialsFile: indicators.hasCredentialsFile,
+ hasSettingsFile: indicators.hasSettingsFile,
+ hasStatsCacheWithActivity: indicators.hasStatsCacheWithActivity,
+ hasProjectsSessions: indicators.hasProjectsSessions,
+ credentials: indicators.credentials,
+ });
+
+ logger.debug('[CREDENTIAL_CHECK] File check details:', {
+ settingsFile: {
+ path: indicators.checks.settingsFile.path,
+ exists: indicators.checks.settingsFile.exists,
+ readable: indicators.checks.settingsFile.readable,
+ error: indicators.checks.settingsFile.error,
+ },
+ statsCache: {
+ path: indicators.checks.statsCache.path,
+ exists: indicators.checks.statsCache.exists,
+ readable: indicators.checks.statsCache.readable,
+ hasDailyActivity: indicators.checks.statsCache.hasDailyActivity,
+ error: indicators.checks.statsCache.error,
+ },
+ projectsDir: {
+ path: indicators.checks.projectsDir.path,
+ exists: indicators.checks.projectsDir.exists,
+ readable: indicators.checks.projectsDir.readable,
+ entryCount: indicators.checks.projectsDir.entryCount,
+ error: indicators.checks.projectsDir.error,
+ },
+ credentialFiles: indicators.checks.credentialFiles.map((cf) => ({
+ path: cf.path,
+ exists: cf.exists,
+ readable: cf.readable,
+ error: cf.error,
+ })),
+ });
+
const hasCliAuth =
indicators.hasStatsCacheWithActivity ||
(indicators.hasSettingsFile && indicators.hasProjectsSessions) ||
(indicators.hasCredentialsFile &&
(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) {
logger.info('✓ Claude Code CLI authentication detected');
return;
@@ -145,7 +213,7 @@ const BOX_CONTENT_WIDTH = 67;
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 w1 = 'The Claude Agent SDK requires authentication to function.'.padEnd(BOX_CONTENT_WIDTH);
const w2 = 'Options:'.padEnd(BOX_CONTENT_WIDTH);
@@ -158,6 +226,33 @@ const BOX_CONTENT_WIDTH = 67;
BOX_CONTENT_WIDTH
);
+ // Build paths checked summary from the indicators (if available)
+ let pathsCheckedInfo = '';
+ if (cliAuthIndicators) {
+ const pathsChecked: string[] = [];
+
+ // Collect paths that were checked
+ if (cliAuthIndicators.checks.settingsFile.path) {
+ pathsChecked.push(`Settings: ${cliAuthIndicators.checks.settingsFile.path}`);
+ }
+ if (cliAuthIndicators.checks.statsCache.path) {
+ pathsChecked.push(`Stats cache: ${cliAuthIndicators.checks.statsCache.path}`);
+ }
+ if (cliAuthIndicators.checks.projectsDir.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) => `║ ${p.substring(0, BOX_CONTENT_WIDTH - 2).padEnd(BOX_CONTENT_WIDTH - 2)} ║`).join('\n')}`;
+ }
+ }
+
logger.warn(`
╔═════════════════════════════════════════════════════════════════════╗
║ ${wHeader}║
@@ -169,7 +264,7 @@ const BOX_CONTENT_WIDTH = 67;
║ ${w3}║
║ ${w4}║
║ ${w5}║
-║ ${w6}║
+║ ${w6}║${pathsCheckedInfo}
║ ║
╚═════════════════════════════════════════════════════════════════════╝
`);
diff --git a/apps/server/src/routes/setup/routes/verify-claude-auth.ts b/apps/server/src/routes/setup/routes/verify-claude-auth.ts
index df04d462..2a8d21b0 100644
--- a/apps/server/src/routes/setup/routes/verify-claude-auth.ts
+++ b/apps/server/src/routes/setup/routes/verify-claude-auth.ts
@@ -320,9 +320,28 @@ export function createVerifyClaudeAuthHandler() {
authMethod,
});
+ // Determine specific auth type for success messages
+ let authType: 'oauth' | 'api_key' | 'cli' | undefined;
+ if (authenticated) {
+ if (authMethod === 'api_key') {
+ authType = 'api_key';
+ } else if (authMethod === 'cli') {
+ // Check if CLI auth is via OAuth (Claude Code subscription) or generic CLI
+ // OAuth tokens are stored in the credentials file by the Claude CLI
+ const { getClaudeAuthIndicators } = await import('@automaker/platform');
+ const indicators = await getClaudeAuthIndicators();
+ if (indicators.credentials?.hasOAuthToken) {
+ authType = 'oauth';
+ } else {
+ authType = 'cli';
+ }
+ }
+ }
+
res.json({
success: true,
authenticated,
+ authType,
error: errorMessage || undefined,
});
} catch (error) {
diff --git a/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx b/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx
index 3a5f6d35..7b597c8c 100644
--- a/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx
+++ b/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx
@@ -69,6 +69,29 @@ export function SandboxRiskDialog({ open, onConfirm, onDeny }: SandboxRiskDialog
For safer operation, consider running Automaker in Docker. See the README for
instructions.
+
+
+
+ Already running in Docker? Try these troubleshooting steps:
+
+
+
+ Ensure IS_CONTAINERIZED=true is
+ set in your docker-compose environment
+
+
+ Verify the server container has the environment variable:{' '}
+
+ docker exec automaker-server printenv IS_CONTAINERIZED
+
+
+ Rebuild and restart containers if you recently changed the configuration
+
+ Check the server logs for startup messages:{' '}
+ docker-compose logs server
+
+
+
diff --git a/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx
index b864bfdb..0b4799d6 100644
--- a/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx
+++ b/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx
@@ -59,6 +59,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
// CLI Verification state
const [cliVerificationStatus, setCliVerificationStatus] = useState('idle');
const [cliVerificationError, setCliVerificationError] = useState(null);
+ const [cliAuthType, setCliAuthType] = useState<'oauth' | 'cli' | null>(null);
// API Key Verification state
const [apiKeyVerificationStatus, setApiKeyVerificationStatus] =
@@ -119,6 +120,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
const verifyCliAuth = useCallback(async () => {
setCliVerificationStatus('verifying');
setCliVerificationError(null);
+ setCliAuthType(null);
try {
const api = getElectronAPI();
@@ -138,12 +140,21 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
if (result.authenticated && !hasLimitReachedError) {
setCliVerificationStatus('verified');
+ // Store the auth type for displaying specific success message
+ const authType = result.authType === 'oauth' ? 'oauth' : 'cli';
+ setCliAuthType(authType);
setClaudeAuthStatus({
authenticated: true,
- method: 'cli_authenticated',
+ method: authType === 'oauth' ? 'oauth_token' : 'cli_authenticated',
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 {
setCliVerificationStatus('error');
setCliVerificationError(
@@ -436,9 +447,15 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
-
CLI Authentication verified!
+
+ {cliAuthType === 'oauth'
+ ? 'Claude Code subscription verified!'
+ : 'CLI Authentication verified!'}
+
- Your Claude CLI is working correctly.
+ {cliAuthType === 'oauth'
+ ? 'Your Claude Code subscription is active and ready to use.'
+ : 'Your Claude CLI is working correctly.'}
diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts
index 89aa07ba..22079822 100644
--- a/apps/ui/src/lib/electron.ts
+++ b/apps/ui/src/lib/electron.ts
@@ -1442,6 +1442,7 @@ interface SetupAPI {
verifyClaudeAuth: (authMethod?: 'cli' | 'api_key') => Promise<{
success: boolean;
authenticated: boolean;
+ authType?: 'oauth' | 'api_key' | 'cli';
error?: string;
}>;
getGhStatus?: () => Promise<{
diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts
index 1f79ff07..acd75d22 100644
--- a/apps/ui/src/lib/http-api-client.ts
+++ b/apps/ui/src/lib/http-api-client.ts
@@ -1350,6 +1350,7 @@ export class HttpApiClient implements ElectronAPI {
): Promise<{
success: boolean;
authenticated: boolean;
+ authType?: 'oauth' | 'api_key' | 'cli';
error?: string;
}> => this.post('/api/setup/verify-claude-auth', { authMethod, apiKey }),
diff --git a/libs/platform/src/index.ts b/libs/platform/src/index.ts
index 5952ba2d..5c0b8078 100644
--- a/libs/platform/src/index.ts
+++ b/libs/platform/src/index.ts
@@ -134,6 +134,8 @@ export {
findClaudeCliPath,
getClaudeAuthIndicators,
type ClaudeAuthIndicators,
+ type FileCheckResult,
+ type DirectoryCheckResult,
findCodexCliPath,
getCodexAuthIndicators,
type CodexAuthIndicators,
diff --git a/libs/platform/src/system-paths.ts b/libs/platform/src/system-paths.ts
index 0d900dfa..fb5e6bd3 100644
--- a/libs/platform/src/system-paths.ts
+++ b/libs/platform/src/system-paths.ts
@@ -976,6 +976,27 @@ export async function findGitBashPath(): Promise {
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
*/
@@ -988,67 +1009,144 @@ export interface ClaudeAuthIndicators {
hasOAuthToken: boolean;
hasApiKey: boolean;
} | null;
+ /** Detailed information about what was checked */
+ checks: {
+ settingsFile: FileCheckResult;
+ statsCache: FileCheckResult & { hasDailyActivity?: boolean };
+ projectsDir: DirectoryCheckResult;
+ credentialFiles: FileCheckResult[];
+ };
}
export async function getClaudeAuthIndicators(): Promise {
+ 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 = {
hasCredentialsFile: false,
hasSettingsFile: false,
hasStatsCacheWithActivity: false,
hasProjectsSessions: false,
credentials: null,
+ checks: {
+ settingsFile: settingsFileCheck,
+ statsCache: statsCacheCheck,
+ projectsDir: projectsDirCheck,
+ credentialFiles: credentialFileChecks,
+ },
};
// Check settings file
try {
- if (await systemPathAccess(getClaudeSettingsPath())) {
+ if (await systemPathAccess(settingsPath)) {
+ settingsFileCheck.exists = true;
+ settingsFileCheck.readable = true;
result.hasSettingsFile = true;
}
- } catch {
- // Ignore errors
+ } catch (err) {
+ settingsFileCheck.error = err instanceof Error ? err.message : String(err);
}
// Check stats cache for recent activity
try {
- const statsContent = await systemPathReadFile(getClaudeStatsCachePath());
- const stats = JSON.parse(statsContent);
- if (stats.dailyActivity && stats.dailyActivity.length > 0) {
- result.hasStatsCacheWithActivity = true;
+ const statsContent = await systemPathReadFile(statsCachePath);
+ statsCacheCheck.exists = true;
+ statsCacheCheck.readable = 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
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) {
result.hasProjectsSessions = true;
}
- } catch {
- // Ignore errors
+ } catch (err) {
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
+ projectsDirCheck.exists = false;
+ } else {
+ projectsDirCheck.error = err instanceof Error ? err.message : String(err);
+ }
}
// Check credentials files
- const credentialPaths = getClaudeCredentialPaths();
- for (const credPath of credentialPaths) {
+ for (let i = 0; i < credentialPaths.length; i++) {
+ const credPath = credentialPaths[i];
+ const credCheck = credentialFileChecks[i];
try {
const content = await systemPathReadFile(credPath);
- const credentials = JSON.parse(content);
- result.hasCredentialsFile = true;
- // Support multiple credential formats:
- // 1. Claude Code CLI format: { claudeAiOauth: { accessToken, refreshToken } }
- // 2. Legacy format: { oauth_token } or { access_token }
- // 3. API key format: { api_key }
- const hasClaudeOauth = !!credentials.claudeAiOauth?.accessToken;
- const hasLegacyOauth = !!(credentials.oauth_token || credentials.access_token);
- result.credentials = {
- hasOAuthToken: hasClaudeOauth || hasLegacyOauth,
- hasApiKey: !!credentials.api_key,
- };
- break;
- } catch {
- // Continue to next path
+ credCheck.exists = true;
+ credCheck.readable = true;
+ try {
+ const credentials = JSON.parse(content);
+ result.hasCredentialsFile = true;
+ // Support multiple credential formats:
+ // 1. Claude Code CLI format: { claudeAiOauth: { accessToken, refreshToken } }
+ // 2. Legacy format: { oauth_token } or { access_token }
+ // 3. API key format: { api_key }
+ const hasClaudeOauth = !!credentials.claudeAiOauth?.accessToken;
+ const hasLegacyOauth = !!(credentials.oauth_token || credentials.access_token);
+ result.credentials = {
+ hasOAuthToken: hasClaudeOauth || hasLegacyOauth,
+ hasApiKey: !!credentials.api_key,
+ };
+ break;
+ } 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);
+ }
}
}
diff --git a/libs/platform/tests/oauth-credential-detection.test.ts b/libs/platform/tests/oauth-credential-detection.test.ts
new file mode 100644
index 00000000..cf5a4705
--- /dev/null
+++ b/libs/platform/tests/oauth-credential-detection.test.ts
@@ -0,0 +1,736 @@
+/**
+ * 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();
+
+ expect(indicators.hasCredentialsFile).toBe(true);
+ expect(indicators.credentials).not.toBeNull();
+ expect(indicators.credentials?.hasOAuthToken).toBe(false);
+ expect(indicators.credentials?.hasApiKey).toBe(false);
+ });
+
+ 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();
+
+ expect(indicators.hasCredentialsFile).toBe(true);
+ expect(indicators.credentials?.hasOAuthToken).toBe(false);
+ expect(indicators.credentials?.hasApiKey).toBe(false);
+ });
+
+ 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();
+
+ expect(indicators.hasCredentialsFile).toBe(true);
+ // Empty strings should not be treated as valid credentials
+ expect(indicators.credentials?.hasOAuthToken).toBe(false);
+ expect(indicators.credentials?.hasApiKey).toBe(false);
+ });
+
+ 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);
+ });
+ });
+
+ 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();
+
+ expect(indicators.hasCredentialsFile).toBe(true);
+ expect(indicators.credentials?.hasOAuthToken).toBe(false);
+ expect(indicators.credentials?.hasApiKey).toBe(false);
+ });
+
+ 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 - should handle gracefully
+ expect(indicators.hasCredentialsFile).toBe(true);
+ expect(indicators.credentials?.hasOAuthToken).toBe(false);
+ expect(indicators.credentials?.hasApiKey).toBe(false);
+ });
+
+ 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);
+ });
+ });
+});
From 0aef72540e5f9eff7919f5b2157ce7c5f1141204 Mon Sep 17 00:00:00 2001
From: Kacper
Date: Mon, 2 Feb 2026 17:54:23 +0100
Subject: [PATCH 05/14] fix(auth): Enhance credential detection logic for OAuth
- Updated getClaudeAuthIndicators() to ensure that empty or token-less credential files do not prevent the detection of valid credentials in subsequent paths.
- Improved error handling for settings file readability checks, providing clearer feedback on file access issues.
- Added unit tests to validate the new behavior, ensuring that the system continues to check all credential paths even when some files are empty or invalid.
This change improves the robustness of the credential detection process and enhances user experience by allowing for more flexible credential management.
---
libs/platform/src/system-paths.ts | 37 ++++++++---
.../tests/oauth-credential-detection.test.ts | 61 +++++++++++++------
2 files changed, 72 insertions(+), 26 deletions(-)
diff --git a/libs/platform/src/system-paths.ts b/libs/platform/src/system-paths.ts
index fb5e6bd3..f1749464 100644
--- a/libs/platform/src/system-paths.ts
+++ b/libs/platform/src/system-paths.ts
@@ -1065,11 +1065,20 @@ export async function getClaudeAuthIndicators(): Promise {
};
// Check settings file
+ // First check existence, then try to read to confirm it's actually readable
try {
if (await systemPathAccess(settingsPath)) {
settingsFileCheck.exists = true;
- settingsFileCheck.readable = true;
- result.hasSettingsFile = 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 (err) {
settingsFileCheck.error = err instanceof Error ? err.message : String(err);
@@ -1117,6 +1126,9 @@ export async function getClaudeAuthIndicators(): Promise {
}
// Check credentials files
+ // We iterate through all credential paths and only stop when we find a file
+ // 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];
@@ -1126,18 +1138,27 @@ export async function getClaudeAuthIndicators(): Promise {
credCheck.readable = true;
try {
const credentials = JSON.parse(content);
- result.hasCredentialsFile = true;
// Support multiple credential formats:
// 1. Claude Code CLI format: { claudeAiOauth: { accessToken, refreshToken } }
// 2. Legacy format: { oauth_token } or { access_token }
// 3. API key format: { api_key }
const hasClaudeOauth = !!credentials.claudeAiOauth?.accessToken;
const hasLegacyOauth = !!(credentials.oauth_token || credentials.access_token);
- result.credentials = {
- hasOAuthToken: hasClaudeOauth || hasLegacyOauth,
- hasApiKey: !!credentials.api_key,
- };
- break;
+ const hasOAuthToken = hasClaudeOauth || hasLegacyOauth;
+ const hasApiKey = !!credentials.api_key;
+
+ // Only consider this a valid credentials file if it actually contains tokens
+ // 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)}`;
}
diff --git a/libs/platform/tests/oauth-credential-detection.test.ts b/libs/platform/tests/oauth-credential-detection.test.ts
index cf5a4705..6e445b22 100644
--- a/libs/platform/tests/oauth-credential-detection.test.ts
+++ b/libs/platform/tests/oauth-credential-detection.test.ts
@@ -173,10 +173,14 @@ describe('OAuth Credential Detection', () => {
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(false);
- expect(indicators.credentials?.hasApiKey).toBe(false);
+ // 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 () => {
@@ -191,9 +195,10 @@ describe('OAuth Credential Detection', () => {
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(false);
+ // 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 () => {
@@ -210,10 +215,10 @@ describe('OAuth Credential Detection', () => {
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
- expect(indicators.hasCredentialsFile).toBe(true);
- // Empty strings should not be treated as valid credentials
- expect(indicators.credentials?.hasOAuthToken).toBe(false);
- expect(indicators.credentials?.hasApiKey).toBe(false);
+ // 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 () => {
@@ -337,6 +342,27 @@ describe('OAuth Credential Detection', () => {
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', () => {
@@ -585,9 +611,9 @@ describe('OAuth Credential Detection', () => {
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(false);
+ // 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 () => {
@@ -598,10 +624,9 @@ describe('OAuth Credential Detection', () => {
const { getClaudeAuthIndicators } = await import('../src/system-paths');
const indicators = await getClaudeAuthIndicators();
- // Array is valid JSON but wrong structure - should handle gracefully
- expect(indicators.hasCredentialsFile).toBe(true);
- expect(indicators.credentials?.hasOAuthToken).toBe(false);
- expect(indicators.credentials?.hasApiKey).toBe(false);
+ // 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 () => {
From 3b361cb0b918b9e37b43c5136af68b7d91921a60 Mon Sep 17 00:00:00 2001
From: Kacper
Date: Thu, 5 Feb 2026 22:17:55 +0100
Subject: [PATCH 06/14] chore: update Codex SDK to version 0.98.0 and add
GPT-5.3-Codex model
- Upgraded @openai/codex-sdk from version 0.77.0 to 0.98.0 in package-lock.json and package.json.
- Introduced new model 'GPT-5.3-Codex' with enhanced capabilities in codex-models.ts and related files.
- Updated descriptions for existing models to reflect their latest features and improvements.
- Adjusted Codex model configuration and display to include the new model and its attributes.
These changes enhance the Codex model offerings and ensure compatibility with the latest SDK version.
---
apps/server/package.json | 2 +-
apps/server/src/providers/codex-models.ts | 28 +++++++++++++------
.../providers/codex-model-configuration.tsx | 14 +++++++---
libs/platform/src/system-paths.ts | 24 ++++++++++++++--
libs/types/src/codex-models.ts | 16 ++++++++---
libs/types/src/model-display.ts | 17 ++++++++---
libs/types/src/model.ts | 11 +++++---
package-lock.json | 8 +++---
8 files changed, 88 insertions(+), 32 deletions(-)
diff --git a/apps/server/package.json b/apps/server/package.json
index c9015aea..0b4deeac 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -34,7 +34,7 @@
"@automaker/utils": "1.0.0",
"@github/copilot-sdk": "^0.1.16",
"@modelcontextprotocol/sdk": "1.25.2",
- "@openai/codex-sdk": "^0.77.0",
+ "@openai/codex-sdk": "^0.98.0",
"cookie-parser": "1.4.7",
"cors": "2.8.5",
"dotenv": "17.2.3",
diff --git a/apps/server/src/providers/codex-models.ts b/apps/server/src/providers/codex-models.ts
index 141d5355..7840888b 100644
--- a/apps/server/src/providers/codex-models.ts
+++ b/apps/server/src/providers/codex-models.ts
@@ -19,12 +19,11 @@ const MAX_OUTPUT_16K = 16000;
export const CODEX_MODELS: ModelDefinition[] = [
// ========== Recommended Codex Models ==========
{
- id: CODEX_MODEL_MAP.gpt52Codex,
- name: 'GPT-5.2-Codex',
- modelString: CODEX_MODEL_MAP.gpt52Codex,
+ id: CODEX_MODEL_MAP.gpt53Codex,
+ name: 'GPT-5.3-Codex',
+ modelString: CODEX_MODEL_MAP.gpt53Codex,
provider: 'openai',
- description:
- 'Most advanced agentic coding model for complex software engineering (default for ChatGPT users).',
+ description: 'Latest frontier agentic coding model.',
contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
@@ -33,12 +32,25 @@ export const CODEX_MODELS: ModelDefinition[] = [
default: 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,
name: 'GPT-5.1-Codex-Max',
modelString: CODEX_MODEL_MAP.gpt51CodexMax,
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,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
@@ -51,7 +63,7 @@ export const CODEX_MODELS: ModelDefinition[] = [
name: 'GPT-5.1-Codex-Mini',
modelString: CODEX_MODEL_MAP.gpt51CodexMini,
provider: 'openai',
- description: 'Smaller, more cost-effective version for faster workflows.',
+ description: 'Optimized for codex. Cheaper, faster, but less capable.',
contextWindow: CONTEXT_WINDOW_128K,
maxOutputTokens: MAX_OUTPUT_16K,
supportsVision: true,
@@ -66,7 +78,7 @@ export const CODEX_MODELS: ModelDefinition[] = [
name: 'GPT-5.2',
modelString: CODEX_MODEL_MAP.gpt52,
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,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
diff --git a/apps/ui/src/components/views/settings-view/providers/codex-model-configuration.tsx b/apps/ui/src/components/views/settings-view/providers/codex-model-configuration.tsx
index a9d8c06e..de1d9555 100644
--- a/apps/ui/src/components/views/settings-view/providers/codex-model-configuration.tsx
+++ b/apps/ui/src/components/views/settings-view/providers/codex-model-configuration.tsx
@@ -27,25 +27,30 @@ interface CodexModelInfo {
}
const CODEX_MODEL_INFO: Record = {
+ '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': {
id: 'codex-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': {
id: 'codex-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': {
id: 'codex-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': {
id: 'codex-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': {
id: 'codex-gpt-5.1',
@@ -160,6 +165,7 @@ export function CodexModelConfiguration({
function supportsReasoningEffort(modelId: string): boolean {
const reasoningModels = [
+ 'codex-gpt-5.3-codex',
'codex-gpt-5.2-codex',
'codex-gpt-5.1-codex-max',
'codex-gpt-5.2',
diff --git a/libs/platform/src/system-paths.ts b/libs/platform/src/system-paths.ts
index 0d900dfa..ce1246eb 100644
--- a/libs/platform/src/system-paths.ts
+++ b/libs/platform/src/system-paths.ts
@@ -54,13 +54,19 @@ export function getClaudeCliPaths(): string[] {
if (isWindows) {
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
- return [
+ const nvmSymlink = process.env.NVM_SYMLINK;
+ const paths = [
path.join(os.homedir(), '.local', 'bin', 'claude.exe'),
path.join(appData, 'npm', 'claude.cmd'),
path.join(appData, 'npm', 'claude'),
path.join(appData, '.npm-global', 'bin', 'claude.cmd'),
path.join(appData, '.npm-global', 'bin', 'claude'),
];
+ // nvm4w (NVM for Windows) symlink path
+ if (nvmSymlink) {
+ paths.push(path.join(nvmSymlink, 'claude.cmd'), path.join(nvmSymlink, 'claude'));
+ }
+ return paths;
}
return [
@@ -130,7 +136,8 @@ export function getCodexCliPaths(): string[] {
if (isWindows) {
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local');
- return [
+ const nvmSymlink = process.env.NVM_SYMLINK;
+ const paths = [
path.join(homeDir, '.local', 'bin', 'codex.exe'),
path.join(appData, 'npm', 'codex.cmd'),
path.join(appData, 'npm', 'codex'),
@@ -142,6 +149,11 @@ export function getCodexCliPaths(): string[] {
path.join(localAppData, 'pnpm', 'codex.cmd'),
path.join(localAppData, 'pnpm', 'codex'),
];
+ // nvm4w (NVM for Windows) symlink path
+ if (nvmSymlink) {
+ paths.push(path.join(nvmSymlink, 'codex.cmd'), path.join(nvmSymlink, 'codex'));
+ }
+ return paths;
}
// Include NVM bin paths for codex installed via npm global under NVM
@@ -1126,7 +1138,8 @@ export function getOpenCodeCliPaths(): string[] {
if (isWindows) {
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local');
- return [
+ const nvmSymlink = process.env.NVM_SYMLINK;
+ const paths = [
// OpenCode's default installation directory
path.join(homeDir, '.opencode', 'bin', 'opencode.exe'),
path.join(homeDir, '.local', 'bin', 'opencode.exe'),
@@ -1143,6 +1156,11 @@ export function getOpenCodeCliPaths(): string[] {
path.join(homeDir, 'go', 'bin', 'opencode.exe'),
path.join(process.env.GOPATH || path.join(homeDir, 'go'), 'bin', 'opencode.exe'),
];
+ // nvm4w (NVM for Windows) symlink path
+ if (nvmSymlink) {
+ paths.push(path.join(nvmSymlink, 'opencode.cmd'), path.join(nvmSymlink, 'opencode'));
+ }
+ return paths;
}
// Include NVM bin paths for opencode installed via npm global under NVM
diff --git a/libs/types/src/codex-models.ts b/libs/types/src/codex-models.ts
index cf4db0ea..934218ea 100644
--- a/libs/types/src/codex-models.ts
+++ b/libs/types/src/codex-models.ts
@@ -6,6 +6,7 @@
* IMPORTANT: All Codex models use 'codex-' prefix to distinguish from Cursor CLI models
*/
export type CodexModelId =
+ | 'codex-gpt-5.3-codex'
| 'codex-gpt-5.2-codex'
| 'codex-gpt-5.1-codex-max'
| 'codex-gpt-5.1-codex-mini'
@@ -29,31 +30,38 @@ export interface CodexModelConfig {
* All keys use 'codex-' prefix to distinguish from Cursor CLI models
*/
export const CODEX_MODEL_CONFIG_MAP: Record = {
+ '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': {
id: 'codex-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,
supportsVision: true,
},
'codex-gpt-5.1-codex-max': {
id: 'codex-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,
supportsVision: true,
},
'codex-gpt-5.1-codex-mini': {
id: 'codex-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,
supportsVision: true,
},
'codex-gpt-5.2': {
id: 'codex-gpt-5.2',
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,
supportsVision: true,
},
diff --git a/libs/types/src/model-display.ts b/libs/types/src/model-display.ts
index 28670328..08eaf208 100644
--- a/libs/types/src/model-display.ts
+++ b/libs/types/src/model-display.ts
@@ -72,10 +72,18 @@ export const CLAUDE_MODELS: ModelOption[] = [
* Official models from https://developers.openai.com/codex/models/
*/
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,
label: 'GPT-5.2-Codex',
- description: 'Most advanced agentic coding model for complex software engineering.',
+ description: 'Frontier agentic coding model.',
badge: 'Premium',
provider: 'codex',
hasReasoning: true,
@@ -83,7 +91,7 @@ export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
{
id: CODEX_MODEL_MAP.gpt51CodexMax,
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',
provider: 'codex',
hasReasoning: true,
@@ -91,7 +99,7 @@ export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
{
id: CODEX_MODEL_MAP.gpt51CodexMini,
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',
provider: 'codex',
hasReasoning: false,
@@ -99,7 +107,7 @@ export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
{
id: CODEX_MODEL_MAP.gpt52,
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',
provider: 'codex',
hasReasoning: true,
@@ -211,6 +219,7 @@ export function getModelDisplayName(model: ModelAlias | string): string {
haiku: 'Claude Haiku',
sonnet: 'Claude Sonnet',
opus: 'Claude Opus',
+ [CODEX_MODEL_MAP.gpt53Codex]: 'GPT-5.3-Codex',
[CODEX_MODEL_MAP.gpt52Codex]: 'GPT-5.2-Codex',
[CODEX_MODEL_MAP.gpt51CodexMax]: 'GPT-5.1-Codex-Max',
[CODEX_MODEL_MAP.gpt51CodexMini]: 'GPT-5.1-Codex-Mini',
diff --git a/libs/types/src/model.ts b/libs/types/src/model.ts
index 5538989e..b6b90da9 100644
--- a/libs/types/src/model.ts
+++ b/libs/types/src/model.ts
@@ -50,15 +50,17 @@ export const LEGACY_CLAUDE_ALIAS_MAP: Record = {
*/
export const CODEX_MODEL_MAP = {
// 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',
- /** Optimized for long-horizon, agentic coding tasks in Codex */
+ /** Codex-optimized flagship for deep and fast reasoning */
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',
// 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',
/** Great for coding and agentic tasks across domains */
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
*/
export const REASONING_CAPABLE_MODELS = new Set([
+ CODEX_MODEL_MAP.gpt53Codex,
CODEX_MODEL_MAP.gpt52Codex,
CODEX_MODEL_MAP.gpt51CodexMax,
CODEX_MODEL_MAP.gpt52,
diff --git a/package-lock.json b/package-lock.json
index 9f4f4d28..0649982d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -45,7 +45,7 @@
"@automaker/utils": "1.0.0",
"@github/copilot-sdk": "^0.1.16",
"@modelcontextprotocol/sdk": "1.25.2",
- "@openai/codex-sdk": "^0.77.0",
+ "@openai/codex-sdk": "^0.98.0",
"cookie-parser": "1.4.7",
"cors": "2.8.5",
"dotenv": "17.2.3",
@@ -3949,9 +3949,9 @@
}
},
"node_modules/@openai/codex-sdk": {
- "version": "0.77.0",
- "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.77.0.tgz",
- "integrity": "sha512-bvJQ4dASnZ7jgfxmseViQwdRupHxs0TwHSZFeYB0gpdOAXnWwDWdGJRCMyphLSHwExRp27JNOk7EBFVmZRBanQ==",
+ "version": "0.98.0",
+ "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.98.0.tgz",
+ "integrity": "sha512-TbPgrBpuSNMJyOXys0HNsh6UoP5VIHu1fVh2KDdACi5XyB0vuPtzBZC+qOsxHz7WXEQPFlomPLyxS6JnE5Okmg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
From 835ffe31853dde25d3676ca6dd4a100268f39570 Mon Sep 17 00:00:00 2001
From: Kacper
Date: Thu, 5 Feb 2026 22:43:22 +0100
Subject: [PATCH 07/14] feat: update Claude model to Opus 4.6 and enhance
adaptive thinking support
- Changed model identifier from `claude-opus-4-5-20251101` to `claude-opus-4-6` across various files, including documentation and code references.
- Updated the SDK to support adaptive thinking for Opus 4.6, allowing the model to determine its own reasoning depth.
- Enhanced the thinking level options to include 'adaptive' and adjusted related components to reflect this change.
- Updated tests to ensure compatibility with the new model and its features.
These changes improve the model's capabilities and user experience by leveraging adaptive reasoning.
---
CLAUDE.md | 2 +-
apps/server/package.json | 2 +-
apps/server/src/lib/sdk-options.ts | 18 ++++++++++-
apps/server/src/providers/claude-provider.ts | 17 ++++++----
apps/server/src/providers/provider-factory.ts | 2 +-
.../tests/unit/lib/model-resolver.test.ts | 4 +--
.../server/tests/unit/lib/sdk-options.test.ts | 24 ++++++++++++++
.../unit/providers/claude-provider.test.ts | 32 +++++++++----------
.../unit/providers/provider-factory.test.ts | 6 ++--
apps/ui/docs/AGENT_ARCHITECTURE.md | 2 +-
.../board-view/dialogs/add-feature-dialog.tsx | 16 +++++++++-
.../board-view/shared/model-constants.ts | 10 +++++-
.../shared/thinking-level-selector.tsx | 14 ++++++--
.../model-defaults/phase-model-selector.tsx | 18 ++++++++---
apps/ui/src/lib/agent-context-parser.ts | 3 +-
docs/llm-shared-packages.md | 2 +-
docs/server/providers.md | 6 ++--
docs/server/utilities.md | 12 +++----
libs/model-resolver/README.md | 12 +++----
libs/model-resolver/tests/resolver.test.ts | 4 +--
libs/types/src/index.ts | 2 ++
libs/types/src/model-display.ts | 2 ++
libs/types/src/model.ts | 6 ++--
libs/types/src/settings.ts | 23 ++++++++++++-
package-lock.json | 10 +++---
25 files changed, 178 insertions(+), 71 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 128cd8d7..84dd1fbb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -161,7 +161,7 @@ Use `resolveModelString()` from `@automaker/model-resolver` to convert model ali
- `haiku` → `claude-haiku-4-5`
- `sonnet` → `claude-sonnet-4-20250514`
-- `opus` → `claude-opus-4-5-20251101`
+- `opus` → `claude-opus-4-6`
## Environment Variables
diff --git a/apps/server/package.json b/apps/server/package.json
index 0b4deeac..ed005c54 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -24,7 +24,7 @@
"test:unit": "vitest run tests/unit"
},
"dependencies": {
- "@anthropic-ai/claude-agent-sdk": "0.1.76",
+ "@anthropic-ai/claude-agent-sdk": "0.2.32",
"@automaker/dependency-resolver": "1.0.0",
"@automaker/git-utils": "1.0.0",
"@automaker/model-resolver": "1.0.0",
diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts
index cc1df2f5..674350a5 100644
--- a/apps/server/src/lib/sdk-options.ts
+++ b/apps/server/src/lib/sdk-options.ts
@@ -253,11 +253,27 @@ function buildMcpOptions(config: CreateSdkOptionsConfig): McpOptions {
/**
* Build thinking options for SDK configuration.
* 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
- * @returns Object with maxThinkingTokens if thinking is enabled
+ * @returns Object with maxThinkingTokens if thinking is enabled with a budget
*/
function buildThinkingOptions(thinkingLevel?: ThinkingLevel): Partial {
+ 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);
logger.debug(
`buildThinkingOptions: thinkingLevel="${thinkingLevel}" -> maxThinkingTokens=${maxThinkingTokens}`
diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts
index cfb59093..78a0a0c7 100644
--- a/apps/server/src/providers/claude-provider.ts
+++ b/apps/server/src/providers/claude-provider.ts
@@ -219,8 +219,11 @@ export class ClaudeProvider extends BaseProvider {
// claudeCompatibleProvider takes precedence over claudeApiProfile
const providerConfig = claudeCompatibleProvider || claudeApiProfile;
- // Convert thinking level to token budget
- const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel);
+ // Build thinking configuration
+ // 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
const sdkOptions: Options = {
@@ -349,13 +352,13 @@ export class ClaudeProvider extends BaseProvider {
getAvailableModels(): ModelDefinition[] {
const models = [
{
- id: 'claude-opus-4-5-20251101',
- name: 'Claude Opus 4.5',
- modelString: 'claude-opus-4-5-20251101',
+ id: 'claude-opus-4-6',
+ name: 'Claude Opus 4.6',
+ modelString: 'claude-opus-4-6',
provider: 'anthropic',
- description: 'Most capable Claude model',
+ description: 'Most capable Claude model with adaptive thinking',
contextWindow: 200000,
- maxOutputTokens: 16000,
+ maxOutputTokens: 128000,
supportsVision: true,
supportsTools: true,
tier: 'premium' as const,
diff --git a/apps/server/src/providers/provider-factory.ts b/apps/server/src/providers/provider-factory.ts
index 1e91760f..a6dff69e 100644
--- a/apps/server/src/providers/provider-factory.ts
+++ b/apps/server/src/providers/provider-factory.ts
@@ -103,7 +103,7 @@ export class ProviderFactory {
/**
* 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.throwOnDisconnected Throw error if provider is disconnected (default: true)
* @returns Provider instance for the model
diff --git a/apps/server/tests/unit/lib/model-resolver.test.ts b/apps/server/tests/unit/lib/model-resolver.test.ts
index c1bff78d..65e3115d 100644
--- a/apps/server/tests/unit/lib/model-resolver.test.ts
+++ b/apps/server/tests/unit/lib/model-resolver.test.ts
@@ -35,7 +35,7 @@ describe('model-resolver.ts', () => {
it("should resolve 'opus' alias to full model string", () => {
const result = resolveModelString('opus');
- expect(result).toBe('claude-opus-4-5-20251101');
+ expect(result).toBe('claude-opus-4-6');
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining('Migrated legacy ID: "opus" -> "claude-opus"')
);
@@ -117,7 +117,7 @@ describe('model-resolver.ts', () => {
describe('getEffectiveModel', () => {
it('should prioritize explicit model over session and default', () => {
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', () => {
diff --git a/apps/server/tests/unit/lib/sdk-options.test.ts b/apps/server/tests/unit/lib/sdk-options.test.ts
index 029cd8fa..69d69794 100644
--- a/apps/server/tests/unit/lib/sdk-options.test.ts
+++ b/apps/server/tests/unit/lib/sdk-options.test.ts
@@ -491,5 +491,29 @@ describe('sdk-options.ts', () => {
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();
+ });
+ });
});
});
diff --git a/apps/server/tests/unit/providers/claude-provider.test.ts b/apps/server/tests/unit/providers/claude-provider.test.ts
index c3f83f8f..7df211ef 100644
--- a/apps/server/tests/unit/providers/claude-provider.test.ts
+++ b/apps/server/tests/unit/providers/claude-provider.test.ts
@@ -39,7 +39,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Hello',
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/test',
});
@@ -59,7 +59,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Test prompt',
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/test/dir',
systemPrompt: 'You are helpful',
maxTurns: 10,
@@ -71,7 +71,7 @@ describe('claude-provider.ts', () => {
expect(sdk.query).toHaveBeenCalledWith({
prompt: 'Test prompt',
options: expect.objectContaining({
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
systemPrompt: 'You are helpful',
maxTurns: 10,
cwd: '/test/dir',
@@ -91,7 +91,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Test',
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/test',
});
@@ -116,7 +116,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Test',
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/test',
abortController,
});
@@ -145,7 +145,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Current message',
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/test',
conversationHistory,
sdkSessionId: 'test-session-id',
@@ -176,7 +176,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: arrayPrompt as any,
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/test',
});
@@ -196,7 +196,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Test',
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/test',
});
@@ -222,7 +222,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Test',
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/test',
});
@@ -286,7 +286,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Test',
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/test',
});
@@ -313,7 +313,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Test',
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/test',
});
@@ -341,7 +341,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Test',
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/test',
});
@@ -366,12 +366,12 @@ describe('claude-provider.ts', () => {
expect(models).toHaveLength(4);
});
- it('should include Claude Opus 4.5', () => {
+ it('should include Claude Opus 4.6', () => {
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?.name).toBe('Claude Opus 4.5');
+ expect(opus?.name).toBe('Claude Opus 4.6');
expect(opus?.provider).toBe('anthropic');
});
@@ -400,7 +400,7 @@ describe('claude-provider.ts', () => {
it('should mark Opus as default', () => {
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);
});
diff --git a/apps/server/tests/unit/providers/provider-factory.test.ts b/apps/server/tests/unit/providers/provider-factory.test.ts
index fbf01e90..b9aef928 100644
--- a/apps/server/tests/unit/providers/provider-factory.test.ts
+++ b/apps/server/tests/unit/providers/provider-factory.test.ts
@@ -54,8 +54,8 @@ describe('provider-factory.ts', () => {
describe('getProviderForModel', () => {
describe('Claude models (claude-* prefix)', () => {
- it('should return ClaudeProvider for claude-opus-4-5-20251101', () => {
- const provider = ProviderFactory.getProviderForModel('claude-opus-4-5-20251101');
+ it('should return ClaudeProvider for claude-opus-4-6', () => {
+ const provider = ProviderFactory.getProviderForModel('claude-opus-4-6');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
@@ -70,7 +70,7 @@ describe('provider-factory.ts', () => {
});
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);
});
});
diff --git a/apps/ui/docs/AGENT_ARCHITECTURE.md b/apps/ui/docs/AGENT_ARCHITECTURE.md
index 4c9f0d11..f5c374c4 100644
--- a/apps/ui/docs/AGENT_ARCHITECTURE.md
+++ b/apps/ui/docs/AGENT_ARCHITECTURE.md
@@ -199,7 +199,7 @@ The agent is configured with:
```javascript
{
- model: "claude-opus-4-5-20251101",
+ model: "claude-opus-4-6",
maxTurns: 20,
cwd: workingDirectory,
allowedTools: [
diff --git a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
index b8dd8776..2dbf0808 100644
--- a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
+++ b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
@@ -264,7 +264,21 @@ export function AddFeatureDialog({
}, [planningMode]);
const handleModelChange = (entry: PhaseModelEntry) => {
- setModelEntry(entry);
+ // Normalize thinking level when switching between adaptive and non-adaptive models
+ const isNewModelAdaptive =
+ entry.model === 'claude-opus' ||
+ (typeof entry.model === 'string' && entry.model.includes('opus-4-6'));
+ 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 => {
diff --git a/apps/ui/src/components/views/board-view/shared/model-constants.ts b/apps/ui/src/components/views/board-view/shared/model-constants.ts
index c56ad46a..2816e556 100644
--- a/apps/ui/src/components/views/board-view/shared/model-constants.ts
+++ b/apps/ui/src/components/views/board-view/shared/model-constants.ts
@@ -167,7 +167,14 @@ export const ALL_MODELS: ModelOption[] = [
...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 = {
none: 'None',
@@ -175,6 +182,7 @@ export const THINKING_LEVEL_LABELS: Record = {
medium: 'Med',
high: 'High',
ultrathink: 'Ultra',
+ adaptive: 'Adaptive',
};
/**
diff --git a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
index 74b791a3..3e111a31 100644
--- a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
+++ b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
@@ -2,19 +2,25 @@ import { Label } from '@/components/ui/label';
import { Brain } from 'lucide-react';
import { cn } from '@/lib/utils';
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 {
selectedLevel: ThinkingLevel;
onLevelSelect: (level: ThinkingLevel) => void;
testIdPrefix?: string;
+ /** Optional model ID to filter available thinking levels (e.g., Opus 4.6 only shows None/Adaptive) */
+ model?: string;
}
export function ThinkingLevelSelector({
selectedLevel,
onLevelSelect,
testIdPrefix = 'thinking-level',
+ model,
}: ThinkingLevelSelectorProps) {
+ const levels = model ? getThinkingLevelsForModel(model) : getThinkingLevelsForModel('');
+
return (
@@ -22,7 +28,7 @@ export function ThinkingLevelSelector({
Thinking Level
- {THINKING_LEVELS.map((level) => (
+ {levels.map((level) => (
- Higher levels give more time to reason through complex problems.
+ {model && getThinkingLevelsForModel(model).includes('adaptive')
+ ? 'Adaptive thinking lets the model decide how much reasoning to use.'
+ : 'Higher levels give more time to reason through complex problems.'}
);
diff --git a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx
index 20420388..25424fa6 100644
--- a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx
+++ b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx
@@ -21,6 +21,7 @@ import {
isGroupSelected,
getSelectedVariant,
codexModelHasThinking,
+ getThinkingLevelsForModel,
} from '@automaker/types';
import {
CLAUDE_MODELS,
@@ -28,7 +29,6 @@ import {
OPENCODE_MODELS,
GEMINI_MODELS,
COPILOT_MODELS,
- THINKING_LEVELS,
THINKING_LEVEL_LABELS,
REASONING_EFFORT_LEVELS,
REASONING_EFFORT_LABELS,
@@ -1296,7 +1296,9 @@ export function PhaseModelSelector({
Thinking Level
- {THINKING_LEVELS.map((level) => (
+ {getThinkingLevelsForModel(
+ model.mapsToClaudeModel === 'opus' ? 'claude-opus' : ''
+ ).map((level) => (
{
@@ -1322,6 +1324,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
+ {level === 'adaptive' && 'Model decides reasoning depth'}
{isSelected && currentThinking === level && (
@@ -1402,7 +1405,9 @@ export function PhaseModelSelector({
Thinking Level
- {THINKING_LEVELS.map((level) => (
+ {getThinkingLevelsForModel(
+ model.mapsToClaudeModel === 'opus' ? 'claude-opus' : ''
+ ).map((level) => (
{
@@ -1428,6 +1433,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
+ {level === 'adaptive' && 'Model decides reasoning depth'}
{isSelected && currentThinking === level && (
@@ -1564,7 +1570,7 @@ export function PhaseModelSelector({
Thinking Level
- {THINKING_LEVELS.map((level) => (
+ {getThinkingLevelsForModel(model.id).map((level) => (
{
@@ -1589,6 +1595,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
+ {level === 'adaptive' && 'Model decides reasoning depth'}
{isSelected && currentThinking === level && (
@@ -1685,7 +1692,7 @@ export function PhaseModelSelector({
Thinking Level
- {THINKING_LEVELS.map((level) => (
+ {getThinkingLevelsForModel(model.id).map((level) => (
{
@@ -1710,6 +1717,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
+ {level === 'adaptive' && 'Model decides reasoning depth'}
{isSelected && currentThinking === level && (
diff --git a/apps/ui/src/lib/agent-context-parser.ts b/apps/ui/src/lib/agent-context-parser.ts
index 8313e055..d6aa877a 100644
--- a/apps/ui/src/lib/agent-context-parser.ts
+++ b/apps/ui/src/lib/agent-context-parser.ts
@@ -27,13 +27,14 @@ export interface AgentTaskInfo {
/**
* 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
*/
export function formatModelName(model: string): string {
// Claude models
+ if (model.includes('opus-4-6')) return 'Opus 4.6';
if (model.includes('opus')) return 'Opus 4.5';
if (model.includes('sonnet')) return 'Sonnet 4.5';
if (model.includes('haiku')) return 'Haiku 4.5';
diff --git a/docs/llm-shared-packages.md b/docs/llm-shared-packages.md
index 9a81ad90..9f558c96 100644
--- a/docs/llm-shared-packages.md
+++ b/docs/llm-shared-packages.md
@@ -142,7 +142,7 @@ const modelId = resolveModelString('sonnet'); // → 'claude-sonnet-4-20250514'
- `haiku` → `claude-haiku-4-5` (fast, simple tasks)
- `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
diff --git a/docs/server/providers.md b/docs/server/providers.md
index 757ecab1..4dae626e 100644
--- a/docs/server/providers.md
+++ b/docs/server/providers.md
@@ -175,7 +175,7 @@ Uses `@anthropic-ai/claude-agent-sdk` for direct SDK integration.
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"`
#### Authentication
@@ -191,7 +191,7 @@ const provider = new ClaudeProvider();
const stream = provider.executeQuery({
prompt: 'What is 2+2?',
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
cwd: '/project/path',
systemPrompt: 'You are a helpful assistant.',
maxTurns: 20,
@@ -701,7 +701,7 @@ Test provider interaction with services:
```typescript
describe('Provider Integration', () => {
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
});
diff --git a/docs/server/utilities.md b/docs/server/utilities.md
index b12e60a2..91d301bb 100644
--- a/docs/server/utilities.md
+++ b/docs/server/utilities.md
@@ -213,7 +213,7 @@ Model alias mapping for Claude models.
export const CLAUDE_MODEL_MAP: Record = {
haiku: 'claude-haiku-4-5',
sonnet: 'claude-sonnet-4-20250514',
- opus: 'claude-opus-4-5-20251101',
+ opus: 'claude-opus-4-6',
} as const;
```
@@ -223,7 +223,7 @@ Default models per provider.
```typescript
export const DEFAULT_MODELS = {
- claude: 'claude-opus-4-5-20251101',
+ claude: 'claude-opus-4-6',
openai: 'gpt-5.2',
} 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';
resolveModelString('opus');
-// Returns: "claude-opus-4-5-20251101"
-// Logs: "[ModelResolver] Resolved model alias: "opus" -> "claude-opus-4-5-20251101""
+// Returns: "claude-opus-4-6"
+// Logs: "[ModelResolver] Resolved model alias: "opus" -> "claude-opus-4-6""
resolveModelString('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"
resolveModelString('invalid-model');
-// Returns: "claude-opus-4-5-20251101"
-// Logs: "[ModelResolver] Unknown model key "invalid-model", using default: "claude-opus-4-5-20251101""
+// Returns: "claude-opus-4-6"
+// Logs: "[ModelResolver] Unknown model key "invalid-model", using default: "claude-opus-4-6""
```
---
diff --git a/libs/model-resolver/README.md b/libs/model-resolver/README.md
index 50bdf4f9..ce5aa3ce 100644
--- a/libs/model-resolver/README.md
+++ b/libs/model-resolver/README.md
@@ -30,15 +30,15 @@ const model2 = resolveModelString('haiku');
// Returns: 'claude-haiku-4-5'
const model3 = resolveModelString('opus');
-// Returns: 'claude-opus-4-5-20251101'
+// Returns: 'claude-opus-4-6'
// Use with custom default
const model4 = resolveModelString(undefined, 'claude-sonnet-4-20250514');
// Returns: 'claude-sonnet-4-20250514' (default)
// Direct model ID passthrough
-const model5 = resolveModelString('claude-opus-4-5-20251101');
-// Returns: 'claude-opus-4-5-20251101' (unchanged)
+const model5 = resolveModelString('claude-opus-4-6');
+// Returns: 'claude-opus-4-6' (unchanged)
```
### Get Effective Model
@@ -72,7 +72,7 @@ console.log(DEFAULT_MODELS.chat); // 'claude-sonnet-4-20250514'
// Model alias mappings
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.opus); // 'claude-opus-4-5-20251101'
+console.log(CLAUDE_MODEL_MAP.opus); // 'claude-opus-4-6'
```
## Usage Example
@@ -103,7 +103,7 @@ const feature: Feature = {
};
prepareFeatureExecution(feature);
-// Output: Executing feature with model: claude-opus-4-5-20251101
+// Output: Executing feature with model: claude-opus-4-6
```
## Supported Models
@@ -112,7 +112,7 @@ prepareFeatureExecution(feature);
- `haiku` → `claude-haiku-4-5`
- `sonnet` → `claude-sonnet-4-20250514`
-- `opus` → `claude-opus-4-5-20251101`
+- `opus` → `claude-opus-4-6`
### Model Selection Guide
diff --git a/libs/model-resolver/tests/resolver.test.ts b/libs/model-resolver/tests/resolver.test.ts
index 84623b5b..7b6af623 100644
--- a/libs/model-resolver/tests/resolver.test.ts
+++ b/libs/model-resolver/tests/resolver.test.ts
@@ -484,12 +484,12 @@ describe('model-resolver', () => {
it('should handle full Claude model string in entry', () => {
const entry: PhaseModelEntry = {
- model: 'claude-opus-4-5-20251101',
+ model: 'claude-opus-4-6',
thinkingLevel: 'high',
};
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');
});
});
diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts
index d6d305fe..e9193327 100644
--- a/libs/types/src/index.ts
+++ b/libs/types/src/index.ts
@@ -196,6 +196,8 @@ export {
PROJECT_SETTINGS_VERSION,
THINKING_TOKEN_BUDGET,
getThinkingTokenBudget,
+ isAdaptiveThinkingModel,
+ getThinkingLevelsForModel,
// Event hook constants
EVENT_HOOK_TRIGGER_LABELS,
// Claude-compatible provider templates (new)
diff --git a/libs/types/src/model-display.ts b/libs/types/src/model-display.ts
index 08eaf208..f8a53e8b 100644
--- a/libs/types/src/model-display.ts
+++ b/libs/types/src/model-display.ts
@@ -149,6 +149,7 @@ export const THINKING_LEVELS: ThinkingLevelOption[] = [
{ id: 'medium', label: 'Medium' },
{ id: 'high', label: 'High' },
{ id: 'ultrathink', label: 'Ultrathink' },
+ { id: 'adaptive', label: 'Adaptive' },
];
/**
@@ -162,6 +163,7 @@ export const THINKING_LEVEL_LABELS: Record = {
medium: 'Med',
high: 'High',
ultrathink: 'Ultra',
+ adaptive: 'Adaptive',
};
/**
diff --git a/libs/types/src/model.ts b/libs/types/src/model.ts
index b6b90da9..2d540cc0 100644
--- a/libs/types/src/model.ts
+++ b/libs/types/src/model.ts
@@ -18,7 +18,7 @@ export type ClaudeCanonicalId = 'claude-haiku' | 'claude-sonnet' | 'claude-opus'
export const CLAUDE_CANONICAL_MAP: Record = {
'claude-haiku': 'claude-haiku-4-5-20251001',
'claude-sonnet': 'claude-sonnet-4-5-20250929',
- 'claude-opus': 'claude-opus-4-5-20251101',
+ 'claude-opus': 'claude-opus-4-6',
} as const;
/**
@@ -29,7 +29,7 @@ export const CLAUDE_CANONICAL_MAP: Record = {
export const CLAUDE_MODEL_MAP: Record = {
haiku: 'claude-haiku-4-5-20251001',
sonnet: 'claude-sonnet-4-5-20250929',
- opus: 'claude-opus-4-5-20251101',
+ opus: 'claude-opus-4-6',
} as const;
/**
@@ -99,7 +99,7 @@ export function getAllCodexModelIds(): CodexModelId[] {
* Uses canonical prefixed IDs for consistent routing.
*/
export const DEFAULT_MODELS = {
- claude: 'claude-opus-4-5-20251101',
+ claude: 'claude-opus-4-6',
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
} as const;
diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts
index eb53564d..06743faa 100644
--- a/libs/types/src/settings.ts
+++ b/libs/types/src/settings.ts
@@ -213,7 +213,7 @@ export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
export type ServerLogLevel = 'error' | 'warn' | 'info' | 'debug';
/** 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
@@ -237,6 +237,7 @@ export const THINKING_TOKEN_BUDGET: Record =
medium: 10000, // Light reasoning
high: 16000, // Complex tasks (recommended starting point)
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];
}
+/**
+ * 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 */
export type ModelProvider = 'claude' | 'cursor' | 'codex' | 'opencode' | 'gemini' | 'copilot';
diff --git a/package-lock.json b/package-lock.json
index 0649982d..8804b479 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,7 +35,7 @@
"version": "0.13.0",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
- "@anthropic-ai/claude-agent-sdk": "0.1.76",
+ "@anthropic-ai/claude-agent-sdk": "0.2.32",
"@automaker/dependency-resolver": "1.0.0",
"@automaker/git-utils": "1.0.0",
"@automaker/model-resolver": "1.0.0",
@@ -657,9 +657,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
- "version": "0.1.76",
- "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.76.tgz",
- "integrity": "sha512-s7RvpXoFaLXLG7A1cJBAPD8ilwOhhc/12fb5mJXRuD561o4FmPtQ+WRfuy9akMmrFRfLsKv8Ornw3ClGAPL2fw==",
+ "version": "0.2.32",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.32.tgz",
+ "integrity": "sha512-8AtsSx/M9jxd0ihS08eqa7VireTEuwQy0i1+6ZJX93LECT6Svlf47dPJiAm7JB+BhVMmwTfQeS6x1akIcCfvbQ==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
@@ -675,7 +675,7 @@
"@img/sharp-win32-x64": "^0.33.5"
},
"peerDependencies": {
- "zod": "^3.24.1 || ^4.0.0"
+ "zod": "^4.0.0"
}
},
"node_modules/@automaker/dependency-resolver": {
From f97453484fc7f484cfeefcde2df1ee52fac3180b Mon Sep 17 00:00:00 2001
From: Kacper
Date: Thu, 5 Feb 2026 23:05:19 +0100
Subject: [PATCH 08/14] feat: enhance adaptive thinking model support and
update UI components
- Added `isAdaptiveThinkingModel` utility to improve model identification logic in the AddFeatureDialog.
- Updated the ThinkingLevelSelector to conditionally display information based on available thinking levels.
- Enhanced model name formatting in agent-context-parser to include 'GPT-5.3 Codex' for better clarity.
These changes improve the user experience by refining model handling and UI feedback related to adaptive thinking capabilities.
---
.../views/board-view/dialogs/add-feature-dialog.tsx | 5 ++---
.../views/board-view/shared/thinking-level-selector.tsx | 2 +-
apps/ui/src/lib/agent-context-parser.ts | 1 +
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
index 2dbf0808..a816204f 100644
--- a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
+++ b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
@@ -28,7 +28,7 @@ import { cn } from '@/lib/utils';
import { modelSupportsThinking } from '@/lib/utils';
import { useAppStore, ThinkingLevel, FeatureImage, PlanningMode, Feature } from '@/store/app-store';
import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types';
-import { supportsReasoningEffort } from '@automaker/types';
+import { supportsReasoningEffort, isAdaptiveThinkingModel } from '@automaker/types';
import {
PrioritySelector,
WorkModeSelector,
@@ -266,8 +266,7 @@ export function AddFeatureDialog({
const handleModelChange = (entry: PhaseModelEntry) => {
// Normalize thinking level when switching between adaptive and non-adaptive models
const isNewModelAdaptive =
- entry.model === 'claude-opus' ||
- (typeof entry.model === 'string' && entry.model.includes('opus-4-6'));
+ typeof entry.model === 'string' && isAdaptiveThinkingModel(entry.model);
const currentLevel = entry.thinkingLevel || 'none';
if (isNewModelAdaptive && currentLevel !== 'none' && currentLevel !== 'adaptive') {
diff --git a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
index 3e111a31..c74b3e9a 100644
--- a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
+++ b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
@@ -46,7 +46,7 @@ export function ThinkingLevelSelector({
))}
- {model && getThinkingLevelsForModel(model).includes('adaptive')
+ {levels.includes('adaptive')
? 'Adaptive thinking lets the model decide how much reasoning to use.'
: 'Higher levels give more time to reason through complex problems.'}
diff --git a/apps/ui/src/lib/agent-context-parser.ts b/apps/ui/src/lib/agent-context-parser.ts
index d6aa877a..996b397b 100644
--- a/apps/ui/src/lib/agent-context-parser.ts
+++ b/apps/ui/src/lib/agent-context-parser.ts
@@ -40,6 +40,7 @@ export function formatModelName(model: string): string {
if (model.includes('haiku')) return 'Haiku 4.5';
// 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') return 'GPT-5.2';
if (model === 'codex-gpt-5.1-codex-max') return 'GPT-5.1 Max';
From 220c8e4ddf8a31adb68b831c63590e8cb742d08a Mon Sep 17 00:00:00 2001
From: Kacper
Date: Thu, 5 Feb 2026 23:19:31 +0100
Subject: [PATCH 09/14] feat: add 'dev-server:url-detected' event type to
EventType
- Introduced a new event type 'dev-server:url-detected' to enhance event handling for the development server.
- This addition allows for better tracking and response to URL detection during server operations.
These changes improve the event system's capability to manage server-related events effectively.
---
libs/types/src/event.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/libs/types/src/event.ts b/libs/types/src/event.ts
index 281f88d8..d11bfd07 100644
--- a/libs/types/src/event.ts
+++ b/libs/types/src/event.ts
@@ -46,6 +46,7 @@ export type EventType =
| 'dev-server:started'
| 'dev-server:output'
| 'dev-server:stopped'
+ | 'dev-server:url-detected'
| 'test-runner:started'
| 'test-runner:progress'
| 'test-runner:output'
From c70344156d3216a5ab828cee2b6756d73b39ce95 Mon Sep 17 00:00:00 2001
From: Shirone
Date: Sun, 15 Feb 2026 16:46:29 +0100
Subject: [PATCH 10/14] chore: update .gitignore to include new configuration
files
- Added .mcp.json and .planning to .gitignore to prevent tracking of configuration files.
---
.gitignore | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.gitignore b/.gitignore
index 7d6c7b0e..d7739863 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,3 +95,5 @@ data/.api-key
data/credentials.json
data/
.codex/
+.mcp.json
+.planning
\ No newline at end of file
From 8ed13564f6524f3658de10e02e568c08814d337f Mon Sep 17 00:00:00 2001
From: Shirone
Date: Sun, 15 Feb 2026 16:59:54 +0100
Subject: [PATCH 11/14] fix: address PR #757 review comments
- Extract getNvmWindowsCliPaths() helper to DRY up NVM_SYMLINK logic
- Update DEFAULT_MODELS.codex to gpt53Codex
- Simplify redundant ternary in thinking-level-selector
- Replace local supportsReasoningEffort with shared import from @automaker/types
- Use model.id fallback in phase-model-selector thinking level resolution
Co-Authored-By: Claude Opus 4.6
---
.../shared/thinking-level-selector.tsx | 2 +-
.../model-defaults/phase-model-selector.tsx | 4 +-
.../providers/codex-model-configuration.tsx | 13 +------
libs/platform/src/system-paths.ts | 37 ++++++++-----------
libs/types/src/model.ts | 2 +-
5 files changed, 21 insertions(+), 37 deletions(-)
diff --git a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
index c74b3e9a..5164e4fa 100644
--- a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
+++ b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
@@ -19,7 +19,7 @@ export function ThinkingLevelSelector({
testIdPrefix = 'thinking-level',
model,
}: ThinkingLevelSelectorProps) {
- const levels = model ? getThinkingLevelsForModel(model) : getThinkingLevelsForModel('');
+ const levels = getThinkingLevelsForModel(model || '');
return (
diff --git a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx
index 25424fa6..0f3c7889 100644
--- a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx
+++ b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx
@@ -1297,7 +1297,7 @@ export function PhaseModelSelector({
Thinking Level
{getThinkingLevelsForModel(
- model.mapsToClaudeModel === 'opus' ? 'claude-opus' : ''
+ model.mapsToClaudeModel === 'opus' ? 'claude-opus' : model.id || ''
).map((level) => (
{getThinkingLevelsForModel(
- model.mapsToClaudeModel === 'opus' ? 'claude-opus' : ''
+ model.mapsToClaudeModel === 'opus' ? 'claude-opus' : model.id || ''
).map((level) => (
);
}
-
-function supportsReasoningEffort(modelId: string): boolean {
- const reasoningModels = [
- 'codex-gpt-5.3-codex',
- 'codex-gpt-5.2-codex',
- 'codex-gpt-5.1-codex-max',
- 'codex-gpt-5.2',
- 'codex-gpt-5.1',
- ];
- return reasoningModels.includes(modelId);
-}
diff --git a/libs/platform/src/system-paths.ts b/libs/platform/src/system-paths.ts
index ce1246eb..5a16e89b 100644
--- a/libs/platform/src/system-paths.ts
+++ b/libs/platform/src/system-paths.ts
@@ -25,6 +25,16 @@ import fs from 'fs/promises';
// 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
*/
@@ -54,19 +64,14 @@ export function getClaudeCliPaths(): string[] {
if (isWindows) {
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
- const nvmSymlink = process.env.NVM_SYMLINK;
- const paths = [
+ return [
path.join(os.homedir(), '.local', 'bin', 'claude.exe'),
path.join(appData, 'npm', 'claude.cmd'),
path.join(appData, 'npm', 'claude'),
path.join(appData, '.npm-global', 'bin', 'claude.cmd'),
path.join(appData, '.npm-global', 'bin', 'claude'),
+ ...getNvmWindowsCliPaths('claude'),
];
- // nvm4w (NVM for Windows) symlink path
- if (nvmSymlink) {
- paths.push(path.join(nvmSymlink, 'claude.cmd'), path.join(nvmSymlink, 'claude'));
- }
- return paths;
}
return [
@@ -136,8 +141,7 @@ export function getCodexCliPaths(): string[] {
if (isWindows) {
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local');
- const nvmSymlink = process.env.NVM_SYMLINK;
- const paths = [
+ return [
path.join(homeDir, '.local', 'bin', 'codex.exe'),
path.join(appData, 'npm', 'codex.cmd'),
path.join(appData, 'npm', 'codex'),
@@ -148,12 +152,8 @@ export function getCodexCliPaths(): string[] {
// pnpm on Windows
path.join(localAppData, 'pnpm', 'codex.cmd'),
path.join(localAppData, 'pnpm', 'codex'),
+ ...getNvmWindowsCliPaths('codex'),
];
- // nvm4w (NVM for Windows) symlink path
- if (nvmSymlink) {
- paths.push(path.join(nvmSymlink, 'codex.cmd'), path.join(nvmSymlink, 'codex'));
- }
- return paths;
}
// Include NVM bin paths for codex installed via npm global under NVM
@@ -1138,8 +1138,7 @@ export function getOpenCodeCliPaths(): string[] {
if (isWindows) {
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local');
- const nvmSymlink = process.env.NVM_SYMLINK;
- const paths = [
+ return [
// OpenCode's default installation directory
path.join(homeDir, '.opencode', 'bin', 'opencode.exe'),
path.join(homeDir, '.local', 'bin', 'opencode.exe'),
@@ -1155,12 +1154,8 @@ export function getOpenCodeCliPaths(): string[] {
// Go installation (if OpenCode is a Go binary)
path.join(homeDir, 'go', 'bin', 'opencode.exe'),
path.join(process.env.GOPATH || path.join(homeDir, 'go'), 'bin', 'opencode.exe'),
+ ...getNvmWindowsCliPaths('opencode'),
];
- // nvm4w (NVM for Windows) symlink path
- if (nvmSymlink) {
- paths.push(path.join(nvmSymlink, 'opencode.cmd'), path.join(nvmSymlink, 'opencode'));
- }
- return paths;
}
// Include NVM bin paths for opencode installed via npm global under NVM
diff --git a/libs/types/src/model.ts b/libs/types/src/model.ts
index 2d540cc0..cdf23cf2 100644
--- a/libs/types/src/model.ts
+++ b/libs/types/src/model.ts
@@ -101,7 +101,7 @@ export function getAllCodexModelIds(): CodexModelId[] {
export const DEFAULT_MODELS = {
claude: 'claude-opus-4-6',
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;
export type ModelAlias = keyof typeof CLAUDE_MODEL_MAP;
From d4f68b659bf022a5916aedaa8f59e49bc984a349 Mon Sep 17 00:00:00 2001
From: Shirone
Date: Sun, 15 Feb 2026 17:46:25 +0100
Subject: [PATCH 12/14] fix: address PR #747 review comments
- Fix warning box path lines being 2 chars too wide (BOX_CONTENT_WIDTH - 4)
- Wrap getClaudeAuthIndicators in try/catch to prevent 500 on auth success
- Convert dynamic import to static import for @automaker/platform
- Simplify verbose debug logging to log objects directly
- Remove unnecessary truthy checks on always-populated path strings
Co-Authored-By: Claude Opus 4.6
---
apps/server/src/index.ts | 54 +++----------------
.../routes/setup/routes/verify-claude-auth.ts | 12 ++---
2 files changed, 14 insertions(+), 52 deletions(-)
diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts
index 3cda977a..4f49d117 100644
--- a/apps/server/src/index.ts
+++ b/apps/server/src/index.ts
@@ -148,42 +148,10 @@ const BOX_CONTENT_WIDTH = 67;
const indicators = cliAuthIndicators;
// Log detailed credential detection results
- logger.debug('[CREDENTIAL_CHECK] Claude CLI auth indicators:', {
- hasCredentialsFile: indicators.hasCredentialsFile,
- hasSettingsFile: indicators.hasSettingsFile,
- hasStatsCacheWithActivity: indicators.hasStatsCacheWithActivity,
- hasProjectsSessions: indicators.hasProjectsSessions,
- credentials: indicators.credentials,
- });
+ const { checks, ...indicatorSummary } = indicators;
+ logger.debug('[CREDENTIAL_CHECK] Claude CLI auth indicators:', indicatorSummary);
- logger.debug('[CREDENTIAL_CHECK] File check details:', {
- settingsFile: {
- path: indicators.checks.settingsFile.path,
- exists: indicators.checks.settingsFile.exists,
- readable: indicators.checks.settingsFile.readable,
- error: indicators.checks.settingsFile.error,
- },
- statsCache: {
- path: indicators.checks.statsCache.path,
- exists: indicators.checks.statsCache.exists,
- readable: indicators.checks.statsCache.readable,
- hasDailyActivity: indicators.checks.statsCache.hasDailyActivity,
- error: indicators.checks.statsCache.error,
- },
- projectsDir: {
- path: indicators.checks.projectsDir.path,
- exists: indicators.checks.projectsDir.exists,
- readable: indicators.checks.projectsDir.readable,
- entryCount: indicators.checks.projectsDir.entryCount,
- error: indicators.checks.projectsDir.error,
- },
- credentialFiles: indicators.checks.credentialFiles.map((cf) => ({
- path: cf.path,
- exists: cf.exists,
- readable: cf.readable,
- error: cf.error,
- })),
- });
+ logger.debug('[CREDENTIAL_CHECK] File check details:', checks);
const hasCliAuth =
indicators.hasStatsCacheWithActivity ||
@@ -231,16 +199,10 @@ const BOX_CONTENT_WIDTH = 67;
if (cliAuthIndicators) {
const pathsChecked: string[] = [];
- // Collect paths that were checked
- if (cliAuthIndicators.checks.settingsFile.path) {
- pathsChecked.push(`Settings: ${cliAuthIndicators.checks.settingsFile.path}`);
- }
- if (cliAuthIndicators.checks.statsCache.path) {
- pathsChecked.push(`Stats cache: ${cliAuthIndicators.checks.statsCache.path}`);
- }
- if (cliAuthIndicators.checks.projectsDir.path) {
- pathsChecked.push(`Projects dir: ${cliAuthIndicators.checks.projectsDir.path}`);
- }
+ // 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}`);
}
@@ -249,7 +211,7 @@ const BOX_CONTENT_WIDTH = 67;
pathsCheckedInfo = `
║ ║
║ ${'Paths checked:'.padEnd(BOX_CONTENT_WIDTH)}║
-${pathsChecked.map((p) => `║ ${p.substring(0, BOX_CONTENT_WIDTH - 2).padEnd(BOX_CONTENT_WIDTH - 2)} ║`).join('\n')}`;
+${pathsChecked.map((p) => `║ ${p.substring(0, BOX_CONTENT_WIDTH - 4).padEnd(BOX_CONTENT_WIDTH - 4)} ║`).join('\n')}`;
}
}
diff --git a/apps/server/src/routes/setup/routes/verify-claude-auth.ts b/apps/server/src/routes/setup/routes/verify-claude-auth.ts
index 2a8d21b0..405ef9a6 100644
--- a/apps/server/src/routes/setup/routes/verify-claude-auth.ts
+++ b/apps/server/src/routes/setup/routes/verify-claude-auth.ts
@@ -6,6 +6,7 @@
import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils';
+import { getClaudeAuthIndicators } from '@automaker/platform';
import { getApiKey } from '../common.js';
import {
createSecureAuthEnv,
@@ -327,12 +328,11 @@ export function createVerifyClaudeAuthHandler() {
authType = 'api_key';
} else if (authMethod === 'cli') {
// Check if CLI auth is via OAuth (Claude Code subscription) or generic CLI
- // OAuth tokens are stored in the credentials file by the Claude CLI
- const { getClaudeAuthIndicators } = await import('@automaker/platform');
- const indicators = await getClaudeAuthIndicators();
- if (indicators.credentials?.hasOAuthToken) {
- authType = 'oauth';
- } else {
+ try {
+ const indicators = await getClaudeAuthIndicators();
+ authType = indicators.credentials?.hasOAuthToken ? 'oauth' : 'cli';
+ } catch {
+ // Fall back to generic CLI if credential check fails
authType = 'cli';
}
}
From 8226699734d22615fba2c7796d5a53599376bf7c Mon Sep 17 00:00:00 2001
From: Shirone
Date: Sun, 15 Feb 2026 18:01:45 +0100
Subject: [PATCH 13/14] fix(docker): add @playwright/test to server
devDependencies
The Dockerfile's playwright install step requires the binary in
node_modules/.bin/, but playwright was only a UI dependency. This adds
@playwright/test to server devDependencies so the Docker build can
successfully run `./node_modules/.bin/playwright install chromium`.
Fixes the "playwright: not found" error during Docker image build.
Co-Authored-By: Claude Opus 4.6
---
apps/server/package.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/apps/server/package.json b/apps/server/package.json
index ed005c54..4a7a75a8 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -45,6 +45,7 @@
"yaml": "2.7.0"
},
"devDependencies": {
+ "@playwright/test": "1.57.0",
"@types/cookie": "0.6.0",
"@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19",
From a08ba1b517278bb1cea2a2cd23ecaaf2bc6299a5 Mon Sep 17 00:00:00 2001
From: Shirone
Date: Sun, 15 Feb 2026 18:13:06 +0100
Subject: [PATCH 14/14] fix: address PR #745 review comments
- .gitignore: add missing trailing newline
- Dockerfile: remove misleading || echo fallback in Playwright install
- index.ts: truncate long paths from beginning instead of end in warning box
- verify-claude-auth.ts: use effectiveAuthMethod to prevent undefined authType
- agent-context-parser.ts: handle claude-opus alias as Opus 4.6
- thinking-level-selector.tsx: improve model prop documentation
Co-Authored-By: Claude Opus 4.6
---
.gitignore | 2 +-
Dockerfile | 2 +-
apps/server/src/index.ts | 8 +++++++-
apps/server/src/routes/setup/routes/verify-claude-auth.ts | 5 +++--
.../views/board-view/shared/thinking-level-selector.tsx | 3 ++-
apps/ui/src/lib/agent-context-parser.ts | 2 +-
6 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/.gitignore b/.gitignore
index d7739863..871c9896 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,4 +96,4 @@ data/credentials.json
data/
.codex/
.mcp.json
-.planning
\ No newline at end of file
+.planning
diff --git a/Dockerfile b/Dockerfile
index 2e745e4c..a68901e4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -154,7 +154,7 @@ COPY --from=server-builder /app/node_modules ./node_modules
USER automaker
RUN ./node_modules/.bin/playwright install chromium && \
echo "=== Playwright Chromium installed ===" && \
- ls -la /home/automaker/.cache/ms-playwright/ || echo "Playwright browsers installed"
+ ls -la /home/automaker/.cache/ms-playwright/
USER root
# Create data and projects directories
diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts
index 4f49d117..85ff0145 100644
--- a/apps/server/src/index.ts
+++ b/apps/server/src/index.ts
@@ -211,7 +211,13 @@ const BOX_CONTENT_WIDTH = 67;
pathsCheckedInfo = `
║ ║
║ ${'Paths checked:'.padEnd(BOX_CONTENT_WIDTH)}║
-${pathsChecked.map((p) => `║ ${p.substring(0, BOX_CONTENT_WIDTH - 4).padEnd(BOX_CONTENT_WIDTH - 4)} ║`).join('\n')}`;
+${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')}`;
}
}
diff --git a/apps/server/src/routes/setup/routes/verify-claude-auth.ts b/apps/server/src/routes/setup/routes/verify-claude-auth.ts
index 405ef9a6..7df27c3d 100644
--- a/apps/server/src/routes/setup/routes/verify-claude-auth.ts
+++ b/apps/server/src/routes/setup/routes/verify-claude-auth.ts
@@ -322,11 +322,12 @@ export function createVerifyClaudeAuthHandler() {
});
// Determine specific auth type for success messages
+ const effectiveAuthMethod = authMethod ?? 'api_key';
let authType: 'oauth' | 'api_key' | 'cli' | undefined;
if (authenticated) {
- if (authMethod === 'api_key') {
+ if (effectiveAuthMethod === 'api_key') {
authType = 'api_key';
- } else if (authMethod === 'cli') {
+ } else if (effectiveAuthMethod === 'cli') {
// Check if CLI auth is via OAuth (Claude Code subscription) or generic CLI
try {
const indicators = await getClaudeAuthIndicators();
diff --git a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
index 5164e4fa..3a69d587 100644
--- a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
+++ b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx
@@ -9,7 +9,8 @@ interface ThinkingLevelSelectorProps {
selectedLevel: ThinkingLevel;
onLevelSelect: (level: ThinkingLevel) => void;
testIdPrefix?: string;
- /** Optional model ID to filter available thinking levels (e.g., Opus 4.6 only shows None/Adaptive) */
+ /** Model ID is required for correct thinking level filtering.
+ * Without it, adaptive thinking won't be available for Opus 4.6. */
model?: string;
}
diff --git a/apps/ui/src/lib/agent-context-parser.ts b/apps/ui/src/lib/agent-context-parser.ts
index 996b397b..11c2a91e 100644
--- a/apps/ui/src/lib/agent-context-parser.ts
+++ b/apps/ui/src/lib/agent-context-parser.ts
@@ -34,7 +34,7 @@ export const DEFAULT_MODEL = 'claude-opus-4-6';
*/
export function formatModelName(model: string): string {
// Claude models
- if (model.includes('opus-4-6')) return 'Opus 4.6';
+ if (model.includes('opus-4-6') || model === 'claude-opus') return 'Opus 4.6';
if (model.includes('opus')) return 'Opus 4.5';
if (model.includes('sonnet')) return 'Sonnet 4.5';
if (model.includes('haiku')) return 'Haiku 4.5';