Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,5 @@ blob-report/
# Misc
*.pem

docker-compose.override.yml
docker-compose.override.yml
.claude/
15 changes: 8 additions & 7 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,11 @@ app.use(express.json({ limit: '50mb' }));
const events: EventEmitter = createEventEmitter();

// Create services
const agentService = new AgentService(DATA_DIR, events);
const featureLoader = new FeatureLoader();
const autoModeService = new AutoModeService(events);
// Note: settingsService is created first so it can be injected into other services
const settingsService = new SettingsService(DATA_DIR);
const agentService = new AgentService(DATA_DIR, events, settingsService);
const featureLoader = new FeatureLoader();
const autoModeService = new AutoModeService(events, settingsService);
const claudeUsageService = new ClaudeUsageService();

// Initialize services
Expand Down Expand Up @@ -148,17 +149,17 @@ app.use('/api/enhance-prompt', createEnhancePromptRoutes());
app.use('/api/worktree', createWorktreeRoutes());
app.use('/api/git', createGitRoutes());
app.use('/api/setup', createSetupRoutes());
app.use('/api/suggestions', createSuggestionsRoutes(events));
app.use('/api/suggestions', createSuggestionsRoutes(events, settingsService));
app.use('/api/models', createModelsRoutes());
app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events));
app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events, settingsService));
app.use('/api/running-agents', createRunningAgentsRoutes(autoModeService));
app.use('/api/workspace', createWorkspaceRoutes());
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/github', createGitHubRoutes(events));
app.use('/api/context', createContextRoutes());
app.use('/api/github', createGitHubRoutes(events, settingsService));
app.use('/api/context', createContextRoutes(settingsService));

// Create HTTP server
const server = createServer(app);
Expand Down
92 changes: 86 additions & 6 deletions apps/server/src/lib/sdk-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,59 @@ function getBaseOptions(): Partial<Options> {
};
}

/**
* Build system prompt configuration based on autoLoadClaudeMd setting.
* When autoLoadClaudeMd is true:
* - Uses preset mode with 'claude_code' to enable CLAUDE.md auto-loading
* - If there's a custom systemPrompt, appends it to the preset
* - Sets settingSources to ['project'] for SDK to load CLAUDE.md files
*
* @param config - The SDK options config
* @returns Object with systemPrompt and settingSources for SDK options
*/
function buildClaudeMdOptions(config: CreateSdkOptionsConfig): {
systemPrompt?: string | SystemPromptConfig;
settingSources?: Array<'user' | 'project' | 'local'>;
} {
if (!config.autoLoadClaudeMd) {
// Standard mode - just pass through the system prompt as-is
return config.systemPrompt ? { systemPrompt: config.systemPrompt } : {};
}

// Auto-load CLAUDE.md mode - use preset with settingSources
const result: {
systemPrompt: SystemPromptConfig;
settingSources: Array<'user' | 'project' | 'local'>;
} = {
systemPrompt: {
type: 'preset',
preset: 'claude_code',
},
// Load both user (~/.claude/CLAUDE.md) and project (.claude/CLAUDE.md) settings
settingSources: ['user', 'project'],
};

// If there's a custom system prompt, append it to the preset
if (config.systemPrompt) {
result.systemPrompt.append = config.systemPrompt;
}

return result;
}

/**
* System prompt configuration for SDK options
* When using preset mode with claude_code, CLAUDE.md files are automatically loaded
*/
export interface SystemPromptConfig {
/** Use preset mode with claude_code to enable CLAUDE.md auto-loading */
type: 'preset';
/** The preset to use - 'claude_code' enables CLAUDE.md loading */
preset: 'claude_code';
/** Optional additional prompt to append to the preset */
append?: string;
}

/**
* Options configuration for creating SDK options
*/
Expand All @@ -160,6 +213,9 @@ export interface CreateSdkOptionsConfig {
type: 'json_schema';
schema: Record<string, unknown>;
};

/** Enable auto-loading of CLAUDE.md files via SDK's settingSources */
autoLoadClaudeMd?: boolean;
}

