diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dcfa8f..6e04c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ All notable changes to this project will be documented in this file, per [the Ke - `list_service_versions` MCP tool — list the PHP, database, and web server versions available to `create_site`, flagging which are already installed versus downloaded on demand (props [@ivanlopez](https://github.com/ivanlopez) via [#80](https://github.com/10up/localwp-agent-tools/pull/80)). - `site_status` now reports a `creationError` when a site created with `create_site` failed during provisioning (props [@ivanlopez](https://github.com/ivanlopez) via [#80](https://github.com/10up/localwp-agent-tools/pull/80)). +### Changed + +- Write Claude Code project context to `CLAUDE.local.md` instead of `CLAUDE.md`, so teams with a committed `CLAUDE.md` are no longer affected. `CLAUDE.local.md` is Claude Code's native local-override file for machine-specific, uncommitted instructions (props [@rickalee](https://github.com/rickalee) via [#78](https://github.com/10up/localwp-agent-tools/pull/78)). +- Any Agent Tools marker block that an older version wrote to `CLAUDE.md` is removed on the next enable, regenerate, disable, project directory change, or agent change. The rest of `CLAUDE.md` is left intact. Teams that committed the generated block will see it removed from `CLAUDE.md` (props [@rickalee](https://github.com/rickalee) via [#78](https://github.com/10up/localwp-agent-tools/pull/78)). + ## [0.2.1] - 2026-03-19 ### Fixed diff --git a/README.md b/README.md index af47408..5559d02 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ When you click "Enable" on a site in Local, the add-on: 1. **Registers the site with the MCP server** — a single HTTP server running in Local's main process that gives AI tools access to WP-CLI, error logs, configuration, and site management 2. **Writes MCP config** (`.mcp.json`, `.cursor/mcp.json`, etc.) — auto-configured with the correct HTTP endpoint for each agent -3. **Generates project context** (`CLAUDE.md`, `.cursorrules`, etc.) — site context including PHP/MySQL versions, active plugins, theme, and file structure +3. **Generates project context** (`CLAUDE.local.md`, `.cursorrules`, etc.) — site context including PHP/MySQL versions, active plugins, theme, and file structure 4. **Updates `.gitignore`** — so generated files aren't committed Then open the site folder in your AI tool of choice and you're ready to go. @@ -31,7 +31,7 @@ Sites remain registered even when stopped, so the MCP endpoint is always reachab | Agent | MCP Config | Context File | | --------------- | -------------------- | --------------------------------- | -| Claude Code | `.mcp.json` | `CLAUDE.md` | +| Claude Code | `.mcp.json` | `CLAUDE.local.md` | | Cursor | `.cursor/mcp.json` | `.cursorrules` | | Windsurf | `.windsurf/mcp.json` | `.windsurfrules` | | VS Code Copilot | `.vscode/mcp.json` | `.github/copilot-instructions.md` | diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 6e137ba..c3827a7 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -35,3 +35,13 @@ export function buildWpCliEnv(config: SiteConfig): NodeJS.ProcessEnv { ...(config.dbPort ? { DB_PORT: String(config.dbPort) } : {}), }; } + +/** + * True when `content` holds at least one complete block that starts with + * `start` and ends with `end`. Used to decide whether a file we did not + * create still carries an Agent Tools marker block. + */ +export function hasMarkerBlock(content: string, start: string, end: string): boolean { + const pattern = new RegExp(`${escapeRegex(start)}[\\s\\S]*?${escapeRegex(end)}`); + return pattern.test(content); +} diff --git a/src/main.ts b/src/main.ts index 6cb129c..486ec7c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,7 @@ import { } from './helpers/paths'; import { SiteConfig, SiteConfigRegistry } from './helpers/site-config'; import { findAvailablePort, savePort, removePortFile, removePortFileSync } from './helpers/port'; +import { hasMarkerBlock } from './helpers/utils'; import { createMcpHttpServer, startMcpHttpServer, stopMcpHttpServer, closeSessionsForSite } from './mcp-server'; import { LocalApi, CreateSiteOptions, CreateSiteResult, ServiceVersion, ServiceVersions } from './tools'; import { @@ -45,6 +46,8 @@ interface AgentTargetConfig { mcpConfigTopLevelKey: string; /** Path to project context/instructions file, relative to project dir */ contextFilePath: string; + /** Context files older versions wrote for this agent; our marker block is removed from them */ + legacyContextFilePaths?: string[]; /** Extra entries to add to .gitignore */ gitignoreEntries: string[]; } @@ -54,8 +57,9 @@ const AGENT_TARGETS: Record = { label: 'Claude Code', mcpConfigPath: '.mcp.json', mcpConfigTopLevelKey: 'mcpServers', - contextFilePath: 'CLAUDE.md', - gitignoreEntries: ['.mcp.json', 'CLAUDE.md'], + contextFilePath: 'CLAUDE.local.md', + legacyContextFilePaths: ['CLAUDE.md'], + gitignoreEntries: ['.mcp.json', 'CLAUDE.local.md'], }, cursor: { label: 'Cursor', @@ -405,6 +409,27 @@ async function updateGitignore(dirPath: string, agents: AgentTarget[]): Promise< await fs.writeFile(gitignorePath, content, 'utf-8'); } +// --------------------------------------------------------------------------- +// Migration Helpers +// --------------------------------------------------------------------------- + +/** + * Removes any Agent Tools marker block from context files that older versions + * wrote for an agent (for example CLAUDE.md, before the move to CLAUDE.local.md). + * Files without our marker block are left untouched. + */ +async function removeLegacyContextFiles(projectPath: string, agent: AgentTarget): Promise { + for (const relativePath of AGENT_TARGETS[agent].legacyContextFilePaths ?? []) { + const legacyPath = path.join(projectPath, relativePath); + if (!(await fs.pathExists(legacyPath))) continue; + + const content = await fs.readFile(legacyPath, 'utf-8'); + if (!hasMarkerBlock(content, CONTEXT_MARKER_START, CONTEXT_MARKER_END)) continue; + + await removeContextFile(legacyPath, agent); + } +} + // --------------------------------------------------------------------------- // Core Functions // --------------------------------------------------------------------------- @@ -435,7 +460,8 @@ async function setupSite(site: Local.Site, notifier: any, projectDir: string, ag const serverEntry = buildMcpServerEntry(agent, mcpServerPort, site.id); await mergeMcpConfig(mcpConfigPath, serverEntry, agentConfig.mcpConfigTopLevelKey); - // Write project context + // Write project context (and clean up any context file an older version wrote) + await removeLegacyContextFiles(projectPath, agent); const contextPath = path.join(projectPath, agentConfig.contextFilePath); await writeContextFile(contextPath, contextContent, agent); } @@ -480,6 +506,7 @@ async function teardownSite(site: Local.Site, notifier: any): Promise { await removeMcpConfigEntry(path.join(projectPath, agentConfig.mcpConfigPath), agentConfig.mcpConfigTopLevelKey); await removeContextFile(path.join(projectPath, agentConfig.contextFilePath), agent); + await removeLegacyContextFiles(projectPath, agent); } // 4. Clean up .gitignore @@ -515,6 +542,7 @@ async function changeProjectDir(site: Local.Site, newProjectDir: string, notifie await removeMcpConfigEntry(path.join(oldPath, agentConfig.mcpConfigPath), agentConfig.mcpConfigTopLevelKey); await removeContextFile(path.join(oldPath, agentConfig.contextFilePath), agent); + await removeLegacyContextFiles(oldPath, agent); } await updateGitignore(oldPath, []); @@ -530,6 +558,7 @@ async function changeProjectDir(site: Local.Site, newProjectDir: string, notifie serverEntry, agentConfig.mcpConfigTopLevelKey, ); + await removeLegacyContextFiles(newPath, agent); await writeContextFile(path.join(newPath, agentConfig.contextFilePath), contextContent, agent); } @@ -566,6 +595,7 @@ async function updateAgents(site: Local.Site, newAgents: AgentTarget[], notifier await removeMcpConfigEntry(path.join(projectPath, agentConfig.mcpConfigPath), agentConfig.mcpConfigTopLevelKey); await removeContextFile(path.join(projectPath, agentConfig.contextFilePath), agent); + await removeLegacyContextFiles(projectPath, agent); } // Add configs for newly selected agents @@ -581,6 +611,7 @@ async function updateAgents(site: Local.Site, newAgents: AgentTarget[], notifier serverEntry, agentConfig.mcpConfigTopLevelKey, ); + await removeLegacyContextFiles(projectPath, agent); await writeContextFile(path.join(projectPath, agentConfig.contextFilePath), contextContent, agent); } } @@ -626,6 +657,7 @@ async function regenerateConfig(site: Local.Site): Promise { serverEntry, agentConfig.mcpConfigTopLevelKey, ); + await removeLegacyContextFiles(projectPath, agent); await writeContextFile(path.join(projectPath, agentConfig.contextFilePath), contextContent, agent); } } diff --git a/tests/helpers/utils.test.ts b/tests/helpers/utils.test.ts index 29b8a4c..5ad384a 100644 --- a/tests/helpers/utils.test.ts +++ b/tests/helpers/utils.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, afterEach } from 'vitest'; -import { buildWpCliEnv } from '../../src/helpers/utils'; +import { buildWpCliEnv, hasMarkerBlock } from '../../src/helpers/utils'; import type { SiteConfig } from '../../src/helpers/site-config'; function makeSiteConfig(overrides: Partial = {}): SiteConfig { @@ -71,3 +71,25 @@ describe('buildWpCliEnv', () => { expect(env.DB_PASSWORD).toBe('root'); }); }); + +describe('hasMarkerBlock', () => { + const start = ''; + const end = ''; + + it('returns true when a complete marker block is present', () => { + const content = `# My notes\n\n${start}\nGenerated context\n${end}\n\nMore notes\n`; + expect(hasMarkerBlock(content, start, end)).toBe(true); + }); + + it('returns false for a file without our markers', () => { + expect(hasMarkerBlock('# My notes\n\nHand-written content\n', start, end)).toBe(false); + }); + + it('returns false when only the start marker is present', () => { + expect(hasMarkerBlock(`${start}\nTruncated block\n`, start, end)).toBe(false); + }); + + it('returns false for an empty file', () => { + expect(hasMarkerBlock('', start, end)).toBe(false); + }); +});