diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index b2afb793..bf0ba55f 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -120,15 +120,55 @@ Paste multiple lines of text (e.g., code snippets, logs) and they'll be automati ## Project Context Files -Place an `AGENT.md` or `CLAUDE.md` file in your project root to provide context to the AI. The contents will be automatically included at the start of new sessions (not when continuing/resuming sessions). +The agent automatically loads context from `AGENT.md` or `CLAUDE.md` files at the start of new sessions (not when continuing/resuming). These files are loaded in hierarchical order to support both global preferences and monorepo structures. -This is useful for: +### File Locations + +Context files are loaded in this order: + +1. **Global context**: `~/.pi/agent/AGENT.md` or `CLAUDE.md` + - Applies to all your coding sessions + - Great for personal coding preferences and workflows + +2. **Parent directories** (top-most first down to current directory) + - Walks up from current directory to filesystem root + - Each directory can have its own `AGENT.md` or `CLAUDE.md` + - Perfect for monorepos with shared context at higher levels + +3. **Current directory**: Your project's `AGENT.md` or `CLAUDE.md` + - Most specific context, loaded last + - Overwrites or extends parent/global context + +**File preference**: In each directory, `AGENT.md` is preferred over `CLAUDE.md` if both exist. + +### What to Include + +Context files are useful for: - Project-specific instructions and guidelines +- Common bash commands and workflows - Architecture documentation - Coding conventions and style guides - Dependencies and setup information +- Testing instructions +- Repository etiquette (branch naming, merge vs. rebase, etc.) -The file is injected as a user message at the beginning of each new session, ensuring the AI has project context without modifying the system prompt. +### Example + +```markdown +# Common Commands +- npm run build: Build the project +- npm test: Run tests + +# Code Style +- Use TypeScript strict mode +- Prefer async/await over promises + +# Workflow +- Always run tests before committing +- Update CHANGELOG.md for user-facing changes +``` + +Each file is injected as a separate user message at the beginning of new sessions, ensuring the AI has full project context without modifying the system prompt. ## Image Support @@ -142,26 +182,6 @@ Supported formats: `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp` The image will be automatically encoded and sent with your message. JPEG and PNG are supported across all vision models. Other formats may only be supported by some models. -## Available Tools - -The agent has access to four core tools for working with your codebase: - -### read - -Read file contents. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit parameters for large files. Lines longer than 2000 characters are truncated. - -### write - -Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories. - -### edit - -Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits. Returns an error if the text appears multiple times or isn't found. - -### bash - -Execute a bash command in the current working directory. Returns stdout and stderr. Commands run with a 30 second timeout. - ## Session Management Sessions are automatically saved in `~/.pi/agent/sessions/` organized by working directory. Each session is stored as a JSONL file with a unique timestamp-based ID. @@ -268,6 +288,26 @@ pi -c "What did we discuss?" pi --provider openai --model gpt-4o "Help me refactor this code" ``` +## Available Tools + +The agent has access to four core tools for working with your codebase: + +### read + +Read file contents. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit parameters for large files. Lines longer than 2000 characters are truncated. + +### write + +Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories. + +### edit + +Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits. Returns an error if the text appears multiple times or isn't found. + +### bash + +Execute a bash command in the current working directory. Returns stdout and stderr. Commands run with a 30 second timeout. + ## License MIT diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index d654aa81..6a12a760 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -3,7 +3,8 @@ import { getModel, type KnownProvider } from "@mariozechner/pi-ai"; import { ProcessTerminal, TUI } from "@mariozechner/pi-tui"; import chalk from "chalk"; import { existsSync, readFileSync } from "fs"; -import { dirname, join } from "path"; +import { homedir } from "os"; +import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; import { SessionManager } from "./session-manager.js"; import { codingTools } from "./tools/index.js"; @@ -149,22 +150,72 @@ Guidelines: Current directory: ${process.cwd()}`; /** - * Look for AGENT.md or CLAUDE.md in the current directory and return its contents + * Look for AGENT.md or CLAUDE.md in a directory (prefers AGENT.md) */ -function loadProjectContext(): string | null { +function loadContextFileFromDir(dir: string): { path: string; content: string } | null { const candidates = ["AGENT.md", "CLAUDE.md"]; for (const filename of candidates) { - if (existsSync(filename)) { + const filePath = join(dir, filename); + if (existsSync(filePath)) { try { - return readFileSync(filename, "utf-8"); + return { + path: filePath, + content: readFileSync(filePath, "utf-8"), + }; } catch (error) { - console.error(chalk.yellow(`Warning: Could not read ${filename}: ${error}`)); + console.error(chalk.yellow(`Warning: Could not read ${filePath}: ${error}`)); } } } return null; } +/** + * Load all project context files in order: + * 1. Global: ~/.pi/agent/AGENT.md or CLAUDE.md + * 2. Parent directories (top-most first) down to cwd + * Each returns {path, content} for separate messages + */ +function loadProjectContextFiles(): Array<{ path: string; content: string }> { + const contextFiles: Array<{ path: string; content: string }> = []; + + // 1. Load global context from ~/.pi/agent/ + const homeDir = homedir(); + const globalContextDir = resolve(process.env.CODING_AGENT_DIR || join(homeDir, ".pi/agent/")); + const globalContext = loadContextFileFromDir(globalContextDir); + if (globalContext) { + contextFiles.push(globalContext); + } + + // 2. Walk up from cwd to root, collecting all context files + const cwd = process.cwd(); + const ancestorContextFiles: Array<{ path: string; content: string }> = []; + + let currentDir = cwd; + const root = resolve("/"); + + while (true) { + const contextFile = loadContextFileFromDir(currentDir); + if (contextFile) { + // Add to beginning so we get top-most parent first + ancestorContextFiles.unshift(contextFile); + } + + // Stop if we've reached root + if (currentDir === root) break; + + // Move up one directory + const parentDir = resolve(currentDir, ".."); + if (parentDir === currentDir) break; // Safety check + currentDir = parentDir; + } + + // Add ancestor files in order (top-most → cwd) + contextFiles.push(...ancestorContextFiles); + + return contextFiles; +} + async function selectSession(sessionManager: SessionManager): Promise { return new Promise((resolve) => { const ui = new TUI(new ProcessTerminal()); @@ -428,23 +479,26 @@ export async function main(args: string[]) { // Note: Session will be started lazily after first user+assistant message exchange // (unless continuing/resuming, in which case it's already initialized) - // Inject project context (AGENT.md/CLAUDE.md) if not continuing/resuming + // Inject project context files (AGENT.md/CLAUDE.md) if not continuing/resuming if (!parsed.continue && !parsed.resume) { - const projectContext = loadProjectContext(); - if (projectContext) { - // Queue the context as a message that will be injected at the start - await agent.queueMessage({ - role: "user", - content: [ - { - type: "text", - text: `[Project Context from ${existsSync("AGENT.md") ? "AGENT.md" : "CLAUDE.md"}]\n\n${projectContext}`, - }, - ], - timestamp: Date.now(), - }); + const contextFiles = loadProjectContextFiles(); + if (contextFiles.length > 0) { + // Queue each context file as a separate message + for (const { path: filePath, content } of contextFiles) { + await agent.queueMessage({ + role: "user", + content: [ + { + type: "text", + text: `[Project Context from ${filePath}]\n\n${content}`, + }, + ], + timestamp: Date.now(), + }); + } if (shouldPrintMessages) { - console.log(chalk.dim(`Loaded project context from ${existsSync("AGENT.md") ? "AGENT.md" : "CLAUDE.md"}`)); + const fileList = contextFiles.map((f) => f.path).join(", "); + console.log(chalk.dim(`Loaded project context from: ${fileList}`)); } } }