Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"express": "5.2.1",
"morgan": "1.10.1",
"node-pty": "1.1.0-beta41",
"openai": "6.15.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a discrepancy in the openai package version. The PR description states version ^4.77.3 is used, but this change introduces version 6.15.0. Could you please clarify which version is correct? Using an unexpected major version could introduce breaking changes.

"ws": "8.18.3"
},
"devDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import { SettingsService } from './services/settings-service.js';
import { createSpecRegenerationRoutes } from './routes/app-spec/index.js';
import { createClaudeRoutes } from './routes/claude/index.js';
import { ClaudeUsageService } from './services/claude-usage-service.js';
import { createCodexRoutes } from './routes/codex/index.js';
import { CodexUsageService } from './services/codex-usage-service.js';
import { createGitHubRoutes } from './routes/github/index.js';
import { createContextRoutes } from './routes/context/index.js';
import { createBacklogPlanRoutes } from './routes/backlog-plan/index.js';
Expand Down Expand Up @@ -166,6 +168,7 @@ const agentService = new AgentService(DATA_DIR, events, settingsService);
const featureLoader = new FeatureLoader();
const autoModeService = new AutoModeService(events, settingsService);
const claudeUsageService = new ClaudeUsageService();
const codexUsageService = new CodexUsageService();
const mcpTestService = new MCPTestService(settingsService);
const ideationService = new IdeationService(events, settingsService, featureLoader);

Expand Down Expand Up @@ -216,6 +219,7 @@ app.use('/api/templates', createTemplatesRoutes());
app.use('/api/terminal', createTerminalRoutes());
app.use('/api/settings', createSettingsRoutes(settingsService));
app.use('/api/claude', createClaudeRoutes(claudeUsageService));
app.use('/api/codex', createCodexRoutes(codexUsageService));
app.use('/api/github', createGitHubRoutes(events, settingsService));
app.use('/api/context', createContextRoutes(settingsService));
app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService));
Expand Down
92 changes: 92 additions & 0 deletions apps/server/src/providers/claude-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,67 @@ const ALLOWED_ENV_VARS = [
'LANG',
'LC_ALL',
];
const MOCK_RESPONSE_TEXT = 'Mock response from Claude provider.';
const MOCK_JSON_INDENT_SPACES = 2;
const DEFAULT_MOCK_STRING = 'mock';
const DEFAULT_MOCK_NUMBER = 0;
const DEFAULT_MOCK_BOOLEAN = false;

type JsonSchema = Record<string, unknown>;

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}

function getSchemaType(schema: JsonSchema): string | undefined {
const rawType = schema.type;
if (typeof rawType === 'string') {
return rawType;
}
if (Array.isArray(rawType)) {
const firstType = rawType.find((entry) => typeof entry === 'string');
return typeof firstType === 'string' ? firstType : undefined;
}
return undefined;
}

function buildMockStructuredOutput(schema: JsonSchema): Record<string, unknown> {
const properties = isRecord(schema.properties) ? schema.properties : {};
const required = Array.isArray(schema.required)
? schema.required.filter((entry): entry is string => typeof entry === 'string')
: [];
const keys = required.length > 0 ? required : Object.keys(properties);
const result: Record<string, unknown> = {};

for (const key of keys) {
const propertySchema = isRecord(properties[key]) ? properties[key] : {};
result[key] = buildMockValue(propertySchema);
}

return result;
}

function buildMockValue(schema: JsonSchema): unknown {
const schemaType = getSchemaType(schema);

if (schemaType === 'string') {
return DEFAULT_MOCK_STRING;
}
if (schemaType === 'number' || schemaType === 'integer') {
return DEFAULT_MOCK_NUMBER;
}
if (schemaType === 'boolean') {
return DEFAULT_MOCK_BOOLEAN;
}
if (schemaType === 'array') {
return [];
}
if (schemaType === 'object' || isRecord(schema.properties)) {
return buildMockStructuredOutput(schema);
}

return DEFAULT_MOCK_STRING;
}

