feat: hierarchical context file loading for monorepos

- Walk up parent directories to load all AGENT.md/CLAUDE.md files
- Load global context from ~/.pi/agent/AGENT.md or CLAUDE.md
- Load order: global → top-most parent → ... → cwd
- Prefer AGENT.md over CLAUDE.md in each directory
- Each context file injected as separate message
- Updated README with detailed documentation
This commit is contained in:
Mario Zechner 2025-11-12 22:17:54 +01:00
parent 812f2f43cd
commit dca3e1cc60
2 changed files with 138 additions and 44 deletions

View file

@ -120,15 +120,55 @@ Paste multiple lines of text (e.g., code snippets, logs) and they'll be automati
## Project Context Files ## 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 - Project-specific instructions and guidelines
- Common bash commands and workflows
- Architecture documentation - Architecture documentation
- Coding conventions and style guides - Coding conventions and style guides
- Dependencies and setup information - 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 ## 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. 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 ## 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. 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" 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 ## License
MIT MIT

View file

@ -3,7 +3,8 @@ import { getModel, type KnownProvider } from "@mariozechner/pi-ai";
import { ProcessTerminal, TUI } from "@mariozechner/pi-tui"; import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
import chalk from "chalk"; import chalk from "chalk";
import { existsSync, readFileSync } from "fs"; 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 { fileURLToPath } from "url";
import { SessionManager } from "./session-manager.js"; import { SessionManager } from "./session-manager.js";
import { codingTools } from "./tools/index.js"; import { codingTools } from "./tools/index.js";
@ -149,22 +150,72 @@ Guidelines:
Current directory: ${process.cwd()}`; 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"]; const candidates = ["AGENT.md", "CLAUDE.md"];
for (const filename of candidates) { for (const filename of candidates) {
if (existsSync(filename)) { const filePath = join(dir, filename);
if (existsSync(filePath)) {
try { try {
return readFileSync(filename, "utf-8"); return {
path: filePath,
content: readFileSync(filePath, "utf-8"),
};
} catch (error) { } catch (error) {
console.error(chalk.yellow(`Warning: Could not read ${filename}: ${error}`)); console.error(chalk.yellow(`Warning: Could not read ${filePath}: ${error}`));
} }
} }
} }
return null; 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<string | null> { async function selectSession(sessionManager: SessionManager): Promise<string | null> {
return new Promise((resolve) => { return new Promise((resolve) => {
const ui = new TUI(new ProcessTerminal()); 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 // Note: Session will be started lazily after first user+assistant message exchange
// (unless continuing/resuming, in which case it's already initialized) // (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) { if (!parsed.continue && !parsed.resume) {
const projectContext = loadProjectContext(); const contextFiles = loadProjectContextFiles();
if (projectContext) { if (contextFiles.length > 0) {
// Queue the context as a message that will be injected at the start // Queue each context file as a separate message
await agent.queueMessage({ for (const { path: filePath, content } of contextFiles) {
role: "user", await agent.queueMessage({
content: [ role: "user",
{ content: [
type: "text", {
text: `[Project Context from ${existsSync("AGENT.md") ? "AGENT.md" : "CLAUDE.md"}]\n\n${projectContext}`, type: "text",
}, text: `[Project Context from ${filePath}]\n\n${content}`,
], },
timestamp: Date.now(), ],
}); timestamp: Date.now(),
});
}
if (shouldPrintMessages) { 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}`));
} }
} }
} }