/**
Expand All @@ -169,11 +225,15 @@ export interface CreateSdkOptionsConfig {
* - Uses read-only tools for codebase analysis
* - Extended turns for thorough exploration
* - Opus model by default (can be overridden)
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Options {
// Validate working directory before creating options
validateWorkingDirectory(config.cwd);

// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);

return {
...getBaseOptions(),
// Override permissionMode - spec generation only needs read-only tools
Expand All @@ -184,7 +244,7 @@ export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Opt
maxTurns: MAX_TURNS.maximum,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.specGeneration],
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
...(config.outputFormat && { outputFormat: config.outputFormat }),
};
Expand All @@ -197,11 +257,15 @@ export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Opt
* - Uses read-only tools (just needs to read the spec)
* - Quick turns since it's mostly JSON generation
* - Sonnet model by default for speed
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig): Options {
// Validate working directory before creating options
validateWorkingDirectory(config.cwd);

// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);

return {
...getBaseOptions(),
// Override permissionMode - feature generation only needs read-only tools
Expand All @@ -210,7 +274,7 @@ export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig):
maxTurns: MAX_TURNS.quick,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.readOnly],
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};
}
Expand All @@ -222,18 +286,22 @@ export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig):
* - Uses read-only tools for analysis
* - Standard turns to allow thorough codebase exploration and structured output generation
* - Opus model by default for thorough analysis
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Options {
// Validate working directory before creating options
validateWorkingDirectory(config.cwd);

// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);

return {
...getBaseOptions(),
model: getModelForUseCase('suggestions', config.model),
maxTurns: MAX_TURNS.extended,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.readOnly],
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
...(config.outputFormat && { outputFormat: config.outputFormat }),
};
Expand All @@ -247,6 +315,7 @@ export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Option
* - Standard turns for interactive sessions
* - Model priority: explicit model > session model > chat default
* - Sandbox enabled for bash safety
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createChatOptions(config: CreateSdkOptionsConfig): Options {
// Validate working directory before creating options
Expand All @@ -255,6 +324,9 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
// Model priority: explicit model > session model > chat default
const effectiveModel = config.model || config.sessionModel;

// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);

return {
...getBaseOptions(),
model: getModelForUseCase('chat', effectiveModel),
Expand All @@ -265,7 +337,7 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
enabled: true,
autoAllowBashIfSandboxed: true,
},
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};
}
Expand All @@ -278,11 +350,15 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
* - Extended turns for thorough feature implementation
* - Uses default model (can be overridden)
* - Sandbox enabled for bash safety
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
// Validate working directory before creating options
validateWorkingDirectory(config.cwd);

// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);

return {
...getBaseOptions(),
model: getModelForUseCase('auto', config.model),
Expand All @@ -293,7 +369,7 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
enabled: true,
autoAllowBashIfSandboxed: true,
},
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};
}
Expand All @@ -302,6 +378,7 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
* Create custom SDK options with explicit configuration
*
* Use this when the preset options don't fit your use case.
* When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createCustomOptions(
config: CreateSdkOptionsConfig & {
Expand All @@ -313,14 +390,17 @@ export function createCustomOptions(
// Validate working directory before creating options
validateWorkingDirectory(config.cwd);

// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);

return {
...getBaseOptions(),
model: getModelForUseCase('default', config.model),
maxTurns: config.maxTurns ?? MAX_TURNS.maximum,
cwd: config.cwd,
allowedTools: config.allowedTools ? [...config.allowedTools] : [...TOOL_PRESETS.readOnly],
...(config.sandbox && { sandbox: config.sandbox }),
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};
}
110 changes: 110 additions & 0 deletions apps/server/src/lib/settings-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Helper utilities for loading settings and context file handling across different parts of the server
*/

import type { SettingsService } from '../services/settings-service.js';
import type { ContextFilesResult, ContextFileInfo } from '@automaker/utils';

