feat: add auto-load CLAUDE.md functionality

- Introduced a new setting to enable automatic loading of CLAUDE.md files from project-specific directories.
- Updated relevant services and components to support the new setting, including the AgentService and AutoModeService.
- Added UI controls for managing the auto-load setting in the settings view.
- Enhanced SDK options to incorporate settingSources for CLAUDE.md loading.
- Updated global and project settings interfaces to include autoLoadClaudeMd property.
This commit is contained in:
Kacper
2025-12-24 22:05:50 +01:00
parent 8a0226512d
commit 07bcb6b767
12 changed files with 306 additions and 15 deletions

View File

@@ -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

View File

@@ -136,6 +136,71 @@ 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;
}
console.log(
'[SDK Options] CLAUDE.md auto-loading enabled:',
JSON.stringify(
{
systemPrompt: result.systemPrompt,
settingSources: result.settingSources,
},
null,
2
)
);
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
*/
@@ -160,6 +225,9 @@ export interface CreateSdkOptionsConfig {
type: 'json_schema';
schema: Record<string, unknown>;
};
/** Enable auto-loading of CLAUDE.md files via SDK's settingSources */
autoLoadClaudeMd?: boolean;
}
/**
@@ -169,11 +237,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
@@ -184,7 +256,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 }),
};
@@ -197,11 +269,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
@@ -210,7 +286,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 }),
};
}
@@ -222,18 +298,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 }),
};
@@ -247,6 +327,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
@@ -255,6 +336,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),
@@ -265,7 +349,7 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
enabled: true,
autoAllowBashIfSandboxed: true,
},
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};
}
@@ -278,11 +362,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),
@@ -293,7 +381,7 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
enabled: true,
autoAllowBashIfSandboxed: true,
},
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};
}
@@ -302,6 +390,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 & {
@@ -313,6 +402,9 @@ 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),
@@ -320,7 +412,7 @@ export function createCustomOptions(
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 }),
};
}

View File

@@ -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

View File

@@ -26,13 +26,14 @@ export interface ExecuteOptions {
prompt: string | Array<{ type: string; text?: string; source?: object }>;
model: string;
cwd: string;
systemPrompt?: string;
systemPrompt?: string | { type: 'preset'; preset: 'claude_code'; append?: string };
maxTurns?: number;
allowedTools?: string[];
mcpServers?: Record<string, unknown>;
abortController?: AbortController;
conversationHistory?: ConversationMessage[]; // Previous messages for context
sdkSessionId?: string; // Claude SDK session ID for resuming conversations
settingSources?: Array<'user' | 'project' | 'local'>; // Claude filesystem settings to load
}
/**

View File

@@ -16,6 +16,7 @@ import {
import { ProviderFactory } from '../providers/provider-factory.js';
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
import { PathNotAllowedError } from '@automaker/platform';
import type { SettingsService } from './settings-service.js';
interface Message {
id: string;
@@ -57,11 +58,13 @@ export class AgentService {
private stateDir: string;
private metadataFile: string;
private events: EventEmitter;
private settingsService: SettingsService | null = null;
constructor(dataDir: string, events: EventEmitter) {
constructor(dataDir: string, events: EventEmitter, settingsService?: SettingsService) {
this.stateDir = path.join(dataDir, 'agent-sessions');
this.metadataFile = path.join(dataDir, 'sessions-metadata.json');
this.events = events;
this.settingsService = settingsService ?? null;
}
async initialize(): Promise<void> {
@@ -186,7 +189,11 @@ export class AgentService {
// Determine the effective working directory for context loading
const effectiveWorkDir = workingDirectory || session.workingDirectory;
// Load autoLoadClaudeMd setting (project setting takes precedence over global)
const autoLoadClaudeMd = await this.getAutoLoadClaudeMdSetting(effectiveWorkDir);
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.)
// Note: When autoLoadClaudeMd is enabled, SDK handles CLAUDE.md loading via settingSources
const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({
projectPath: effectiveWorkDir,
fsModule: secureFs as Parameters<typeof loadContextFiles>[0]['fsModule'],
@@ -205,6 +212,7 @@ export class AgentService {
sessionModel: session.model,
systemPrompt: combinedSystemPrompt,
abortController: session.abortController!,
autoLoadClaudeMd,
});
// Extract model, maxTurns, and allowedTools from SDK options
@@ -224,11 +232,12 @@ export class AgentService {
prompt: '', // Will be set below based on images
model: effectiveModel,
cwd: effectiveWorkDir,
systemPrompt: combinedSystemPrompt,
systemPrompt: sdkOptions.systemPrompt,
maxTurns: maxTurns,
allowedTools: allowedTools,
abortController: session.abortController!,
conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined,
settingSources: sdkOptions.settingSources,
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
};
@@ -595,6 +604,37 @@ You have full access to the codebase and can:
- Execute tests and builds`;
}
/**
* Get the autoLoadClaudeMd setting, with project settings taking precedence over global.
* Returns false if settings service is not available.
*/
private async getAutoLoadClaudeMdSetting(projectPath: string): Promise<boolean> {
if (!this.settingsService) {
console.log('[AgentService] SettingsService not available, autoLoadClaudeMd disabled');
return false;
}
try {
// Check project settings first (takes precedence)
const projectSettings = await this.settingsService.getProjectSettings(projectPath);
if (projectSettings.autoLoadClaudeMd !== undefined) {
console.log(
`[AgentService] autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}`
);
return projectSettings.autoLoadClaudeMd;
}
// Fall back to global settings
const globalSettings = await this.settingsService.getGlobalSettings();
const result = globalSettings.autoLoadClaudeMd ?? false;
console.log(`[AgentService] autoLoadClaudeMd from global settings: ${result}`);
return result;
} catch (error) {
console.error('[AgentService] Failed to load autoLoadClaudeMd setting:', error);
return false;
}
}
private generateId(): string {
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
}

