Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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` |
Expand Down
10 changes: 10 additions & 0 deletions src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
38 changes: 35 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
findWpCli,
} from './helpers/paths';
import { SiteConfig, SiteConfigRegistry } from './helpers/site-config';
import { findAvailablePort, savePort, removePortFile, removePortFileSync } from './helpers/port';

Check warning on line 14 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

'removePortFile' is defined but never used
import { hasMarkerBlock } from './helpers/utils';
import { createMcpHttpServer, startMcpHttpServer, stopMcpHttpServer, closeSessionsForSite } from './mcp-server';
import { LocalApi, CreateSiteOptions, CreateSiteResult, ServiceVersion, ServiceVersions } from './tools';
import {
Expand Down Expand Up @@ -45,6 +46,8 @@
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[];
}
Expand All @@ -54,8 +57,9 @@
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',
Expand Down Expand Up @@ -193,7 +197,7 @@
* Builds the MCP server entry for a specific agent.
* Each agent has different JSON shapes for HTTP MCP servers.
*/
function buildMcpServerEntry(agent: AgentTarget, port: number, siteId: string): Record<string, any> {

Check warning on line 200 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const url = `http://localhost:${port}/sites/${siteId}/mcp`;

switch (agent) {
Expand All @@ -215,10 +219,10 @@
*/
async function mergeMcpConfig(
configPath: string,
serverEntry: Record<string, any>,

Check warning on line 222 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
topLevelKey: string,
): Promise<void> {
let existing: any = {};

Check warning on line 225 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type

if (await fs.pathExists(configPath)) {
try {
Expand Down Expand Up @@ -405,11 +409,32 @@
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<void> {
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
// ---------------------------------------------------------------------------

async function setupSite(site: Local.Site, notifier: any, projectDir: string, agents: AgentTarget[]): Promise<void> {

Check warning on line 437 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const sitePath = getSitePath(site);
const projectPath = getProjectPath(sitePath, projectDir);

Expand All @@ -435,7 +460,8 @@
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);
}
Expand All @@ -461,7 +487,7 @@
});
}

async function teardownSite(site: Local.Site, notifier: any): Promise<void> {

Check warning on line 490 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const sitePath = getSitePath(site);
const projectDir = getStoredProjectDir(site);
const projectPath = getProjectPath(sitePath, projectDir);
Expand All @@ -480,6 +506,7 @@

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
Expand All @@ -500,7 +527,7 @@
});
}

async function changeProjectDir(site: Local.Site, newProjectDir: string, notifier: any): Promise<void> {

Check warning on line 530 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const sitePath = getSitePath(site);
const oldProjectDir = getStoredProjectDir(site);
const oldPath = getProjectPath(sitePath, oldProjectDir);
Expand All @@ -515,6 +542,7 @@

await removeMcpConfigEntry(path.join(oldPath, agentConfig.mcpConfigPath), agentConfig.mcpConfigTopLevelKey);
await removeContextFile(path.join(oldPath, agentConfig.contextFilePath), agent);
await removeLegacyContextFiles(oldPath, agent);
}
await updateGitignore(oldPath, []);

Expand All @@ -530,6 +558,7 @@
serverEntry,
agentConfig.mcpConfigTopLevelKey,
);
await removeLegacyContextFiles(newPath, agent);
await writeContextFile(path.join(newPath, agentConfig.contextFilePath), contextContent, agent);
}

Expand All @@ -549,7 +578,7 @@
});
}

async function updateAgents(site: Local.Site, newAgents: AgentTarget[], notifier: any): Promise<void> {

Check warning on line 581 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
if (!isAgentToolsEnabled(site)) return;

const sitePath = getSitePath(site);
Expand All @@ -566,6 +595,7 @@

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
Expand All @@ -581,6 +611,7 @@
serverEntry,
agentConfig.mcpConfigTopLevelKey,
);
await removeLegacyContextFiles(projectPath, agent);
await writeContextFile(path.join(projectPath, agentConfig.contextFilePath), contextContent, agent);
}
}
Expand Down Expand Up @@ -626,6 +657,7 @@
serverEntry,
agentConfig.mcpConfigTopLevelKey,
);
await removeLegacyContextFiles(projectPath, agent);
await writeContextFile(path.join(projectPath, agentConfig.contextFilePath), contextContent, agent);
}
}
Expand Down
24 changes: 23 additions & 1 deletion tests/helpers/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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> = {}): SiteConfig {
Expand Down Expand Up @@ -71,3 +71,25 @@ describe('buildWpCliEnv', () => {
expect(env.DB_PASSWORD).toBe('root');
});
});

describe('hasMarkerBlock', () => {
const start = '<!-- >>> Agent Tools (auto-generated, do not edit) -->';
const end = '<!-- <<< Agent Tools -->';

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);
});
});
Loading