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

Resolved conflict in agent-service.ts by keeping both:
- agents parameter for custom subagents (from our branch)
- thinkingLevel and reasoningEffort parameters (from v0.9.0rc)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2026-01-08 22:57:09 +01:00
46 changed files with 1570 additions and 525 deletions

View File

@@ -6,7 +6,8 @@
import path from 'path';
import * as secureFs from '../lib/secure-fs.js';
import type { EventEmitter } from '../lib/events.js';
import type { ExecuteOptions, ThinkingLevel } from '@automaker/types';
import type { ExecuteOptions, ThinkingLevel, ReasoningEffort } from '@automaker/types';
import { stripProviderPrefix } from '@automaker/types';
import {
readImageAsBase64,
buildPromptWithImages,
@@ -59,6 +60,7 @@ interface Session {
workingDirectory: string;
model?: string;
thinkingLevel?: ThinkingLevel; // Thinking level for Claude models
reasoningEffort?: ReasoningEffort; // Reasoning effort for Codex models
sdkSessionId?: string; // Claude SDK session ID for conversation continuity
promptQueue: QueuedPrompt[]; // Queue of prompts to auto-run after current task
}
@@ -148,6 +150,7 @@ export class AgentService {
imagePaths,
model,
thinkingLevel,
reasoningEffort,
}: {
sessionId: string;
message: string;
@@ -155,6 +158,7 @@ export class AgentService {
imagePaths?: string[];
model?: string;
thinkingLevel?: ThinkingLevel;
reasoningEffort?: ReasoningEffort;
}) {
const session = this.sessions.get(sessionId);
if (!session) {
@@ -167,7 +171,7 @@ export class AgentService {
throw new Error('Agent is already processing a message');
}
// Update session model and thinking level if provided
// Update session model, thinking level, and reasoning effort if provided
if (model) {
session.model = model;
await this.updateSession(sessionId, { model });
@@ -175,6 +179,9 @@ export class AgentService {
if (thinkingLevel !== undefined) {
session.thinkingLevel = thinkingLevel;
}
if (reasoningEffort !== undefined) {
session.reasoningEffort = reasoningEffort;
}
// Validate vision support before processing images
const effectiveModel = model || session.model;
@@ -284,8 +291,9 @@ export class AgentService {
: baseSystemPrompt;
// Build SDK options using centralized configuration
// Use thinking level from request, or fall back to session's stored thinking level
// Use thinking level and reasoning effort from request, or fall back to session's stored values
const effectiveThinkingLevel = thinkingLevel ?? session.thinkingLevel;
const effectiveReasoningEffort = reasoningEffort ?? session.reasoningEffort;
const sdkOptions = createChatOptions({
cwd: effectiveWorkDir,
model: model,
@@ -342,13 +350,17 @@ export class AgentService {
}
}
// Get provider for this model
// Get provider for this model (with prefix)
const provider = ProviderFactory.getProviderForModel(effectiveModel);
// Strip provider prefix - providers should receive bare model IDs
const bareModel = stripProviderPrefix(effectiveModel);
// Build options for provider
const options: ExecuteOptions = {
prompt: '', // Will be set below based on images
model: effectiveModel,
model: bareModel, // Bare model ID (e.g., "gpt-5.1-codex-max", "composer-1")
originalModel: effectiveModel, // Original with prefix for logging (e.g., "codex-gpt-5.1-codex-max")
cwd: effectiveWorkDir,
systemPrompt: sdkOptions.systemPrompt,
maxTurns: maxTurns,
@@ -359,6 +371,8 @@ export class AgentService {
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration
agents: customSubagents, // Pass custom subagents for task delegation
thinkingLevel: effectiveThinkingLevel, // Pass thinking level for Claude models
reasoningEffort: effectiveReasoningEffort, // Pass reasoning effort for Codex models
};
// Build prompt content with images

View File

@@ -1,6 +1,9 @@
import * as os from 'os';
import { findCodexCliPath } from '@automaker/platform';
import { checkCodexAuthentication } from '../lib/codex-auth.js';
import { spawnProcess } from '@automaker/platform';
import * as fs from 'fs';
import * as path from 'path';
export interface CodexRateLimitWindow {
limit: number;
@@ -32,11 +35,10 @@ export interface CodexUsageData {
/**
* Codex Usage Service
*
* Unlike Claude Code CLI which provides a `/usage` command, Codex CLI
* does not expose usage statistics directly. This service returns a
* clear message explaining this limitation.
*
* Future enhancement: Could query OpenAI API headers for rate limit info.
* Attempts to fetch usage data from Codex CLI and OpenAI API.
* Codex CLI doesn't provide a direct usage command, but we can:
* 1. Parse usage info from error responses (rate limit errors contain plan info)
* 2. Check for OpenAI API usage if API key is available
*/
export class CodexUsageService {
private codexBinary = 'codex';
@@ -47,8 +49,6 @@ export class CodexUsageService {
* Check if Codex CLI is available on the system
*/
async isAvailable(): Promise<boolean> {
// Prefer our platform-aware resolver over `which/where` because the server
// process PATH may not include npm global bins (nvm/fnm/volta/pnpm).
this.cachedCliPath = await findCodexCliPath();
return Boolean(this.cachedCliPath);
}
@@ -56,31 +56,241 @@ export class CodexUsageService {
/**
* Attempt to fetch usage data
*
* Note: Codex CLI doesn't provide usage statistics like Claude Code does.
* This method returns an error explaining this limitation.
* Tries multiple approaches:
* 1. Check for OpenAI API key in environment
* 2. Make a test request to capture rate limit headers
* 3. Parse usage info from error responses
*/
async fetchUsageData(): Promise<CodexUsageData> {
// Check authentication status first
const isAuthenticated = await this.checkAuthentication();
const cliPath = this.cachedCliPath || (await findCodexCliPath());
if (!isAuthenticated) {
throw new Error("Codex is not authenticated. Please run 'codex login' to authenticate.");
if (!cliPath) {
throw new Error('Codex CLI not found. Please install it with: npm install -g @openai/codex');
}
// Codex CLI doesn't provide a usage command
// Return an error that will be caught and displayed
// Check if user has an API key that we can use
const hasApiKey = !!process.env.OPENAI_API_KEY;
if (hasApiKey) {
// Try to get usage from OpenAI API
const openaiUsage = await this.fetchOpenAIUsage();
if (openaiUsage) {
return openaiUsage;
}
}
// Try to get usage from Codex CLI by making a simple request
const codexUsage = await this.fetchCodexUsage(cliPath);
if (codexUsage) {
return codexUsage;
}
// Fallback: try to parse usage from auth file
const authUsage = await this.fetchFromAuthFile();
if (authUsage) {
return authUsage;
}
// If all else fails, return a message with helpful information
throw new Error(
'Codex usage statistics are not available. Unlike Claude Code, the Codex CLI does not provide a built-in usage command. ' +
'Usage limits are enforced by OpenAI but cannot be queried via the CLI. ' +
'Check your OpenAI dashboard at https://platform.openai.com/usage for detailed usage information.'
'Codex usage statistics require additional configuration. ' +
'To enable usage tracking:\n\n' +
'1. Set your OpenAI API key in the environment:\n' +
' export OPENAI_API_KEY=sk-...\n\n' +
'2. Or check your usage at:\n' +
' https://platform.openai.com/usage\n\n' +
'Note: If using Codex CLI with ChatGPT OAuth authentication, ' +
'usage data must be queried through your OpenAI account.'
);
}
/**
* Try to fetch usage from OpenAI API using the API key
*/
private async fetchOpenAIUsage(): Promise<CodexUsageData | null> {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) return null;
try {
const endTime = Math.floor(Date.now() / 1000);
const startTime = endTime - 7 * 24 * 60 * 60; // Last 7 days
const response = await fetch(
`https://api.openai.com/v1/organization/usage/completions?start_time=${startTime}&end_time=${endTime}&limit=1`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
}
);
if (response.ok) {
const data = await response.json();
return this.parseOpenAIUsage(data);
}
} catch (error) {
console.log('[CodexUsage] Failed to fetch from OpenAI API:', error);
}
return null;
}
/**
* Parse OpenAI usage API response
*/
private parseOpenAIUsage(data: any): CodexUsageData {
let totalInputTokens = 0;
let totalOutputTokens = 0;
if (data.data && Array.isArray(data.data)) {
for (const bucket of data.data) {
if (bucket.results && Array.isArray(bucket.results)) {
for (const result of bucket.results) {
totalInputTokens += result.input_tokens || 0;
totalOutputTokens += result.output_tokens || 0;
}
}
}
}
return {
rateLimits: {
planType: 'unknown',
credits: {
hasCredits: true,
},
},
lastUpdated: new Date().toISOString(),
};
}
/**
* Try to fetch usage by making a test request to Codex CLI
* and parsing rate limit information from the response
*/
private async fetchCodexUsage(cliPath: string): Promise<CodexUsageData | null> {
try {
// Make a simple request to trigger rate limit info if at limit
const result = await spawnProcess({
command: cliPath,
args: ['exec', '--', 'echo', 'test'],
cwd: process.cwd(),
env: {
...process.env,
TERM: 'dumb',
},
timeout: 10000,
});
// Parse the output for rate limit information
const combinedOutput = (result.stdout + result.stderr).toLowerCase();
// Check if we got a rate limit error
const rateLimitMatch = combinedOutput.match(
/usage_limit_reached.*?"plan_type":"([^"]+)".*?"resets_at":(\d+).*?"resets_in_seconds":(\d+)/
);
if (rateLimitMatch) {
const planType = rateLimitMatch[1] as CodexPlanType;
const resetsAt = parseInt(rateLimitMatch[2], 10);
const resetsInSeconds = parseInt(rateLimitMatch[3], 10);
return {
rateLimits: {
planType,
primary: {
limit: 0,
used: 0,
remaining: 0,
usedPercent: 100,
windowDurationMins: Math.ceil(resetsInSeconds / 60),
resetsAt,
},
},
lastUpdated: new Date().toISOString(),
};
}
// If no rate limit, return basic info
return {
rateLimits: {
planType: 'plus',
credits: {
hasCredits: true,
unlimited: false,
},
},
lastUpdated: new Date().toISOString(),
};
} catch (error) {
console.log('[CodexUsage] Failed to fetch from Codex CLI:', error);
}
return null;
}
/**
* Try to extract usage info from the Codex auth file
*/
private async fetchFromAuthFile(): Promise<CodexUsageData | null> {
try {
const authFilePath = path.join(os.homedir(), '.codex', 'auth.json');
if (fs.existsSync(authFilePath)) {
const authContent = fs.readFileSync(authFilePath, 'utf-8');
const authData = JSON.parse(authContent);
// Extract plan type from the ID token claims
if (authData.tokens?.id_token) {
const idToken = authData.tokens.id_token;
const claims = this.parseJwt(idToken);
const planType = claims?.['https://chatgpt.com/account_type'] || 'unknown';
const isPlus = planType === 'plus';
return {
rateLimits: {
planType: planType as CodexPlanType,
credits: {
hasCredits: true,
unlimited: !isPlus,
},
},
lastUpdated: new Date().toISOString(),
};
}
}
} catch (error) {
console.log('[CodexUsage] Failed to parse auth file:', error);
}
return null;
}
/**
* Parse JWT token to extract claims
*/
private parseJwt(token: string): any {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
} catch {
return null;
}
}
/**
* Check if Codex is authenticated
*/
private async checkAuthentication(): Promise<boolean> {
// Use the cached CLI path if available, otherwise fall back to finding it
const cliPath = this.cachedCliPath || (await findCodexCliPath());
const authCheck = await checkCodexAuthentication(cliPath);
return authCheck.authenticated;

View File

@@ -40,6 +40,7 @@ import type { SettingsService } from './settings-service.js';
import type { FeatureLoader } from './feature-loader.js';
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
import { resolveModelString } from '@automaker/model-resolver';
import { stripProviderPrefix } from '@automaker/types';
const logger = createLogger('IdeationService');
@@ -201,7 +202,7 @@ export class IdeationService {
existingWorkContext
);
// Resolve model alias to canonical identifier
// Resolve model alias to canonical identifier (with prefix)
const modelId = resolveModelString(options?.model ?? 'sonnet');
// Create SDK options
@@ -214,9 +215,13 @@ export class IdeationService {
const provider = ProviderFactory.getProviderForModel(modelId);
// Strip provider prefix - providers need bare model IDs
const bareModel = stripProviderPrefix(modelId);
const executeOptions: ExecuteOptions = {
prompt: message,
model: modelId,
model: bareModel,
originalModel: modelId,
cwd: projectPath,
systemPrompt: sdkOptions.systemPrompt,
maxTurns: 1, // Single turn for ideation
@@ -648,7 +653,7 @@ export class IdeationService {
existingWorkContext
);
// Resolve model alias to canonical identifier
// Resolve model alias to canonical identifier (with prefix)
const modelId = resolveModelString('sonnet');
// Create SDK options
@@ -661,9 +666,13 @@ export class IdeationService {
const provider = ProviderFactory.getProviderForModel(modelId);
// Strip provider prefix - providers need bare model IDs
const bareModel = stripProviderPrefix(modelId);
const executeOptions: ExecuteOptions = {
prompt: prompt.prompt,
model: modelId,
model: bareModel,
originalModel: modelId,
cwd: projectPath,
systemPrompt: sdkOptions.systemPrompt,
maxTurns: 1,