/**
* Get the autoLoadClaudeMd setting, with project settings taking precedence over global.
* Returns false if settings service is not available.
*
* @param projectPath - Path to the project
* @param settingsService - Optional settings service instance
* @param logPrefix - Prefix for log messages (e.g., '[DescribeImage]')
* @returns Promise resolving to the autoLoadClaudeMd setting value
*/
export async function getAutoLoadClaudeMdSetting(
projectPath: string,
settingsService?: SettingsService | null,
logPrefix = '[SettingsHelper]'
): Promise<boolean> {
if (!settingsService) {
console.log(`${logPrefix} SettingsService not available, autoLoadClaudeMd disabled`);
return false;
}

try {
// Check project settings first (takes precedence)
const projectSettings = await settingsService.getProjectSettings(projectPath);
if (projectSettings.autoLoadClaudeMd !== undefined) {
console.log(
`${logPrefix} autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}`
);
return projectSettings.autoLoadClaudeMd;
}

// Fall back to global settings
const globalSettings = await settingsService.getGlobalSettings();
const result = globalSettings.autoLoadClaudeMd ?? false;
console.log(`${logPrefix} autoLoadClaudeMd from global settings: ${result}`);
return result;
} catch (error) {
console.error(`${logPrefix} Failed to load autoLoadClaudeMd setting:`, error);
throw error;
}
}

/**
* Filters out CLAUDE.md from context files when autoLoadClaudeMd is enabled
* and rebuilds the formatted prompt without it.
*
* When autoLoadClaudeMd is true, the SDK handles CLAUDE.md loading via settingSources,
* so we need to exclude it from the manual context loading to avoid duplication.
* Other context files (CODE_QUALITY.md, CONVENTIONS.md, etc.) are preserved.
*
* @param contextResult - Result from loadContextFiles
* @param autoLoadClaudeMd - Whether SDK auto-loading is enabled
* @returns Filtered context prompt (empty string if no non-CLAUDE.md files)
*/
export function filterClaudeMdFromContext(
contextResult: ContextFilesResult,
autoLoadClaudeMd: boolean
): string {
// If autoLoadClaudeMd is disabled, return the original prompt unchanged
if (!autoLoadClaudeMd || contextResult.files.length === 0) {
return contextResult.formattedPrompt;
}

// Filter out CLAUDE.md (case-insensitive)
const nonClaudeFiles = contextResult.files.filter((f) => f.name.toLowerCase() !== 'claude.md');

// If all files were CLAUDE.md, return empty string
if (nonClaudeFiles.length === 0) {
return '';
}

// Rebuild prompt without CLAUDE.md using the same format as loadContextFiles
const formattedFiles = nonClaudeFiles.map((file) => formatContextFileEntry(file));

return `# Project Context Files

The following context files provide project-specific rules, conventions, and guidelines.
Each file serves a specific purpose - use the description to understand when to reference it.
If you need more details about a context file, you can read the full file at the path provided.

**IMPORTANT**: You MUST follow the rules and conventions specified in these files.
- Follow ALL commands exactly as shown (e.g., if the project uses \`pnpm\`, NEVER use \`npm\` or \`npx\`)
- Follow ALL coding conventions, commit message formats, and architectural patterns specified
- Reference these rules before running ANY shell commands or making commits

---

${formattedFiles.join('\n\n---\n\n')}

---

**REMINDER**: Before taking any action, verify you are following the conventions specified above.
`;
}

/**
* Format a single context file entry for the prompt
* (Matches the format used in @automaker/utils/context-loader.ts)
*/
function formatContextFileEntry(file: ContextFileInfo): string {
const header = `## ${file.name}`;
const pathInfo = `**Path:** \`${file.path}\``;
const descriptionInfo = file.description ? `\n**Purpose:** ${file.description}` : '';
return `${header}\n${pathInfo}${descriptionInfo}\n\n${file.content}`;
}
2 changes: 2 additions & 0 deletions apps/server/src/providers/claude-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export class ClaudeProvider extends BaseProvider {
...(sdkSessionId && conversationHistory && conversationHistory.length > 0
? { resume: sdkSessionId }
: {}),
// Forward settingSources for CLAUDE.md file loading
...(options.settingSources && { settingSources: options.settingSources }),
};

// Build prompt payload
Expand Down
Loading