/**
* Build environment for the SDK with only explicitly allowed variables
Expand All @@ -53,6 +114,35 @@ export class ClaudeProvider extends BaseProvider {
* Execute a query using Claude Agent SDK
*/
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
if (process.env.AUTOMAKER_MOCK_AGENT === 'true') {
let responseText = MOCK_RESPONSE_TEXT;
let structuredOutput: Record<string, unknown> | undefined;

if (options.outputFormat?.type === 'json_schema' && isRecord(options.outputFormat.schema)) {
structuredOutput = buildMockStructuredOutput(options.outputFormat.schema);
responseText = JSON.stringify(structuredOutput, null, MOCK_JSON_INDENT_SPACES);
}

yield {
type: 'assistant',
message: {
role: 'assistant',
content: [{ type: 'text', text: responseText }],
},
};

const resultMessage: ProviderMessage & { structured_output?: Record<string, unknown> } = {
type: 'result',
subtype: 'success',
result: responseText,
};
if (structuredOutput) {
resultMessage.structured_output = structuredOutput;
}
yield resultMessage;
return;
}

const {
prompt,
model,
Expand Down Expand Up @@ -104,6 +194,8 @@ export class ClaudeProvider extends BaseProvider {
...(options.mcpServers && { mcpServers: options.mcpServers }),
// Extended thinking configuration
...(maxThinkingTokens && { maxThinkingTokens }),
// Forward structured output configuration
...(options.outputFormat && { outputFormat: options.outputFormat }),
};

// Build prompt payload
Expand Down
85 changes: 85 additions & 0 deletions apps/server/src/providers/codex-config-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Codex Config Manager - Writes MCP server configuration for Codex CLI
*/

import path from 'path';
import type { McpServerConfig } from '@automaker/types';
import * as secureFs from '../lib/secure-fs.js';

const CODEX_CONFIG_DIR = '.codex';
const CODEX_CONFIG_FILENAME = 'config.toml';
const CODEX_MCP_SECTION = 'mcp_servers';

function formatTomlString(value: string): string {
return JSON.stringify(value);
}

function formatTomlArray(values: string[]): string {
const formatted = values.map((value) => formatTomlString(value)).join(', ');
return `[${formatted}]`;
}

function formatTomlInlineTable(values: Record<string, string>): string {
const entries = Object.entries(values).map(
([key, value]) => `${key} = ${formatTomlString(value)}`
);
return `{ ${entries.join(', ')} }`;
}

function formatTomlKey(key: string): string {
return `"${key.replace(/"/g, '\\"')}"`;
}

function buildServerBlock(name: string, server: McpServerConfig): string[] {
const lines: string[] = [];
const section = `${CODEX_MCP_SECTION}.${formatTomlKey(name)}`;
lines.push(`[${section}]`);

if (server.type) {
lines.push(`type = ${formatTomlString(server.type)}`);
}

if ('command' in server && server.command) {
lines.push(`command = ${formatTomlString(server.command)}`);
}

if ('args' in server && server.args && server.args.length > 0) {
lines.push(`args = ${formatTomlArray(server.args)}`);
}

if ('env' in server && server.env && Object.keys(server.env).length > 0) {
lines.push(`env = ${formatTomlInlineTable(server.env)}`);
}

if ('url' in server && server.url) {
lines.push(`url = ${formatTomlString(server.url)}`);
}

if ('headers' in server && server.headers && Object.keys(server.headers).length > 0) {
lines.push(`headers = ${formatTomlInlineTable(server.headers)}`);
}

return lines;
}

export class CodexConfigManager {
async configureMcpServers(
cwd: string,
mcpServers: Record<string, McpServerConfig>
): Promise<void> {
const configDir = path.join(cwd, CODEX_CONFIG_DIR);
const configPath = path.join(configDir, CODEX_CONFIG_FILENAME);

await secureFs.mkdir(configDir, { recursive: true });

const blocks: string[] = [];
for (const [name, server] of Object.entries(mcpServers)) {
blocks.push(...buildServerBlock(name, server), '');
}

const content = blocks.join('\n').trim();
if (content) {
await secureFs.writeFile(configPath, content + '\n', 'utf-8');
}
}
}
Loading
Loading