diff --git a/.gitignore b/.gitignore index c752c12e..5efd9e1f 100644 --- a/.gitignore +++ b/.gitignore @@ -79,4 +79,5 @@ blob-report/ # Misc *.pem -docker-compose.override.yml \ No newline at end of file +docker-compose.override.yml +.claude/ \ No newline at end of file diff --git a/README.md b/README.md index 15309a6e..f9cabfd0 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ - [What Makes Automaker Different?](#what-makes-automaker-different) - [The Workflow](#the-workflow) - - [Powered by Claude Code](#powered-by-claude-code) + - [Powered by Claude Agent SDK](#powered-by-claude-agent-sdk) - [Why This Matters](#why-this-matters) - [Security Disclaimer](#security-disclaimer) - [Community & Support](#community--support) @@ -28,22 +28,36 @@ - [Quick Start](#quick-start) - [How to Run](#how-to-run) - [Development Mode](#development-mode) - - [Electron Desktop App (Recommended)](#electron-desktop-app-recommended) - - [Web Browser Mode](#web-browser-mode) - [Building for Production](#building-for-production) - - [Running Production Build](#running-production-build) - [Testing](#testing) - [Linting](#linting) - - [Authentication Options](#authentication-options) - - [Persistent Setup (Optional)](#persistent-setup-optional) + - [Environment Configuration](#environment-configuration) + - [Authentication Setup](#authentication-setup) - [Features](#features) + - [Core Workflow](#core-workflow) + - [AI & Planning](#ai--planning) + - [Project Management](#project-management) + - [Collaboration & Review](#collaboration--review) + - [Developer Tools](#developer-tools) + - [Advanced Features](#advanced-features) - [Tech Stack](#tech-stack) + - [Frontend](#frontend) + - [Backend](#backend) + - [Testing & Quality](#testing--quality) + - [Shared Libraries](#shared-libraries) +- [Available Views](#available-views) +- [Architecture](#architecture) + - [Monorepo Structure](#monorepo-structure) + - [How It Works](#how-it-works) + - [Key Architectural Patterns](#key-architectural-patterns) + - [Security & Isolation](#security--isolation) + - [Data Storage](#data-storage) - [Learn More](#learn-more) - [License](#license) -Automaker is an autonomous AI development studio that transforms how you build software. Instead of manually writing every line of code, you describe features on a Kanban board and watch as AI agents powered by Claude Code automatically implement them. +Automaker is an autonomous AI development studio that transforms how you build software. Instead of manually writing every line of code, you describe features on a Kanban board and watch as AI agents powered by Claude Agent SDK automatically implement them. Built with React, Vite, Electron, and Express, Automaker provides a complete workflow for managing AI agents through a desktop application (or web browser), with features like real-time streaming, git worktree isolation, plan approval, and multi-agent task execution. ![Automaker UI](https://i.imgur.com/jdwKydM.png) @@ -59,9 +73,9 @@ Traditional development tools help you write code. Automaker helps you **orchest 4. **Review & Verify** - Review the changes, run tests, and approve when ready 5. **Ship Faster** - Build entire applications in days, not weeks -### Powered by Claude Code +### Powered by Claude Agent SDK -Automaker leverages the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) to give AI agents full access to your codebase. Agents can read files, write code, execute commands, run tests, and make git commitsβ€”all while working in isolated git worktrees to keep your main branch safe. +Automaker leverages the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) to give AI agents full access to your codebase. Agents can read files, write code, execute commands, run tests, and make git commitsβ€”all while working in isolated git worktrees to keep your main branch safe. The SDK provides autonomous AI agents that can use tools, make decisions, and complete complex multi-step tasks without constant human intervention. ### Why This Matters @@ -95,8 +109,7 @@ In the Discord, you can: - πŸš€ Show off projects built with AI agents - 🀝 Collaborate with other developers and contributors -πŸ‘‰ **Join the Discord:** -https://discord.gg/jjem7aEDKU +πŸ‘‰ **Join the Discord:** [Agentic Jumpstart Discord](https://discord.gg/jjem7aEDKU) --- @@ -104,28 +117,49 @@ https://discord.gg/jjem7aEDKU ### Prerequisites -- Node.js 18+ -- npm -- [Claude Code CLI](https://code.claude.com/docs/en/overview) installed and authenticated +- **Node.js 18+** (tested with Node.js 22) +- **npm** (comes with Node.js) +- **Authentication** (choose one): + - **[Claude Code CLI](https://code.claude.com/docs/en/overview)** (recommended) - Install and authenticate, credentials used automatically + - **Anthropic API Key** - Direct API key for Claude Agent SDK ([get one here](https://console.anthropic.com/)) ### Quick Start ```bash -# 1. Clone the repo +# 1. Clone the repository git clone https://github.com/AutoMaker-Org/automaker.git cd automaker # 2. Install dependencies npm install -# 3. Build local shared packages +# 3. Build shared packages (Now can be skipped npm install / run dev does it automaticly) npm run build:packages -# 4. Run Automaker (pick your mode) +# 4. Set up authentication (skip if using Claude Code CLI) +# If using Claude Code CLI: credentials are detected automatically +# If using API key directly, choose one method: + +# Option A: Environment variable +export ANTHROPIC_API_KEY="sk-ant-..." + +# Option B: Create .env file in project root +echo "ANTHROPIC_API_KEY=sk-ant-..." > .env + +# 5. Start Automaker (interactive launcher) npm run dev -# Then choose your run mode when prompted, or use specific commands below +# Choose between: +# 1. Web Application (browser at localhost:3007) +# 2. Desktop Application (Electron - recommended) ``` +**Note:** The `npm run dev` command will: + +- Check for dependencies and install if needed +- Install Playwright browsers for E2E tests +- Kill any processes on ports 3007/3008 +- Present an interactive menu to choose your run mode + ## How to Run ### Development Mode @@ -163,31 +197,65 @@ npm run dev:web ### Building for Production +#### Web Application + ```bash -# Build Next.js app +# Build for web deployment (uses Vite) npm run build -# Build Electron app for distribution -npm run build:electron +# Run production build +npm run start ``` -### Running Production Build +#### Desktop Application ```bash -# Start production Next.js server -npm run start +# Build for current platform (macOS/Windows/Linux) +npm run build:electron + +# Platform-specific builds +npm run build:electron:mac # macOS (DMG + ZIP, x64 + arm64) +npm run build:electron:win # Windows (NSIS installer, x64) +npm run build:electron:linux # Linux (AppImage + DEB, x64) + +# Output directory: apps/ui/release/ +``` + +#### Docker Deployment + +```bash +# Build and run with Docker Compose (recommended for security) +docker-compose up -d + +# Access at http://localhost:3007 +# API at http://localhost:3008 ``` ### Testing -```bash -# Run tests headless -npm run test +#### End-to-End Tests (Playwright) -# Run tests with browser visible -npm run test:headed +```bash +npm run test # Headless E2E tests +npm run test:headed # Browser visible E2E tests ``` +#### Unit Tests (Vitest) + +```bash +npm run test:server # Server unit tests +npm run test:server:coverage # Server tests with coverage +npm run test:packages # All shared package tests +npm run test:all # Packages + server tests +``` + +#### Test Configuration + +- E2E tests run on ports 3007 (UI) and 3008 (server) +- Automatically starts test servers before running +- Uses Chromium browser via Playwright +- Mock agent mode available in CI with `AUTOMAKER_MOCK_AGENT=true` + ### Linting ```bash @@ -195,59 +263,283 @@ npm run test:headed npm run lint ``` -### Authentication Options +### Environment Configuration -Automaker supports multiple authentication methods (in order of priority): +#### Authentication (if not using Claude Code CLI) -| Method | Environment Variable | Description | -| ---------------- | -------------------- | ------------------------------- | -| API Key (env) | `ANTHROPIC_API_KEY` | Anthropic API key | -| API Key (stored) | β€” | Anthropic API key stored in app | +- `ANTHROPIC_API_KEY` - Your Anthropic API key for Claude Agent SDK (not needed if using Claude Code CLI) -### Persistent Setup (Optional) +#### Optional - Server + +- `PORT` - Server port (default: 3008) +- `DATA_DIR` - Data storage directory (default: ./data) +- `ENABLE_REQUEST_LOGGING` - HTTP request logging (default: true) + +#### Optional - Security + +- `AUTOMAKER_API_KEY` - Optional API authentication for the server +- `ALLOWED_ROOT_DIRECTORY` - Restrict file operations to specific directory +- `CORS_ORIGIN` - CORS policy (default: \*) + +#### Optional - Development + +- `VITE_SKIP_ELECTRON` - Skip Electron in dev mode +- `OPEN_DEVTOOLS` - Auto-open DevTools in Electron + +### Authentication Setup + +#### Option 1: Claude Code CLI (Recommended) + +Install and authenticate the Claude Code CLI following the [official quickstart guide](https://code.claude.com/docs/en/quickstart). + +Once authenticated, Automaker will automatically detect and use your CLI credentials. No additional configuration needed! + +#### Option 2: Direct API Key + +If you prefer not to use the CLI, you can provide an Anthropic API key directly using one of these methods: + +##### 2a. Shell Configuration Add to your `~/.bashrc` or `~/.zshrc`: ```bash -export ANTHROPIC_API_KEY="YOUR_API_KEY_HERE" +export ANTHROPIC_API_KEY="sk-ant-..." ``` -Then restart your terminal or run `source ~/.bashrc`. +Then restart your terminal or run `source ~/.bashrc` (or `source ~/.zshrc`). + +##### 2b. .env File + +Create a `.env` file in the project root (gitignored): + +```bash +ANTHROPIC_API_KEY=sk-ant-... +PORT=3008 +DATA_DIR=./data +``` + +##### 2c. In-App Storage + +The application can store your API key securely in the settings UI. The key is persisted in the `DATA_DIR` directory. ## Features +### Core Workflow + - πŸ“‹ **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages - πŸ€– **AI Agent Integration** - Automatic AI agent assignment to implement features when moved to "In Progress" -- 🧠 **Multi-Model Support** - Choose from multiple AI models including Claude Opus, Sonnet, and more -- πŸ’­ **Extended Thinking** - Enable extended thinking modes for complex problem-solving -- πŸ“‘ **Real-time Agent Output** - View live agent output, logs, and file diffs as features are being implemented -- πŸ” **Project Analysis** - AI-powered project structure analysis to understand your codebase -- πŸ“ **Context Management** - Add context files to help AI agents understand your project better -- πŸ’‘ **Feature Suggestions** - AI-generated feature suggestions based on your project -- πŸ–ΌοΈ **Image Support** - Attach images and screenshots to feature descriptions -- ⚑ **Concurrent Processing** - Configure concurrency to process multiple features simultaneously -- πŸ§ͺ **Test Integration** - Automatic test running and verification for implemented features -- πŸ”€ **Git Integration** - View git diffs and track changes made by AI agents -- πŸ‘€ **AI Profiles** - Create and manage different AI agent profiles for various tasks -- πŸ’¬ **Chat History** - Keep track of conversations and interactions with AI agents -- ⌨️ **Keyboard Shortcuts** - Efficient navigation and actions via keyboard shortcuts -- 🎨 **Dark/Light Theme** - Beautiful UI with theme support -- πŸ–₯️ **Cross-Platform** - Desktop application built with Electron for Windows, macOS, and Linux +- πŸ”€ **Git Worktree Isolation** - Each feature executes in isolated git worktrees to protect your main branch +- πŸ“‘ **Real-time Streaming** - Watch AI agents work in real-time with live tool usage, progress updates, and task completion +- πŸ”„ **Follow-up Instructions** - Send additional instructions to running agents without stopping them + +### AI & Planning + +- 🧠 **Multi-Model Support** - Choose from Claude Opus, Sonnet, and Haiku per feature +- πŸ’­ **Extended Thinking** - Enable thinking modes (none, medium, deep, ultra) for complex problem-solving +- πŸ“ **Planning Modes** - Four planning levels: skip (direct implementation), lite (quick plan), spec (task breakdown), full (phased execution) +- βœ… **Plan Approval** - Review and approve AI-generated plans before implementation begins +- πŸ“Š **Multi-Agent Task Execution** - Spec mode spawns dedicated agents per task for focused implementation + +### Project Management + +- πŸ” **Project Analysis** - AI-powered codebase analysis to understand your project structure +- πŸ’‘ **Feature Suggestions** - AI-generated feature suggestions based on project analysis +- πŸ“ **Context Management** - Add markdown, images, and documentation files that agents automatically reference +- πŸ”— **Dependency Blocking** - Features can depend on other features, enforcing execution order +- 🌳 **Graph View** - Visualize feature dependencies with interactive graph visualization +- πŸ“‹ **GitHub Integration** - Import issues, validate feasibility, and convert to tasks automatically + +### Collaboration & Review + +- πŸ§ͺ **Verification Workflow** - Features move to "Waiting Approval" for review and testing +- πŸ’¬ **Agent Chat** - Interactive chat sessions with AI agents for exploratory work +- πŸ‘€ **AI Profiles** - Create custom agent configurations with different prompts, models, and settings +- πŸ“œ **Session History** - Persistent chat sessions across restarts with full conversation history +- πŸ” **Git Diff Viewer** - Review changes made by agents before approving + +### Developer Tools + +- πŸ–₯️ **Integrated Terminal** - Full terminal access with tabs, splits, and persistent sessions +- πŸ–ΌοΈ **Image Support** - Attach screenshots and diagrams to feature descriptions for visual context +- ⚑ **Concurrent Execution** - Configure how many features can run simultaneously (default: 3) +- ⌨️ **Keyboard Shortcuts** - Fully customizable shortcuts for navigation and actions +- 🎨 **Theme System** - 25+ themes including Dark, Light, Dracula, Nord, Catppuccin, and more +- πŸ–₯️ **Cross-Platform** - Desktop app for macOS (x64, arm64), Windows (x64), and Linux (x64) +- 🌐 **Web Mode** - Run in browser or as Electron desktop app + +### Advanced Features + +- πŸ” **Docker Isolation** - Security-focused Docker deployment with no host filesystem access +- 🎯 **Worktree Management** - Create, switch, commit, and create PRs from worktrees +- πŸ“Š **Usage Tracking** - Monitor Claude API usage with detailed metrics +- πŸ”Š **Audio Notifications** - Optional completion sounds (mutable in settings) +- πŸ’Ύ **Auto-save** - All work automatically persisted to `.automaker/` directory ## Tech Stack -- [Next.js](https://nextjs.org) - React framework -- [Electron](https://www.electronjs.org/) - Desktop application framework -- [Tailwind CSS](https://tailwindcss.com/) - Styling -- [Zustand](https://zustand-demo.pmnd.rs/) - State management -- [dnd-kit](https://dndkit.com/) - Drag and drop functionality +### Frontend + +- **React 19** - UI framework +- **Vite 7** - Build tool and development server +- **Electron 39** - Desktop application framework +- **TypeScript 5.9** - Type safety +- **TanStack Router** - File-based routing +- **Zustand 5** - State management with persistence +- **Tailwind CSS 4** - Utility-first styling with 25+ themes +- **Radix UI** - Accessible component primitives +- **dnd-kit** - Drag and drop for Kanban board +- **@xyflow/react** - Graph visualization for dependencies +- **xterm.js** - Integrated terminal emulator +- **CodeMirror 6** - Code editor for XML/syntax highlighting +- **Lucide Icons** - Icon library + +### Backend + +- **Node.js** - JavaScript runtime with ES modules +- **Express 5** - HTTP server framework +- **TypeScript 5.9** - Type safety +- **Claude Agent SDK** - AI agent integration (@anthropic-ai/claude-agent-sdk) +- **WebSocket (ws)** - Real-time event streaming +- **node-pty** - PTY terminal sessions + +### Testing & Quality + +- **Playwright** - End-to-end testing +- **Vitest** - Unit testing framework +- **ESLint 9** - Code linting +- **Prettier 3** - Code formatting +- **Husky** - Git hooks for pre-commit formatting + +### Shared Libraries + +- **@automaker/types** - Shared TypeScript definitions +- **@automaker/utils** - Logging, error handling, image processing +- **@automaker/prompts** - AI prompt templates +- **@automaker/platform** - Path management and security +- **@automaker/model-resolver** - Claude model alias resolution +- **@automaker/dependency-resolver** - Feature dependency ordering +- **@automaker/git-utils** - Git operations and worktree management + +## Available Views + +Automaker provides several specialized views accessible via the sidebar or keyboard shortcuts: + +| View | Shortcut | Description | +| ------------------ | -------- | ------------------------------------------------------------------------------------------------ | +| **Board** | `K` | Kanban board for managing feature workflow (Backlog β†’ In Progress β†’ Waiting Approval β†’ Verified) | +| **Agent** | `A` | Interactive chat sessions with AI agents for exploratory work and questions | +| **Spec** | `D` | Project specification editor with AI-powered generation and feature suggestions | +| **Context** | `C` | Manage context files (markdown, images) that AI agents automatically reference | +| **Profiles** | `M` | Create and manage AI agent profiles with custom prompts and configurations | +| **Settings** | `S` | Configure themes, shortcuts, defaults, authentication, and more | +| **Terminal** | `T` | Integrated terminal with tabs, splits, and persistent sessions | +| **GitHub Issues** | - | Import and validate GitHub issues, convert to tasks | +| **Running Agents** | - | View all active agents across projects with status and progress | + +### Keyboard Navigation + +All shortcuts are customizable in Settings. Default shortcuts: + +- **Navigation:** `K` (Board), `A` (Agent), `D` (Spec), `C` (Context), `S` (Settings), `M` (Profiles), `T` (Terminal) +- **UI:** `` ` `` (Toggle sidebar) +- **Actions:** `N` (New item in current view), `G` (Start next features), `O` (Open project), `P` (Project picker) +- **Projects:** `Q`/`E` (Cycle previous/next project) + +## Architecture + +### Monorepo Structure + +Automaker is built as an npm workspace monorepo with two main applications and seven shared packages: + +```text +automaker/ +β”œβ”€β”€ apps/ +β”‚ β”œβ”€β”€ ui/ # React + Vite + Electron frontend +β”‚ └── server/ # Express + WebSocket backend +└── libs/ # Shared packages + β”œβ”€β”€ types/ # Core TypeScript definitions + β”œβ”€β”€ utils/ # Logging, errors, utilities + β”œβ”€β”€ prompts/ # AI prompt templates + β”œβ”€β”€ platform/ # Path management, security + β”œβ”€β”€ model-resolver/ # Claude model aliasing + β”œβ”€β”€ dependency-resolver/ # Feature dependency ordering + └── git-utils/ # Git operations & worktree management +``` + +### How It Works + +1. **Feature Definition** - Users create feature cards on the Kanban board with descriptions, images, and configuration +2. **Git Worktree Creation** - When a feature starts, a git worktree is created for isolated development +3. **Agent Execution** - Claude Agent SDK executes in the worktree with full file system and command access +4. **Real-time Streaming** - Agent output streams via WebSocket to the frontend for live monitoring +5. **Plan Approval** (optional) - For spec/full planning modes, agents generate plans that require user approval +6. **Multi-Agent Tasks** (spec mode) - Each task in the spec gets a dedicated agent for focused implementation +7. **Verification** - Features move to "Waiting Approval" where changes can be reviewed via git diff +8. **Integration** - After approval, changes can be committed and PRs created from the worktree + +### Key Architectural Patterns + +- **Event-Driven Architecture** - All server operations emit events that stream to the frontend +- **Provider Pattern** - Extensible AI provider system (currently Claude, designed for future providers) +- **Service-Oriented Backend** - Modular services for agent management, features, terminals, settings +- **State Management** - Zustand with persistence for frontend state across restarts +- **File-Based Storage** - No database; features stored as JSON files in `.automaker/` directory + +### Security & Isolation + +- **Git Worktrees** - Each feature executes in an isolated git worktree, protecting your main branch +- **Path Sandboxing** - Optional `ALLOWED_ROOT_DIRECTORY` restricts file access +- **Docker Isolation** - Recommended deployment uses Docker with no host filesystem access +- **Plan Approval** - Optional plan review before implementation prevents unwanted changes + +### Data Storage + +Automaker uses a file-based storage system (no database required): + +#### Per-Project Data + +Stored in `{projectPath}/.automaker/`: + +```text +.automaker/ +β”œβ”€β”€ features/ # Feature JSON files and images +β”‚ └── {featureId}/ +β”‚ β”œβ”€β”€ feature.json # Feature metadata +β”‚ β”œβ”€β”€ agent-output.md # AI agent output log +β”‚ └── images/ # Attached images +β”œβ”€β”€ context/ # Context files for AI agents +β”œβ”€β”€ settings.json # Project-specific settings +β”œβ”€β”€ spec.md # Project specification +β”œβ”€β”€ analysis.json # Project structure analysis +└── feature-suggestions.json # AI-generated suggestions +``` + +#### Global Data + +Stored in `DATA_DIR` (default `./data`): + +```text +data/ +β”œβ”€β”€ settings.json # Global settings, profiles, shortcuts +β”œβ”€β”€ credentials.json # API keys (encrypted) +β”œβ”€β”€ sessions-metadata.json # Chat session metadata +└── agent-sessions/ # Conversation histories + └── {sessionId}.json +``` ## Learn More -To learn more about Next.js, take a look at the following resources: +### Documentation -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +- [Project Documentation](./docs/) - Architecture guides, patterns, and developer docs +- [Docker Isolation Guide](./docs/docker-isolation.md) - Security-focused Docker deployment +- [Shared Packages Guide](./docs/llm-shared-packages.md) - Using monorepo packages + +### Community + +Join the **Agentic Jumpstart** Discord to connect with other builders exploring **agentic coding**: + +πŸ‘‰ [Agentic Jumpstart Discord](https://discord.gg/jjem7aEDKU) ## License diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 548f4629..bd0fd4a0 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -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 @@ -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); diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts index 7853fbd2..e7fc3578 100644 --- a/apps/server/src/lib/sdk-options.ts +++ b/apps/server/src/lib/sdk-options.ts @@ -136,6 +136,59 @@ function getBaseOptions(): Partial { }; } +/** + * 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 */ @@ -160,6 +213,9 @@ export interface CreateSdkOptionsConfig { type: 'json_schema'; schema: Record; }; + + /** Enable auto-loading of CLAUDE.md files via SDK's settingSources */ + autoLoadClaudeMd?: boolean; } /** @@ -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 @@ -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 }), }; @@ -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 @@ -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 }), }; } @@ -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 }), }; @@ -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 @@ -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), @@ -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 }), }; } @@ -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), @@ -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 }), }; } @@ -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 & { @@ -313,6 +390,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 +400,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 }), }; } diff --git a/apps/server/src/lib/settings-helpers.ts b/apps/server/src/lib/settings-helpers.ts new file mode 100644 index 00000000..9c4456ff --- /dev/null +++ b/apps/server/src/lib/settings-helpers.ts @@ -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 { + 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}`; +} diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts index 2ed2728d..9237cdf6 100644 --- a/apps/server/src/providers/claude-provider.ts +++ b/apps/server/src/providers/claude-provider.ts @@ -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 diff --git a/apps/server/src/providers/types.ts b/apps/server/src/providers/types.ts index f3aa22d5..5a594361 100644 --- a/apps/server/src/providers/types.ts +++ b/apps/server/src/providers/types.ts @@ -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; 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 } /** diff --git a/apps/server/src/routes/app-spec/generate-features-from-spec.ts b/apps/server/src/routes/app-spec/generate-features-from-spec.ts index 17a83078..e2b7124d 100644 --- a/apps/server/src/routes/app-spec/generate-features-from-spec.ts +++ b/apps/server/src/routes/app-spec/generate-features-from-spec.ts @@ -10,6 +10,8 @@ import { createFeatureGenerationOptions } from '../../lib/sdk-options.js'; import { logAuthStatus } from './common.js'; import { parseAndCreateFeatures } from './parse-and-create-features.js'; import { getAppSpecPath } from '@automaker/platform'; +import type { SettingsService } from '../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js'; const logger = createLogger('SpecRegeneration'); @@ -19,7 +21,8 @@ export async function generateFeaturesFromSpec( projectPath: string, events: EventEmitter, abortController: AbortController, - maxFeatures?: number + maxFeatures?: number, + settingsService?: SettingsService ): Promise { const featureCount = maxFeatures ?? DEFAULT_MAX_FEATURES; logger.debug('========== generateFeaturesFromSpec() started =========='); @@ -91,9 +94,17 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge projectPath: projectPath, }); + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + settingsService, + '[FeatureGeneration]' + ); + const options = createFeatureGenerationOptions({ cwd: projectPath, abortController, + autoLoadClaudeMd, }); logger.debug('SDK Options:', JSON.stringify(options, null, 2)); diff --git a/apps/server/src/routes/app-spec/generate-spec.ts b/apps/server/src/routes/app-spec/generate-spec.ts index 4b6a6426..0762bb90 100644 --- a/apps/server/src/routes/app-spec/generate-spec.ts +++ b/apps/server/src/routes/app-spec/generate-spec.ts @@ -17,6 +17,8 @@ import { createSpecGenerationOptions } from '../../lib/sdk-options.js'; import { logAuthStatus } from './common.js'; import { generateFeaturesFromSpec } from './generate-features-from-spec.js'; import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform'; +import type { SettingsService } from '../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js'; const logger = createLogger('SpecRegeneration'); @@ -27,7 +29,8 @@ export async function generateSpec( abortController: AbortController, generateFeatures?: boolean, analyzeProject?: boolean, - maxFeatures?: number + maxFeatures?: number, + settingsService?: SettingsService ): Promise { logger.info('========== generateSpec() started =========='); logger.info('projectPath:', projectPath); @@ -83,9 +86,17 @@ ${getStructuredSpecPromptInstruction()}`; content: 'Starting spec generation...\n', }); + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + settingsService, + '[SpecRegeneration]' + ); + const options = createSpecGenerationOptions({ cwd: projectPath, abortController, + autoLoadClaudeMd, outputFormat: { type: 'json_schema', schema: specOutputSchema, @@ -269,7 +280,13 @@ ${getStructuredSpecPromptInstruction()}`; // Create a new abort controller for feature generation const featureAbortController = new AbortController(); try { - await generateFeaturesFromSpec(projectPath, events, featureAbortController, maxFeatures); + await generateFeaturesFromSpec( + projectPath, + events, + featureAbortController, + maxFeatures, + settingsService + ); // Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures } catch (featureError) { logger.error('Feature generation failed:', featureError); diff --git a/apps/server/src/routes/app-spec/index.ts b/apps/server/src/routes/app-spec/index.ts index 47950cd3..342aecd7 100644 --- a/apps/server/src/routes/app-spec/index.ts +++ b/apps/server/src/routes/app-spec/index.ts @@ -9,13 +9,17 @@ import { createGenerateHandler } from './routes/generate.js'; import { createGenerateFeaturesHandler } from './routes/generate-features.js'; import { createStopHandler } from './routes/stop.js'; import { createStatusHandler } from './routes/status.js'; +import type { SettingsService } from '../../services/settings-service.js'; -export function createSpecRegenerationRoutes(events: EventEmitter): Router { +export function createSpecRegenerationRoutes( + events: EventEmitter, + settingsService?: SettingsService +): Router { const router = Router(); router.post('/create', createCreateHandler(events)); - router.post('/generate', createGenerateHandler(events)); - router.post('/generate-features', createGenerateFeaturesHandler(events)); + router.post('/generate', createGenerateHandler(events, settingsService)); + router.post('/generate-features', createGenerateFeaturesHandler(events, settingsService)); router.post('/stop', createStopHandler()); router.get('/status', createStatusHandler()); diff --git a/apps/server/src/routes/app-spec/routes/generate-features.ts b/apps/server/src/routes/app-spec/routes/generate-features.ts index a2e6143a..0c80a9b6 100644 --- a/apps/server/src/routes/app-spec/routes/generate-features.ts +++ b/apps/server/src/routes/app-spec/routes/generate-features.ts @@ -13,10 +13,14 @@ import { getErrorMessage, } from '../common.js'; import { generateFeaturesFromSpec } from '../generate-features-from-spec.js'; +import type { SettingsService } from '../../../services/settings-service.js'; const logger = createLogger('SpecRegeneration'); -export function createGenerateFeaturesHandler(events: EventEmitter) { +export function createGenerateFeaturesHandler( + events: EventEmitter, + settingsService?: SettingsService +) { return async (req: Request, res: Response): Promise => { logger.info('========== /generate-features endpoint called =========='); logger.debug('Request body:', JSON.stringify(req.body, null, 2)); @@ -49,7 +53,7 @@ export function createGenerateFeaturesHandler(events: EventEmitter) { setRunningState(true, abortController); logger.info('Starting background feature generation task...'); - generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures) + generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures, settingsService) .catch((error) => { logError(error, 'Feature generation failed with error'); events.emit('spec-regeneration:event', { diff --git a/apps/server/src/routes/app-spec/routes/generate.ts b/apps/server/src/routes/app-spec/routes/generate.ts index 341d634d..a03dacb7 100644 --- a/apps/server/src/routes/app-spec/routes/generate.ts +++ b/apps/server/src/routes/app-spec/routes/generate.ts @@ -13,10 +13,11 @@ import { getErrorMessage, } from '../common.js'; import { generateSpec } from '../generate-spec.js'; +import type { SettingsService } from '../../../services/settings-service.js'; const logger = createLogger('SpecRegeneration'); -export function createGenerateHandler(events: EventEmitter) { +export function createGenerateHandler(events: EventEmitter, settingsService?: SettingsService) { return async (req: Request, res: Response): Promise => { logger.info('========== /generate endpoint called =========='); logger.debug('Request body:', JSON.stringify(req.body, null, 2)); @@ -67,7 +68,8 @@ export function createGenerateHandler(events: EventEmitter) { abortController, generateFeatures, analyzeProject, - maxFeatures + maxFeatures, + settingsService ) .catch((error) => { logError(error, 'Generation failed with error'); diff --git a/apps/server/src/routes/context/index.ts b/apps/server/src/routes/context/index.ts index 37e447bf..3f49f1c1 100644 --- a/apps/server/src/routes/context/index.ts +++ b/apps/server/src/routes/context/index.ts @@ -8,17 +8,19 @@ import { Router } from 'express'; import { createDescribeImageHandler } from './routes/describe-image.js'; import { createDescribeFileHandler } from './routes/describe-file.js'; +import type { SettingsService } from '../../services/settings-service.js'; /** * Create the context router * + * @param settingsService - Optional settings service for loading autoLoadClaudeMd setting * @returns Express router with context endpoints */ -export function createContextRoutes(): Router { +export function createContextRoutes(settingsService?: SettingsService): Router { const router = Router(); - router.post('/describe-image', createDescribeImageHandler()); - router.post('/describe-file', createDescribeFileHandler()); + router.post('/describe-image', createDescribeImageHandler(settingsService)); + router.post('/describe-file', createDescribeFileHandler(settingsService)); return router; } diff --git a/apps/server/src/routes/context/routes/describe-file.ts b/apps/server/src/routes/context/routes/describe-file.ts index 0e680b65..472cbb76 100644 --- a/apps/server/src/routes/context/routes/describe-file.ts +++ b/apps/server/src/routes/context/routes/describe-file.ts @@ -17,6 +17,8 @@ import { PathNotAllowedError } from '@automaker/platform'; import { createCustomOptions } from '../../../lib/sdk-options.js'; import * as secureFs from '../../../lib/secure-fs.js'; import * as path from 'path'; +import type { SettingsService } from '../../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; const logger = createLogger('DescribeFile'); @@ -72,9 +74,12 @@ async function extractTextFromStream( /** * Create the describe-file request handler * + * @param settingsService - Optional settings service for loading autoLoadClaudeMd setting * @returns Express request handler for file description */ -export function createDescribeFileHandler(): (req: Request, res: Response) => Promise { +export function createDescribeFileHandler( + settingsService?: SettingsService +): (req: Request, res: Response) => Promise { return async (req: Request, res: Response): Promise => { try { const { filePath } = req.body as DescribeFileRequestBody; @@ -165,6 +170,13 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`; // Use the file's directory as the working directory const cwd = path.dirname(resolvedPath); + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + cwd, + settingsService, + '[DescribeFile]' + ); + // Use centralized SDK options with proper cwd validation // No tools needed since we're passing file content directly const sdkOptions = createCustomOptions({ @@ -172,6 +184,7 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`; model: CLAUDE_MODEL_MAP.haiku, maxTurns: 1, allowedTools: [], + autoLoadClaudeMd, sandbox: { enabled: true, autoAllowBashIfSandboxed: true }, }); diff --git a/apps/server/src/routes/context/routes/describe-image.ts b/apps/server/src/routes/context/routes/describe-image.ts index 64ddfa0f..e4821b4a 100644 --- a/apps/server/src/routes/context/routes/describe-image.ts +++ b/apps/server/src/routes/context/routes/describe-image.ts @@ -17,6 +17,8 @@ import { CLAUDE_MODEL_MAP } from '@automaker/types'; import { createCustomOptions } from '../../../lib/sdk-options.js'; import * as fs from 'fs'; import * as path from 'path'; +import type { SettingsService } from '../../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; const logger = createLogger('DescribeImage'); @@ -226,9 +228,12 @@ async function extractTextFromStream( * Uses Claude SDK query with multi-part content blocks to include the image (base64), * matching the agent runner behavior. * + * @param settingsService - Optional settings service for loading autoLoadClaudeMd setting * @returns Express request handler for image description */ -export function createDescribeImageHandler(): (req: Request, res: Response) => Promise { +export function createDescribeImageHandler( + settingsService?: SettingsService +): (req: Request, res: Response) => Promise { return async (req: Request, res: Response): Promise => { const requestId = `describe-image-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; const startedAt = Date.now(); @@ -325,12 +330,20 @@ export function createDescribeImageHandler(): (req: Request, res: Response) => P const cwd = path.dirname(actualPath); logger.info(`[${requestId}] Using cwd=${cwd}`); + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + cwd, + settingsService, + '[DescribeImage]' + ); + // Use the same centralized option builder used across the server (validates cwd) const sdkOptions = createCustomOptions({ cwd, model: CLAUDE_MODEL_MAP.haiku, maxTurns: 1, allowedTools: [], + autoLoadClaudeMd, sandbox: { enabled: true, autoAllowBashIfSandboxed: true }, }); diff --git a/apps/server/src/routes/github/index.ts b/apps/server/src/routes/github/index.ts index e8088159..1a2f12ae 100644 --- a/apps/server/src/routes/github/index.ts +++ b/apps/server/src/routes/github/index.ts @@ -16,8 +16,12 @@ import { createDeleteValidationHandler, createMarkViewedHandler, } from './routes/validation-endpoints.js'; +import type { SettingsService } from '../../services/settings-service.js'; -export function createGitHubRoutes(events: EventEmitter): Router { +export function createGitHubRoutes( + events: EventEmitter, + settingsService?: SettingsService +): Router { const router = Router(); router.post('/check-remote', validatePathParams('projectPath'), createCheckGitHubRemoteHandler()); @@ -26,7 +30,7 @@ export function createGitHubRoutes(events: EventEmitter): Router { router.post( '/validate-issue', validatePathParams('projectPath'), - createValidateIssueHandler(events) + createValidateIssueHandler(events, settingsService) ); // Validation management endpoints diff --git a/apps/server/src/routes/github/routes/validate-issue.ts b/apps/server/src/routes/github/routes/validate-issue.ts index 3e75098e..c987453a 100644 --- a/apps/server/src/routes/github/routes/validate-issue.ts +++ b/apps/server/src/routes/github/routes/validate-issue.ts @@ -23,6 +23,8 @@ import { logError, logger, } from './validation-common.js'; +import type { SettingsService } from '../../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; /** Valid model values for validation */ const VALID_MODELS: readonly AgentModel[] = ['opus', 'sonnet', 'haiku'] as const; @@ -54,7 +56,8 @@ async function runValidation( issueLabels: string[] | undefined, model: AgentModel, events: EventEmitter, - abortController: AbortController + abortController: AbortController, + settingsService?: SettingsService ): Promise { // Emit start event const startEvent: IssueValidationEvent = { @@ -76,12 +79,20 @@ async function runValidation( // Build the prompt const prompt = buildValidationPrompt(issueNumber, issueTitle, issueBody, issueLabels); + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + settingsService, + '[ValidateIssue]' + ); + // Create SDK options with structured output and abort controller const options = createSuggestionsOptions({ cwd: projectPath, model, systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT, abortController, + autoLoadClaudeMd, outputFormat: { type: 'json_schema', schema: issueValidationSchema as Record, @@ -190,7 +201,10 @@ async function runValidation( * - System prompt guiding the validation process * - Async execution with event emission */ -export function createValidateIssueHandler(events: EventEmitter) { +export function createValidateIssueHandler( + events: EventEmitter, + settingsService?: SettingsService +) { return async (req: Request, res: Response): Promise => { try { const { @@ -256,7 +270,8 @@ export function createValidateIssueHandler(events: EventEmitter) { issueLabels, model, events, - abortController + abortController, + settingsService ) .catch((error) => { // Error is already handled inside runValidation (event emitted) diff --git a/apps/server/src/routes/suggestions/generate-suggestions.ts b/apps/server/src/routes/suggestions/generate-suggestions.ts index e4d6aaed..2af01a42 100644 --- a/apps/server/src/routes/suggestions/generate-suggestions.ts +++ b/apps/server/src/routes/suggestions/generate-suggestions.ts @@ -9,6 +9,8 @@ import { createSuggestionsOptions } from '../../lib/sdk-options.js'; import { FeatureLoader } from '../../services/feature-loader.js'; import { getAppSpecPath } from '@automaker/platform'; import * as secureFs from '../../lib/secure-fs.js'; +import type { SettingsService } from '../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js'; const logger = createLogger('Suggestions'); @@ -125,7 +127,8 @@ export async function generateSuggestions( projectPath: string, suggestionType: string, events: EventEmitter, - abortController: AbortController + abortController: AbortController, + settingsService?: SettingsService ): Promise { const typePrompts: Record = { features: 'Analyze this project and suggest new features that would add value.', @@ -154,9 +157,17 @@ The response will be automatically formatted as structured JSON.`; // Don't send initial message - let the agent output speak for itself // The first agent message will be captured as an info entry + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + settingsService, + '[Suggestions]' + ); + const options = createSuggestionsOptions({ cwd: projectPath, abortController, + autoLoadClaudeMd, outputFormat: { type: 'json_schema', schema: suggestionsSchema, diff --git a/apps/server/src/routes/suggestions/index.ts b/apps/server/src/routes/suggestions/index.ts index 2ea6f9ae..01e22879 100644 --- a/apps/server/src/routes/suggestions/index.ts +++ b/apps/server/src/routes/suggestions/index.ts @@ -8,11 +8,19 @@ import { validatePathParams } from '../../middleware/validate-paths.js'; import { createGenerateHandler } from './routes/generate.js'; import { createStopHandler } from './routes/stop.js'; import { createStatusHandler } from './routes/status.js'; +import type { SettingsService } from '../../services/settings-service.js'; -export function createSuggestionsRoutes(events: EventEmitter): Router { +export function createSuggestionsRoutes( + events: EventEmitter, + settingsService?: SettingsService +): Router { const router = Router(); - router.post('/generate', validatePathParams('projectPath'), createGenerateHandler(events)); + router.post( + '/generate', + validatePathParams('projectPath'), + createGenerateHandler(events, settingsService) + ); router.post('/stop', createStopHandler()); router.get('/status', createStatusHandler()); diff --git a/apps/server/src/routes/suggestions/routes/generate.ts b/apps/server/src/routes/suggestions/routes/generate.ts index 939e0cde..da57ed76 100644 --- a/apps/server/src/routes/suggestions/routes/generate.ts +++ b/apps/server/src/routes/suggestions/routes/generate.ts @@ -7,10 +7,11 @@ import type { EventEmitter } from '../../../lib/events.js'; import { createLogger } from '@automaker/utils'; import { getSuggestionsStatus, setRunningState, getErrorMessage, logError } from '../common.js'; import { generateSuggestions } from '../generate-suggestions.js'; +import type { SettingsService } from '../../../services/settings-service.js'; const logger = createLogger('Suggestions'); -export function createGenerateHandler(events: EventEmitter) { +export function createGenerateHandler(events: EventEmitter, settingsService?: SettingsService) { return async (req: Request, res: Response): Promise => { try { const { projectPath, suggestionType = 'features' } = req.body as { @@ -37,7 +38,7 @@ export function createGenerateHandler(events: EventEmitter) { setRunningState(true, abortController); // Start generation in background - generateSuggestions(projectPath, suggestionType, events, abortController) + generateSuggestions(projectPath, suggestionType, events, abortController, settingsService) .catch((error) => { logError(error, 'Generate suggestions failed (background)'); events.emit('suggestions:event', { diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index 93df5566..323c23c8 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -16,6 +16,8 @@ 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'; +import { getAutoLoadClaudeMdSetting, filterClaudeMdFromContext } from '../lib/settings-helpers.js'; interface Message { id: string; @@ -57,11 +59,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 { @@ -186,12 +190,23 @@ 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 getAutoLoadClaudeMdSetting( + effectiveWorkDir, + this.settingsService, + '[AgentService]' + ); + // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ + const contextResult = await loadContextFiles({ projectPath: effectiveWorkDir, fsModule: secureFs as Parameters[0]['fsModule'], }); + // When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication + // (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md + const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd); + // Build combined system prompt with base prompt and context files const baseSystemPrompt = this.getSystemPrompt(); const combinedSystemPrompt = contextFilesPrompt @@ -205,6 +220,7 @@ export class AgentService { sessionModel: session.model, systemPrompt: combinedSystemPrompt, abortController: session.abortController!, + autoLoadClaudeMd, }); // Extract model, maxTurns, and allowedTools from SDK options @@ -224,11 +240,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 }; diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 1da65e35..bcdb92a8 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -25,8 +25,14 @@ import { promisify } from 'util'; import path from 'path'; import * as secureFs from '../lib/secure-fs.js'; import type { EventEmitter } from '../lib/events.js'; -import { createAutoModeOptions, validateWorkingDirectory } from '../lib/sdk-options.js'; +import { + createAutoModeOptions, + createCustomOptions, + validateWorkingDirectory, +} from '../lib/sdk-options.js'; import { FeatureLoader } from './feature-loader.js'; +import type { SettingsService } from './settings-service.js'; +import { getAutoLoadClaudeMdSetting, filterClaudeMdFromContext } from '../lib/settings-helpers.js'; const execAsync = promisify(exec); @@ -341,9 +347,11 @@ export class AutoModeService { private autoLoopAbortController: AbortController | null = null; private config: AutoModeConfig | null = null; private pendingApprovals = new Map(); + private settingsService: SettingsService | null = null; - constructor(events: EventEmitter) { + constructor(events: EventEmitter, settingsService?: SettingsService) { this.events = events; + this.settingsService = settingsService ?? null; } /** @@ -551,14 +559,25 @@ export class AutoModeService { // Update feature status to in_progress await this.updateFeatureStatus(projectPath, featureId, 'in_progress'); + // Load autoLoadClaudeMd setting to determine context loading strategy + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + this.settingsService, + '[AutoMode]' + ); + // Build the prompt - use continuation prompt if provided (for recovery after plan approval) let prompt: string; // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt - const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ + const contextResult = await loadContextFiles({ projectPath, fsModule: secureFs as Parameters[0]['fsModule'], }); + // When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication + // (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md + const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd); + if (options?.continuationPrompt) { // Continuation prompt is used when recovering from a plan approval // The plan was already approved, so skip the planning phase @@ -604,6 +623,7 @@ export class AutoModeService { planningMode: feature.planningMode, requirePlanApproval: feature.requirePlanApproval, systemPrompt: contextFilesPrompt || undefined, + autoLoadClaudeMd, } ); @@ -746,12 +766,23 @@ export class AutoModeService { // No previous context } + // Load autoLoadClaudeMd setting to determine context loading strategy + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + this.settingsService, + '[AutoMode]' + ); + // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt - const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ + const contextResult = await loadContextFiles({ projectPath, fsModule: secureFs as Parameters[0]['fsModule'], }); + // When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication + // (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md + const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd); + // Build complete prompt with feature info, previous context, and follow-up instructions let fullPrompt = `## Follow-up on Feature Implementation @@ -879,6 +910,7 @@ Address the follow-up instructions above. Review the previous work and make the planningMode: 'skip', // Follow-ups don't require approval previousContent: previousContext || undefined, systemPrompt: contextFilesPrompt || undefined, + autoLoadClaudeMd, } ); @@ -1065,11 +1097,6 @@ Address the follow-up instructions above. Review the previous work and make the * Analyze project to gather context */ async analyzeProject(projectPath: string): Promise { - // Validate project path before proceeding - // This is called here because analyzeProject builds ExecuteOptions directly - // without using a factory function from sdk-options.ts - validateWorkingDirectory(projectPath); - const abortController = new AbortController(); const analysisFeatureId = `analysis-${Date.now()}`; @@ -1097,13 +1124,31 @@ Format your response as a structured markdown document.`; const analysisModel = resolveModelString(undefined, DEFAULT_MODELS.claude); const provider = ProviderFactory.getProviderForModel(analysisModel); - const options: ExecuteOptions = { - prompt, + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + this.settingsService, + '[AutoMode]' + ); + + // Use createCustomOptions for centralized SDK configuration with CLAUDE.md support + const sdkOptions = createCustomOptions({ + cwd: projectPath, model: analysisModel, maxTurns: 5, - cwd: projectPath, allowedTools: ['Read', 'Glob', 'Grep'], abortController, + autoLoadClaudeMd, + }); + + const options: ExecuteOptions = { + prompt, + model: sdkOptions.model ?? analysisModel, + cwd: sdkOptions.cwd ?? projectPath, + maxTurns: sdkOptions.maxTurns, + allowedTools: sdkOptions.allowedTools as string[], + abortController, + settingSources: sdkOptions.settingSources, }; const stream = provider.executeQuery(options); @@ -1708,6 +1753,7 @@ This helps parse your summary correctly in the output logs.`; requirePlanApproval?: boolean; previousContent?: string; systemPrompt?: string; + autoLoadClaudeMd?: boolean; } ): Promise { const finalProjectPath = options?.projectPath || projectPath; @@ -1780,11 +1826,19 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. return; } + // Load autoLoadClaudeMd setting (project setting takes precedence over global) + // Use provided value if available, otherwise load from settings + const autoLoadClaudeMd = + options?.autoLoadClaudeMd !== undefined + ? options.autoLoadClaudeMd + : await getAutoLoadClaudeMdSetting(finalProjectPath, this.settingsService, '[AutoMode]'); + // 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 +1877,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 diff --git a/apps/ui/src/components/dialogs/file-browser-dialog.tsx b/apps/ui/src/components/dialogs/file-browser-dialog.tsx index dc9c1c2e..9dae11bb 100644 --- a/apps/ui/src/components/dialogs/file-browser-dialog.tsx +++ b/apps/ui/src/components/dialogs/file-browser-dialog.tsx @@ -1,15 +1,5 @@ -import { useState, useEffect, useRef, useCallback } from 'react'; -import { - FolderOpen, - Folder, - ChevronRight, - Home, - ArrowLeft, - HardDrive, - CornerDownLeft, - Clock, - X, -} from 'lucide-react'; +import { useState, useEffect, useCallback } from 'react'; +import { FolderOpen, Folder, ChevronRight, HardDrive, Clock, X } from 'lucide-react'; import { Dialog, DialogContent, @@ -19,7 +9,7 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; +import { PathInput } from '@/components/ui/path-input'; import { getJSON, setJSON } from '@/lib/storage'; import { getDefaultWorkspaceDirectory, saveLastProjectDirectory } from '@/lib/workspace-config'; @@ -78,7 +68,6 @@ export function FileBrowserDialog({ initialPath, }: FileBrowserDialogProps) { const [currentPath, setCurrentPath] = useState(''); - const [pathInput, setPathInput] = useState(''); const [parentPath, setParentPath] = useState(null); const [directories, setDirectories] = useState([]); const [drives, setDrives] = useState([]); @@ -86,7 +75,6 @@ export function FileBrowserDialog({ const [error, setError] = useState(''); const [warning, setWarning] = useState(''); const [recentFolders, setRecentFolders] = useState([]); - const pathInputRef = useRef(null); // Load recent folders when dialog opens useEffect(() => { @@ -120,7 +108,6 @@ export function FileBrowserDialog({ if (result.success) { setCurrentPath(result.currentPath); - setPathInput(result.currentPath); setParentPath(result.parentPath); setDirectories(result.directories); setDrives(result.drives || []); @@ -142,11 +129,10 @@ export function FileBrowserDialog({ [browseDirectory] ); - // Reset current path when dialog closes + // Reset state when dialog closes useEffect(() => { if (!open) { setCurrentPath(''); - setPathInput(''); setParentPath(null); setDirectories([]); setError(''); @@ -172,9 +158,6 @@ export function FileBrowserDialog({ const pathToUse = initialPath || defaultDir; if (pathToUse) { - // Pre-fill the path input immediately - setPathInput(pathToUse); - // Then browse to that directory browseDirectory(pathToUse); } else { // No default directory, browse home directory @@ -183,7 +166,6 @@ export function FileBrowserDialog({ } catch { // If config fetch fails, try initialPath or fall back to home directory if (initialPath) { - setPathInput(initialPath); browseDirectory(initialPath); } else { browseDirectory(); @@ -199,34 +181,21 @@ export function FileBrowserDialog({ browseDirectory(dir.path); }; - const handleGoToParent = () => { - if (parentPath) { - browseDirectory(parentPath); - } - }; - - const handleGoHome = () => { + const handleGoHome = useCallback(() => { browseDirectory(); - }; + }, [browseDirectory]); + + const handleNavigate = useCallback( + (path: string) => { + browseDirectory(path); + }, + [browseDirectory] + ); const handleSelectDrive = (drivePath: string) => { browseDirectory(drivePath); }; - const handleGoToPath = () => { - const trimmedPath = pathInput.trim(); - if (trimmedPath) { - browseDirectory(trimmedPath); - } - }; - - const handlePathInputKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleGoToPath(); - } - }; - const handleSelect = useCallback(() => { if (currentPath) { addRecentFolder(currentPath); @@ -275,31 +244,15 @@ export function FileBrowserDialog({
- {/* Direct path input */} -
- setPathInput(e.target.value)} - onKeyDown={handlePathInputKeyDown} - className="flex-1 font-mono text-xs h-8" - data-testid="path-input" - disabled={loading} - /> - -
+ {/* Path navigation */} + {/* Recent folders */} {recentFolders.length > 0 && ( @@ -352,35 +305,8 @@ export function FileBrowserDialog({
)} - {/* Current path breadcrumb */} -
- - {parentPath && ( - - )} -
- {currentPath || 'Loading...'} -
-
- {/* Directory list */} -
+
{loading && (
Loading directories...
@@ -423,8 +349,8 @@ export function FileBrowserDialog({
- Paste a full path above, or click on folders to navigate. Press Enter or click Go to - jump to a path. + Paste a full path above, or click on folders to navigate. Press Enter or click β†’ to jump + to a path.
diff --git a/apps/ui/src/components/ui/breadcrumb.tsx b/apps/ui/src/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..f8f23e56 --- /dev/null +++ b/apps/ui/src/components/ui/breadcrumb.tsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { ChevronRight, MoreHorizontal } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { + return
); diff --git a/apps/ui/src/components/views/settings-view/claude/claude-md-settings.tsx b/apps/ui/src/components/views/settings-view/claude/claude-md-settings.tsx new file mode 100644 index 00000000..920984be --- /dev/null +++ b/apps/ui/src/components/views/settings-view/claude/claude-md-settings.tsx @@ -0,0 +1,82 @@ +import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; +import { FileCode } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface ClaudeMdSettingsProps { + autoLoadClaudeMd: boolean; + onAutoLoadClaudeMdChange: (enabled: boolean) => void; +} + +/** + * ClaudeMdSettings Component + * + * UI control for the autoLoadClaudeMd setting which enables automatic loading + * of project instructions from .claude/CLAUDE.md files via the Claude Agent SDK. + * + * Usage: + * ```tsx + * + * ``` + */ +export function ClaudeMdSettings({ + autoLoadClaudeMd, + onAutoLoadClaudeMdChange, +}: ClaudeMdSettingsProps) { + return ( +
+
+
+
+ +
+

+ CLAUDE.md Integration +

+
+

+ Configure automatic loading of project-specific instructions. +

+
+
+
+ onAutoLoadClaudeMdChange(checked === true)} + className="mt-1" + data-testid="auto-load-claude-md-checkbox" + /> +
+ +

+ Automatically load project instructions from{' '} + + .claude/CLAUDE.md + {' '} + files. When enabled, Claude will read and follow conventions specified in your + project's CLAUDE.md file. Project settings override global settings. +

+
+
+
+
+ ); +} diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index df5d85a5..2bca750b 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -223,6 +223,7 @@ export async function syncSettingsToServer(): Promise { muteDoneSound: state.muteDoneSound, enhancementModel: state.enhancementModel, validationModel: state.validationModel, + autoLoadClaudeMd: state.autoLoadClaudeMd, keyboardShortcuts: state.keyboardShortcuts, aiProfiles: state.aiProfiles, projects: state.projects, diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 978d67cc..874e1a6d 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -478,6 +478,9 @@ export interface AppState { // Validation Model Settings validationModel: AgentModel; // Model used for GitHub issue validation (default: opus) + // Claude Agent SDK Settings + autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option + // Project Analysis projectAnalysis: ProjectAnalysis | null; isAnalyzing: boolean; @@ -751,6 +754,9 @@ export interface AppActions { // Validation Model actions setValidationModel: (model: AgentModel) => void; + // Claude Agent SDK Settings actions + setAutoLoadClaudeMd: (enabled: boolean) => Promise; + // AI Profile actions addAIProfile: (profile: Omit) => void; updateAIProfile: (id: string, updates: Partial) => void; @@ -922,6 +928,7 @@ const initialState: AppState = { muteDoneSound: false, // Default to sound enabled (not muted) enhancementModel: 'sonnet', // Default to sonnet for feature enhancement validationModel: 'opus', // Default to opus for GitHub issue validation + autoLoadClaudeMd: false, // Default to disabled (user must opt-in) aiProfiles: DEFAULT_AI_PROFILES, projectAnalysis: null, isAnalyzing: false, @@ -1547,6 +1554,14 @@ export const useAppStore = create()( // Validation Model actions setValidationModel: (model) => set({ validationModel: model }), + // Claude Agent SDK Settings actions + setAutoLoadClaudeMd: async (enabled) => { + set({ autoLoadClaudeMd: enabled }); + // Sync to server settings file + const { syncSettingsToServer } = await import('@/hooks/use-settings-migration'); + await syncSettingsToServer(); + }, + // AI Profile actions addAIProfile: (profile) => { const id = `profile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; @@ -2690,6 +2705,7 @@ export const useAppStore = create()( muteDoneSound: state.muteDoneSound, enhancementModel: state.enhancementModel, validationModel: state.validationModel, + autoLoadClaudeMd: state.autoLoadClaudeMd, // Profiles and sessions aiProfiles: state.aiProfiles, chatSessions: state.chatSessions, diff --git a/libs/git-utils/src/diff.ts b/libs/git-utils/src/diff.ts index c3b048a4..72451dca 100644 --- a/libs/git-utils/src/diff.ts +++ b/libs/git-utils/src/diff.ts @@ -45,29 +45,45 @@ ${addedLines} /** * Generate a synthetic unified diff for an untracked (new) file * This is needed because `git diff HEAD` doesn't include untracked files + * + * If the path is a directory, this will recursively generate diffs for all files inside */ export async function generateSyntheticDiffForNewFile( basePath: string, relativePath: string ): Promise { - const fullPath = path.join(basePath, relativePath); + // Remove trailing slash if present (git status reports directories with trailing /) + const cleanPath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath; + const fullPath = path.join(basePath, cleanPath); try { - // Check if it's a binary file - if (isBinaryFile(relativePath)) { - return `diff --git a/${relativePath} b/${relativePath} -new file mode 100644 -index 0000000..0000000 -Binary file ${relativePath} added -`; - } - // Get file stats to check size and type const stats = await secureFs.stat(fullPath); - // Check if it's a directory (can happen with untracked directories from git status) + // Check if it's a directory first (before binary check) + // This handles edge cases like directories named "images.png/" if (stats.isDirectory()) { - return createNewFileDiff(relativePath, '040000', ['[Directory]']); + const filesInDir = await listAllFilesInDirectory(basePath, cleanPath); + if (filesInDir.length === 0) { + // Empty directory + return createNewFileDiff(cleanPath, '040000', ['[Empty directory]']); + } + // Generate diffs for all files in the directory sequentially + // Using sequential processing to avoid exhausting file descriptors on large directories + const diffs: string[] = []; + for (const filePath of filesInDir) { + diffs.push(await generateSyntheticDiffForNewFile(basePath, filePath)); + } + return diffs.join(''); + } + + // Check if it's a binary file (after directory check to handle dirs with binary extensions) + if (isBinaryFile(cleanPath)) { + return `diff --git a/${cleanPath} b/${cleanPath} +new file mode 100644 +index 0000000..0000000 +Binary file ${cleanPath} added +`; } const fileSize = Number(stats.size); @@ -92,11 +108,11 @@ Binary file ${relativePath} added const lineCount = lines.length; const addedLines = lines.map((line) => `+${line}`).join('\n'); - let diff = `diff --git a/${relativePath} b/${relativePath} + let diff = `diff --git a/${cleanPath} b/${cleanPath} new file mode 100644 index 0000000..0000000 --- /dev/null -+++ b/${relativePath} ++++ b/${cleanPath} @@ -0,0 +1,${lineCount} @@ ${addedLines}`; @@ -110,7 +126,7 @@ ${addedLines}`; // Log the error for debugging logger.error(`Failed to generate synthetic diff for ${fullPath}:`, error); // Return a placeholder diff - return createNewFileDiff(relativePath, '100644', ['[Unable to read file content]']); + return createNewFileDiff(cleanPath, '100644', ['[Unable to read file content]']); } } diff --git a/libs/git-utils/tests/diff.test.ts b/libs/git-utils/tests/diff.test.ts index 953d2763..4b03878e 100644 --- a/libs/git-utils/tests/diff.test.ts +++ b/libs/git-utils/tests/diff.test.ts @@ -118,7 +118,7 @@ describe('diff.ts', () => { expect(diff).toContain('[Unable to read file content]'); }); - it('should handle directory path gracefully', async () => { + it('should handle empty directory path gracefully', async () => { const dirName = 'some-directory'; const dirPath = path.join(tempDir, dirName); await fs.mkdir(dirPath); @@ -127,7 +127,38 @@ describe('diff.ts', () => { expect(diff).toContain(`diff --git a/${dirName} b/${dirName}`); expect(diff).toContain('new file mode 040000'); - expect(diff).toContain('[Directory]'); + expect(diff).toContain('[Empty directory]'); + }); + + it('should expand directory with files and generate diffs for each file', async () => { + const dirName = 'new-feature'; + const dirPath = path.join(tempDir, dirName); + await fs.mkdir(dirPath); + await fs.writeFile(path.join(dirPath, 'index.ts'), 'export const foo = 1;\n'); + await fs.writeFile(path.join(dirPath, 'utils.ts'), 'export const bar = 2;\n'); + + const diff = await generateSyntheticDiffForNewFile(tempDir, dirName); + + // Should contain diffs for both files in the directory + expect(diff).toContain(`diff --git a/${dirName}/index.ts b/${dirName}/index.ts`); + expect(diff).toContain(`diff --git a/${dirName}/utils.ts b/${dirName}/utils.ts`); + expect(diff).toContain('+export const foo = 1;'); + expect(diff).toContain('+export const bar = 2;'); + // Should NOT contain a diff for the directory itself + expect(diff).not.toContain('[Empty directory]'); + }); + + it('should handle directory path with trailing slash', async () => { + const dirName = 'trailing-slash-dir'; + const dirPath = path.join(tempDir, dirName); + await fs.mkdir(dirPath); + await fs.writeFile(path.join(dirPath, 'file.txt'), 'content\n'); + + // git status reports untracked directories with trailing slash + const diff = await generateSyntheticDiffForNewFile(tempDir, `${dirName}/`); + + expect(diff).toContain(`diff --git a/${dirName}/file.txt b/${dirName}/file.txt`); + expect(diff).toContain('+content'); }); }); diff --git a/libs/types/src/provider.ts b/libs/types/src/provider.ts index f3aa22d5..53c92717 100644 --- a/libs/types/src/provider.ts +++ b/libs/types/src/provider.ts @@ -19,6 +19,15 @@ export interface ConversationMessage { content: string | Array<{ type: string; text?: string; source?: object }>; } +/** + * System prompt preset configuration for CLAUDE.md auto-loading + */ +export interface SystemPromptPreset { + type: 'preset'; + preset: 'claude_code'; + append?: string; +} + /** * Options for executing a query via a provider */ @@ -26,13 +35,14 @@ export interface ExecuteOptions { prompt: string | Array<{ type: string; text?: string; source?: object }>; model: string; cwd: string; - systemPrompt?: string; + systemPrompt?: string | SystemPromptPreset; maxTurns?: number; allowedTools?: string[]; mcpServers?: Record; abortController?: AbortController; conversationHistory?: ConversationMessage[]; // Previous messages for context sdkSessionId?: string; // Claude SDK session ID for resuming conversations + settingSources?: Array<'user' | 'project' | 'local'>; // Sources for CLAUDE.md loading } /** diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index e18c2987..e73e7269 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -297,6 +297,10 @@ export interface GlobalSettings { // Window State (Electron only) /** Persisted window bounds for restoring position/size across sessions */ windowBounds?: WindowBounds; + + // Claude Agent SDK Settings + /** Auto-load CLAUDE.md files using SDK's settingSources option */ + autoLoadClaudeMd?: boolean; } /** @@ -392,6 +396,10 @@ export interface ProjectSettings { // Session Tracking /** Last chat session selected in this project */ lastSelectedSessionId?: string; + + // Claude Agent SDK Settings + /** Auto-load CLAUDE.md files using SDK's settingSources option (project override) */ + autoLoadClaudeMd?: boolean; } /** @@ -450,6 +458,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { recentFolders: [], worktreePanelCollapsed: false, lastSelectedSessionByProject: {}, + autoLoadClaudeMd: false, }; /** Default credentials (empty strings - user must provide API keys) */