fix: namespace cursor agent installs

This commit is contained in:
Affaan Mustafa 2026-04-30 02:12:24 -04:00 committed by Affaan Mustafa
parent 5881554a1c
commit e1d6d853f7
9 changed files with 121 additions and 7 deletions

View File

@ -1133,7 +1133,7 @@ These are not bundled with ECC and are not audited by this repo, but they are wo
## Cursor IDE Support ## Cursor IDE Support
ECC provides **full Cursor IDE support** with hooks, rules, agents, skills, commands, and MCP configs adapted for Cursor's native format. ECC provides Cursor IDE support with hooks, rules, agents, skills, commands, and MCP configs adapted for Cursor's project layout.
### Quick Start (Cursor) ### Quick Start (Cursor)
@ -1156,11 +1156,17 @@ ECC provides **full Cursor IDE support** with hooks, rules, agents, skills, comm
| Hook Events | 15 | sessionStart, beforeShellExecution, afterFileEdit, beforeMCPExecution, beforeSubmitPrompt, and 10 more | | Hook Events | 15 | sessionStart, beforeShellExecution, afterFileEdit, beforeMCPExecution, beforeSubmitPrompt, and 10 more |
| Hook Scripts | 16 | Thin Node.js scripts delegating to `scripts/hooks/` via shared adapter | | Hook Scripts | 16 | Thin Node.js scripts delegating to `scripts/hooks/` via shared adapter |
| Rules | 34 | 9 common (alwaysApply) + 25 language-specific (TypeScript, Python, Go, Swift, PHP) | | Rules | 34 | 9 common (alwaysApply) + 25 language-specific (TypeScript, Python, Go, Swift, PHP) |
| Agents | Shared | Via AGENTS.md at root (read by Cursor natively) | | Agents | 48 | `.cursor/agents/ecc-*.md` when installed; prefixed to avoid collisions with user or marketplace agents |
| Skills | Shared + Bundled | Via AGENTS.md at root and `.cursor/skills/` for translated additions | | Skills | Shared + Bundled | `.cursor/skills/` for translated additions |
| Commands | Shared | `.cursor/commands/` if installed | | Commands | Shared | `.cursor/commands/` if installed |
| MCP Config | Shared | `.cursor/mcp.json` if installed | | MCP Config | Shared | `.cursor/mcp.json` if installed |
### Cursor Loading Notes
ECC does not install root `AGENTS.md` into `.cursor/`. Cursor treats nested `AGENTS.md` files as directory context, so copying ECC's repo identity into a host project would pollute that project.
Cursor-native loading behavior can vary by Cursor build. ECC installs agents as `.cursor/agents/ecc-*.md`; if your Cursor build does not expose project agents, those files still work as explicit reference definitions instead of hidden global prompt context.
### Hook Architecture (DRY Adapter Pattern) ### Hook Architecture (DRY Adapter Pattern)
Cursor has **more hook events than Claude Code** (20 vs 8). The `.cursor/hooks/adapter.js` module transforms Cursor's stdin JSON to Claude Code's format, allowing existing `scripts/hooks/*.js` to be reused without duplication. Cursor has **more hook events than Claude Code** (20 vs 8). The `.cursor/hooks/adapter.js` module transforms Cursor's stdin JSON to Claude Code's format, allowing existing `scripts/hooks/*.js` to be reused without duplication.

View File

@ -0,0 +1,26 @@
'use strict';
const path = require('path');
function toCursorAgentFileName(fileName) {
if (!fileName || fileName.startsWith('ecc-')) {
return fileName;
}
return `ecc-${fileName}`;
}
function toCursorAgentRelativePath(relativePath) {
const segments = String(relativePath || '').split(/[\\/]+/).filter(Boolean);
if (segments.length === 0) {
return relativePath;
}
const fileName = segments.pop();
return path.join(...segments, toCursorAgentFileName(fileName));
}
module.exports = {
toCursorAgentFileName,
toCursorAgentRelativePath,
};