View File

@@ -27,6 +27,7 @@ import * as secureFs from '../lib/secure-fs.js';
import type { EventEmitter } from '../lib/events.js';
import { createAutoModeOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
import { FeatureLoader } from './feature-loader.js';
import type { SettingsService } from './settings-service.js';
const execAsync = promisify(exec);
@@ -341,9 +342,11 @@ export class AutoModeService {
private autoLoopAbortController: AbortController | null = null;
private config: AutoModeConfig | null = null;
private pendingApprovals = new Map<string, PendingApproval>();
private settingsService: SettingsService | null = null;
constructor(events: EventEmitter) {
constructor(events: EventEmitter, settingsService?: SettingsService) {
this.events = events;
this.settingsService = settingsService ?? null;
}
/**
@@ -1780,11 +1783,15 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
return;
}
// Load autoLoadClaudeMd setting (project setting takes precedence over global)
const autoLoadClaudeMd = await this.getAutoLoadClaudeMdSetting(finalProjectPath);
// Build SDK options using centralized configuration for feature implementation
const sdkOptions = createAutoModeOptions({
cwd: workDir,
model: model,
abortController,
autoLoadClaudeMd,
});
// Extract model, maxTurns, and allowedTools from SDK options
@@ -1823,7 +1830,8 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
cwd: workDir,
allowedTools: allowedTools,
abortController,
systemPrompt: options?.systemPrompt,
systemPrompt: sdkOptions.systemPrompt,
settingSources: sdkOptions.settingSources,
};
// Execute via provider
@@ -2494,4 +2502,35 @@ Begin implementing task ${task.id} now.`;
}
});
}
/**
* Get the autoLoadClaudeMd setting, with project settings taking precedence over global.
* Returns false if settings service is not available.
*/
private async getAutoLoadClaudeMdSetting(projectPath: string): Promise<boolean> {
if (!this.settingsService) {
console.log('[AutoMode] SettingsService not available, autoLoadClaudeMd disabled');
return false;
}
try {
// Check project settings first (takes precedence)
const projectSettings = await this.settingsService.getProjectSettings(projectPath);
if (projectSettings.autoLoadClaudeMd !== undefined) {
console.log(
`[AutoMode] autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}`
);
return projectSettings.autoLoadClaudeMd;
}
// Fall back to global settings
const globalSettings = await this.settingsService.getGlobalSettings();
const result = globalSettings.autoLoadClaudeMd ?? false;
console.log(`[AutoMode] autoLoadClaudeMd from global settings: ${result}`);
return result;
} catch (error) {
console.error('[AutoMode] Failed to load autoLoadClaudeMd setting:', error);
return false;
}
}
}