View File

@ -3,6 +3,7 @@ const os = require('os');
const path = require('path'); const path = require('path');
const { execFileSync } = require('child_process'); const { execFileSync } = require('child_process');
const { toCursorAgentRelativePath } = require('./cursor-agent-names');
const { LEGACY_INSTALL_TARGETS, parseInstallArgs } = require('./install/request'); const { LEGACY_INSTALL_TARGETS, parseInstallArgs } = require('./install/request');
const { const {
SUPPORTED_INSTALL_TARGETS, SUPPORTED_INSTALL_TARGETS,
@ -154,7 +155,13 @@ function addRecursiveCopyOperations(operations, options) {
for (const relativeFile of relativeFiles) { for (const relativeFile of relativeFiles) {
const sourceRelativePath = path.join(options.sourceRelativeDir, relativeFile); const sourceRelativePath = path.join(options.sourceRelativeDir, relativeFile);
const sourcePath = path.join(options.sourceRoot, sourceRelativePath); const sourcePath = path.join(options.sourceRoot, sourceRelativePath);
const destinationPath = path.join(options.destinationDir, relativeFile); const destinationRelativePath = typeof options.destinationRelativePathTransform === 'function'
? options.destinationRelativePathTransform(relativeFile, sourceRelativePath)
: relativeFile;
if (!destinationRelativePath) {
continue;
}
const destinationPath = path.join(options.destinationDir, destinationRelativePath);
operations.push(buildCopyFileOperation({ operations.push(buildCopyFileOperation({
moduleId: options.moduleId, moduleId: options.moduleId,
sourcePath, sourcePath,
@ -351,6 +358,7 @@ function planCursorLegacyInstall(context) {
sourceRoot: context.sourceRoot, sourceRoot: context.sourceRoot,
sourceRelativeDir: path.join('.cursor', 'agents'), sourceRelativeDir: path.join('.cursor', 'agents'),
destinationDir: path.join(targetRoot, 'agents'), destinationDir: path.join(targetRoot, 'agents'),
destinationRelativePathTransform: toCursorAgentRelativePath,
}); });
addRecursiveCopyOperations(operations, { addRecursiveCopyOperations(operations, {
moduleId: 'legacy-cursor-install', moduleId: 'legacy-cursor-install',

View File

@ -1,7 +1,9 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { toCursorAgentFileName } = require('../cursor-agent-names');
const { const {
createFlatFileOperations,
createFlatRuleOperations, createFlatRuleOperations,
createInstallTargetAdapter, createInstallTargetAdapter,
createManagedOperation, createManagedOperation,
@ -149,6 +151,16 @@ module.exports = createInstallTargetAdapter({
})); }));
} }
if (sourceRelativePath === 'agents') {
return takeUniqueOperations(createFlatFileOperations({
moduleId: module.id,
repoRoot,
sourceRelativePath,
destinationDir: path.join(targetRoot, 'agents'),
destinationNameTransform: toCursorAgentFileName,
}));
}
if (sourceRelativePath === '.cursor') { if (sourceRelativePath === '.cursor') {
const cursorRoot = path.join(repoRoot, '.cursor'); const cursorRoot = path.join(repoRoot, '.cursor');
if (!fs.existsSync(cursorRoot) || !fs.statSync(cursorRoot).isDirectory()) { if (!fs.existsSync(cursorRoot) || !fs.statSync(cursorRoot).isDirectory()) {

View File

@ -181,7 +181,7 @@ function createNamespacedFlatRuleOperations(adapter, moduleId, sourceRelativePat
return operations; return operations;
} }
function createFlatRuleOperations({ function createFlatFileOperations({
moduleId, moduleId,
repoRoot, repoRoot,
sourceRelativePath, sourceRelativePath,
@ -242,6 +242,10 @@ function createFlatRuleOperations({
return operations; return operations;
} }
function createFlatRuleOperations(options) {
return createFlatFileOperations(options);
}
function createInstallTargetAdapter(config) { function createInstallTargetAdapter(config) {
const adapter = { const adapter = {
id: config.id, id: config.id,
@ -342,6 +346,7 @@ function createInstallTargetAdapter(config) {
module.exports = { module.exports = {
buildValidationIssue, buildValidationIssue,
createFlatFileOperations,
createFlatRuleOperations, createFlatRuleOperations,
createInstallTargetAdapter, createInstallTargetAdapter,
createManagedOperation, createManagedOperation,

View File

@ -213,7 +213,10 @@ function runTests() {
assert.strictEqual(plan.installRoot, targetRoot); assert.strictEqual(plan.installRoot, targetRoot);
assert.ok(operationFor(plan, path.join('.cursor', 'rules', 'common-style.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'rules', 'common-style.md')));
assert.ok(operationFor(plan, path.join('.cursor', 'rules', 'typescript-style.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'rules', 'typescript-style.md')));
assert.ok(operationFor(plan, path.join('.cursor', 'agents', 'planner.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'agents', 'ecc-planner.md')));
assert.ok(!plan.operations.some(operation => (
operation.destinationPath.endsWith(path.join('.cursor', 'agents', 'planner.md'))
)));
assert.ok(operationFor(plan, path.join('.cursor', 'skills', 'demo', 'SKILL.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'skills', 'demo', 'SKILL.md')));
assert.ok(operationFor(plan, path.join('.cursor', 'commands', 'plan.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'commands', 'plan.md')));
assert.ok(operationFor(plan, path.join('.cursor', 'hooks', 'hook.js'))); assert.ok(operationFor(plan, path.join('.cursor', 'hooks', 'hook.js')));

View File

@ -202,6 +202,44 @@ function runTests() {
); );
})) passed++; else failed++; })) passed++; else failed++;
if (test('plans cursor agents with ecc-prefixed filenames to avoid agent collisions', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules: [
{
id: 'agents-core',
paths: ['agents'],
},
],
});
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'agents/architect.md'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'agents', 'ecc-architect.md')
)),
'Should prefix Cursor agent files with ecc-'
);
assert.ok(
!plan.operations.some(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'agents', 'architect.md')
)),
'Should not write bare Cursor agent filenames'
);
assert.ok(
!plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'agents'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'agents')
)),
'Should not plan a whole-directory Cursor agent copy'
);
})) passed++; else failed++;
if (test('plans cursor platform rule files as .mdc and excludes rule README docs', () => { if (test('plans cursor platform rule files as .mdc and excludes rule README docs', () => {
const repoRoot = path.join(__dirname, '..', '..'); const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app'; const projectRoot = '/workspace/app';

View File

@ -136,7 +136,8 @@ function runTests() {
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.mdc'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.mdc')));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.md'))); assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.md')));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'README.mdc'))); assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'README.mdc')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'architect.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'ecc-architect.md')));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'commands', 'plan.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'mcp.json'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'mcp.json')));

View File

@ -93,6 +93,21 @@ function runTests() {
); );
})) passed++; else failed++; })) passed++; else failed++;
if (test('README documents Cursor agent namespace and loading caveat', () => {
assert.ok(
readme.includes('`.cursor/agents/ecc-*.md`'),
'README should document the Cursor agent namespace'
);
assert.ok(
readme.includes('Cursor-native loading behavior can vary by Cursor build.'),
'README should avoid overclaiming Cursor agent loading semantics'
);
assert.ok(
readme.includes('ECC does not install root `AGENTS.md` into `.cursor/`.'),
'README should explain why root AGENTS.md is not copied into Cursor context'
);
})) passed++; else failed++;
if (test('README explains plugin-path cleanup and rules scoping', () => { if (test('README explains plugin-path cleanup and rules scoping', () => {
assert.ok( assert.ok(
readme.includes('remove the plugin from Claude Code'), readme.includes('remove the plugin from Claude Code'),