diff --git a/README.md b/README.md
index 958a4b5..38aa9d2 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@
npx @colbymchenry/codegraph
```
-Interactive installer configures Claude Code automatically
+Interactive installer configures Claude Code and Cursor automatically
@@ -69,35 +69,35 @@ We ran the same complex task 3 times with and without CodeGraph:
### 🔄 How It Works
```
-┌─────────────────────────────────────────────────────────────────┐
+┌──────────────────────────────────────────────────────────────────┐
│ Claude Code │
│ │
│ "Implement user authentication" │
│ │ │
│ ▼ │
-│ ┌─────────────────┐ ┌─────────────────┐ │
-│ │ Explore Agent │ ──── │ Explore Agent │ │
-│ └────────┬────────┘ └────────┬────────┘ │
+│ ┌─────────────────┐ ┌─────────────────┐ │
+│ │ Explore Agent │ ──── │ Explore Agent │ │
+│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
└───────────┼────────────────────────┼─────────────────────────────┘
│ │
▼ ▼
-┌───────────────────────────────────────────────────────────────────┐
-│ CodeGraph MCP Server │
+┌──────────────────────────────────────────────────────────────────┐
+│ CodeGraph MCP Server │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Search │ │ Callers │ │ Context │ │
│ │ "auth" │ │ "login()" │ │ for task │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
-│ │ │ │ │
-│ └────────────────┼────────────────┘ │
-│ ▼ │
-│ ┌───────────────────────┐ │
-│ │ SQLite Graph DB │ │
-│ │ • 387 symbols │ │
-│ │ • 1,204 edges │ │
-│ │ • Instant lookups │ │
-│ └───────────────────────┘ │
-└───────────────────────────────────────────────────────────────────┘
+│ │ │ │ │
+│ └────────────────┼────────────────┘ │
+│ ▼ │
+│ ┌───────────────────────┐ │
+│ │ SQLite Graph DB │ │
+│ │ • 387 symbols │ │
+│ │ • 1,204 edges │ │
+│ │ • Instant lookups │ │
+│ └───────────────────────┘ │
+└──────────────────────────────────────────────────────────────────┘
```
**Without CodeGraph:** Explore agents use `grep`, `glob`, and `Read` to scan files → many API calls, high token usage
@@ -162,15 +162,16 @@ npx @colbymchenry/codegraph
```
The interactive installer will:
-- Configure the MCP server in `~/.claude.json`
-- Set up auto-allow permissions for CodeGraph tools
-- Add global instructions to `~/.claude/CLAUDE.md` (teaches Claude how to use CodeGraph)
+- Ask which IDE(s) to configure (Claude Code, Cursor, or both)
+- Configure the MCP server (`.claude.json` and/or `.cursor/mcp.json`)
+- Set up auto-allow permissions for CodeGraph tools (Claude Code)
+- Add instructions to teach your IDE how to use CodeGraph
- Install Claude Code hooks for automatic index syncing
- Optionally initialize your current project
-### 2. Restart Claude Code
+### 2. Restart Your IDE
-Restart Claude Code for the MCP server to load.
+Restart Claude Code or Cursor for the MCP server to load.
### 3. Initialize Projects
@@ -181,7 +182,9 @@ cd your-project
codegraph init -i
```
-That's it! Claude Code will now use CodeGraph tools automatically when a `.codegraph/` directory exists.
+That's it! Your IDE will now use CodeGraph tools automatically when a `.codegraph/` directory exists.
+
+> **Note for Cursor users:** CodeGraph MCP tools are only available in **Agent mode**, not in Composer.
Manual Setup (Alternative)
@@ -193,12 +196,11 @@ If you prefer manual configuration:
npm install -g @colbymchenry/codegraph
```
-**Add to `~/.claude.json`:**
+**Add to `~/.claude.json` or `./.cursor/mcp.json`:**
```json
{
"mcpServers": {
"codegraph": {
- "type": "stdio",
"command": "codegraph",
"args": ["serve", "--mcp"]
}
@@ -296,21 +298,34 @@ codegraph serve --mcp # Start MCP server
### `codegraph` / `codegraph install`
-Run the interactive installer for Claude Code integration. Configures MCP server and permissions automatically.
+Run the interactive installer for IDE integration. Configures MCP server and permissions automatically.
```bash
-codegraph # Run installer (when no args)
-codegraph install # Run installer (explicit)
-npx @colbymchenry/codegraph # Run via npx (no global install needed)
-```
-
-The installer will:
-1. Ask for installation location (global `~/.claude` or local `./.claude`)
-2. Configure the MCP server in `claude.json`
-3. Optionally set up auto-allow permissions
-4. Add global instructions to `~/.claude/CLAUDE.md` (teaches Claude how to use CodeGraph)
-5. Install Claude Code hooks for automatic index syncing
-6. For local installs: initialize and index the current project
+# Interactive mode (prompts for IDE selection and location)
+codegraph # Run installer (when no args)
+codegraph install # Run installer (explicit)
+npx @colbymchenry/codegraph # Run via npx (no global install needed)
+
+# Non-interactive mode (skip prompts)
+codegraph install --ide=claude --location=local # Claude Code only (local)
+codegraph install --ide=cursor # Cursor only (always local)
+codegraph install --ide=all --location=local # Both IDEs (local)
+codegraph install --ide=claude,cursor # Both IDEs (prompts for location)
+```
+
+**Interactive installer flow:**
+1. Asks which IDE(s) to configure (Claude Code, Cursor, or both)
+2. Asks for installation location (global or local) - defaults to local
+3. Configures the MCP server (`.claude.json` and/or `.cursor/mcp.json`)
+4. Sets up auto-allow permissions (Claude Code only)
+5. Adds instructions to teach your IDE how to use CodeGraph
+6. Installs Claude Code hooks for automatic index syncing
+7. For local installs: optionally initializes and indexes the current project
+
+**Non-interactive mode:**
+- Useful for CI/CD pipelines and scripts
+- Auto-detects non-TTY shells and uses defaults
+- Can specify IDE(s) and location via flags to skip all prompts
### `codegraph init [path]`
diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts
index e2e24d1..a5f740c 100644
--- a/__tests__/installer.test.ts
+++ b/__tests__/installer.test.ts
@@ -16,10 +16,23 @@ import {
writeMcpConfig,
writePermissions,
writeClaudeMd,
- hasMcpConfig,
+ writeHooks,
+ hasClaudeMcpConfig,
hasPermissions,
hasClaudeMdSection,
+ writeCursorMcpConfig,
+ writeCursorRules,
+ hasCursorMcpConfig,
} from '../src/installer/config-writer';
+import {
+ isInteractive,
+ promptIDE,
+ promptInstallLocation,
+ DEFAULT_IDE,
+ DEFAULT_LOCATION,
+} from '../src/installer/prompts';
+import { parseIDEArg, validateLocation } from '../src/installer/index';
+import type { InstallLocation } from '../src/installer/prompts';
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-installer-test-'));
@@ -31,6 +44,113 @@ function cleanupTempDir(dir: string): void {
}
}
+/**
+ * E2E Test helper utilities
+ */
+class InstallerTestHelper {
+ private origCwd: string;
+ private tempDir: string;
+ private origHome: string | undefined;
+
+ constructor() {
+ this.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-e2e-'));
+ this.origCwd = process.cwd();
+ this.origHome = process.env.HOME;
+ }
+
+ /**
+ * Setup test environment
+ */
+ setup() {
+ process.chdir(this.tempDir);
+ // Override HOME for global installs during tests
+ process.env.HOME = this.tempDir;
+ }
+
+ /**
+ * Cleanup test environment
+ */
+ cleanup() {
+ process.chdir(this.origCwd);
+ if (this.origHome !== undefined) {
+ process.env.HOME = this.origHome;
+ }
+ if (fs.existsSync(this.tempDir)) {
+ fs.rmSync(this.tempDir, { recursive: true, force: true });
+ }
+ }
+
+ /**
+ * Verify Claude Code installation files
+ */
+ verifyClaudeInstall(location: InstallLocation) {
+ const baseDir = location === 'global' ? this.tempDir : process.cwd();
+
+ // Check .claude.json
+ const claudeJsonPath = path.join(baseDir, '.claude.json');
+ expect(fs.existsSync(claudeJsonPath), `${claudeJsonPath} should exist`).toBe(true);
+
+ const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8'));
+ expect(claudeJson.mcpServers?.codegraph, 'MCP server config should exist').toBeDefined();
+ expect(claudeJson.mcpServers.codegraph.command).toBe('codegraph');
+ expect(claudeJson.mcpServers.codegraph.args).toEqual(['serve', '--mcp']);
+
+ // Check .claude/settings.json (for hooks)
+ const settingsPath = path.join(baseDir, '.claude', 'settings.json');
+ expect(fs.existsSync(settingsPath), `${settingsPath} should exist`).toBe(true);
+
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
+ expect(settings.hooks, 'Hooks should exist').toBeDefined();
+ expect(JSON.stringify(settings.hooks)).toContain('codegraph');
+
+ // Check CLAUDE.md
+ const claudeMdPath = path.join(baseDir, '.claude', 'CLAUDE.md');
+ expect(fs.existsSync(claudeMdPath), `${claudeMdPath} should exist`).toBe(true);
+
+ const claudeMd = fs.readFileSync(claudeMdPath, 'utf-8');
+ expect(claudeMd).toContain('## CodeGraph');
+ expect(claudeMd).toContain('');
+ expect(claudeMd).toContain('codegraph_search');
+ }
+
+ /**
+ * Verify Cursor installation files
+ */
+ verifyCursorInstall() {
+ // Check .cursor/mcp.json
+ const mcpJsonPath = path.join(process.cwd(), '.cursor', 'mcp.json');
+ expect(fs.existsSync(mcpJsonPath), `${mcpJsonPath} should exist`).toBe(true);
+
+ const mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
+ expect(mcpJson.mcpServers?.codegraph, 'MCP server config should exist').toBeDefined();
+ expect(mcpJson.mcpServers.codegraph.command).toBe('codegraph');
+
+ // Check .cursor/rules/codegraph.md
+ const rulesPath = path.join(process.cwd(), '.cursor', 'rules', 'codegraph.md');
+ expect(fs.existsSync(rulesPath), `${rulesPath} should exist`).toBe(true);
+
+ const rules = fs.readFileSync(rulesPath, 'utf-8');
+ expect(rules).toContain('## CodeGraph');
+ expect(rules).toContain('codegraph_search');
+ expect(rules).toContain('Agent mode');
+ expect(rules).not.toContain(''); // No markers for Cursor
+ }
+
+ /**
+ * Verify files do NOT exist
+ */
+ verifyClaudeNotInstalled(location: InstallLocation) {
+ const baseDir = location === 'global' ? this.tempDir : process.cwd();
+ const claudeJsonPath = path.join(baseDir, '.claude.json');
+ expect(fs.existsSync(claudeJsonPath)).toBe(false);
+ }
+
+ verifyCursorNotInstalled() {
+ const mcpJsonPath = path.join(process.cwd(), '.cursor', 'mcp.json');
+ expect(fs.existsSync(mcpJsonPath)).toBe(false);
+ }
+}
+
describe('Installer Config Writer', () => {
let origCwd: string;
let tempDir: string;
@@ -217,3 +337,558 @@ describe('Installer Config Writer', () => {
});
});
});
+
+/**
+ * End-to-End Installer Tests
+ *
+ * Tests all combinations of IDE installations:
+ * - Claude Code only (global)
+ * - Claude Code only (local)
+ * - Cursor only (local)
+ * - Both IDEs (local)
+ */
+describe('Installer E2E Tests', () => {
+ let helper: InstallerTestHelper;
+
+ beforeEach(() => {
+ helper = new InstallerTestHelper();
+ helper.setup();
+ });
+
+ afterEach(() => {
+ helper.cleanup();
+ });
+
+ describe('Claude Code Only - Global', () => {
+ it('should install Claude Code globally', () => {
+ const location: InstallLocation = 'global';
+
+ // Simulate full installation
+ writeMcpConfig(location);
+ writePermissions(location);
+ writeHooks(location);
+ writeClaudeMd(location);
+
+ // Verify installation
+ helper.verifyClaudeInstall(location);
+ helper.verifyCursorNotInstalled();
+
+ // Verify detection
+ expect(hasClaudeMcpConfig(location)).toBe(true);
+ expect(hasCursorMcpConfig()).toBe(false);
+ });
+ });
+
+ describe('Claude Code Only - Local', () => {
+ it('should install Claude Code locally', () => {
+ const location: InstallLocation = 'local';
+
+ // Simulate full installation
+ writeMcpConfig(location);
+ writePermissions(location);
+ writeHooks(location);
+ writeClaudeMd(location);
+
+ // Verify installation
+ helper.verifyClaudeInstall(location);
+ helper.verifyCursorNotInstalled();
+
+ // Verify detection
+ expect(hasClaudeMcpConfig(location)).toBe(true);
+ expect(hasCursorMcpConfig()).toBe(false);
+ });
+ });
+
+ describe('Cursor Only', () => {
+ it('should install Cursor locally', () => {
+ // Cursor only supports local
+ writeCursorMcpConfig();
+ writeCursorRules();
+
+ // Verify installation
+ helper.verifyCursorInstall();
+ helper.verifyClaudeNotInstalled('local');
+ helper.verifyClaudeNotInstalled('global');
+
+ // Verify detection
+ expect(hasCursorMcpConfig()).toBe(true);
+ expect(hasClaudeMcpConfig('local')).toBe(false);
+ });
+ });
+
+ describe('Both IDEs - Local', () => {
+ it('should install both Claude Code and Cursor locally', () => {
+ const location: InstallLocation = 'local';
+
+ // Install Claude Code
+ writeMcpConfig(location);
+ writePermissions(location);
+ writeHooks(location);
+ writeClaudeMd(location);
+
+ // Install Cursor
+ writeCursorMcpConfig();
+ writeCursorRules();
+
+ // Verify both installations
+ helper.verifyClaudeInstall(location);
+ helper.verifyCursorInstall();
+
+ // Verify detection
+ expect(hasClaudeMcpConfig(location)).toBe(true);
+ expect(hasCursorMcpConfig()).toBe(true);
+ });
+ });
+
+ describe('Update Scenarios', () => {
+ it('should update existing Claude Code installation', () => {
+ const location: InstallLocation = 'local';
+
+ // First install
+ writeMcpConfig(location);
+ writeClaudeMd(location);
+
+ const claudeMdPath = path.join(process.cwd(), '.claude', 'CLAUDE.md');
+ const firstContent = fs.readFileSync(claudeMdPath, 'utf-8');
+
+ // Second install (update)
+ writeMcpConfig(location);
+ writeClaudeMd(location);
+
+ const secondContent = fs.readFileSync(claudeMdPath, 'utf-8');
+
+ // Content should be updated but structure preserved
+ expect(secondContent).toContain('');
+ expect(secondContent).toContain('## CodeGraph');
+ });
+
+ it('should update existing Cursor installation', () => {
+ // First install
+ writeCursorMcpConfig();
+ writeCursorRules();
+
+ const rulesPath = path.join(process.cwd(), '.cursor', 'rules', 'codegraph.md');
+ const firstContent = fs.readFileSync(rulesPath, 'utf-8');
+
+ // Second install (update)
+ writeCursorRules();
+
+ const secondContent = fs.readFileSync(rulesPath, 'utf-8');
+
+ // Should overwrite with latest template
+ expect(secondContent).toContain('## CodeGraph');
+ expect(secondContent.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Detection Scenarios', () => {
+ it('should detect no IDEs when starting fresh', () => {
+ // Verify clean slate - no IDEs detected
+ expect(hasClaudeMcpConfig('local')).toBe(false);
+ expect(hasClaudeMcpConfig('global')).toBe(false);
+ expect(hasCursorMcpConfig()).toBe(false);
+
+ // Verify no config directories exist
+ expect(fs.existsSync(path.join(process.cwd(), '.claude'))).toBe(false);
+ expect(fs.existsSync(path.join(process.cwd(), '.cursor'))).toBe(false);
+ expect(fs.existsSync(path.join(helper['tempDir'], '.claude'))).toBe(false);
+ });
+
+ it('should detect Claude Code after installation', () => {
+ const location: InstallLocation = 'local';
+
+ // Initially not detected
+ expect(hasClaudeMcpConfig(location)).toBe(false);
+
+ // Install
+ writeMcpConfig(location);
+
+ // Now detected
+ expect(hasClaudeMcpConfig(location)).toBe(true);
+ });
+
+ it('should detect Cursor after installation', () => {
+ // Initially not detected
+ expect(hasCursorMcpConfig()).toBe(false);
+
+ // Install
+ writeCursorMcpConfig();
+
+ // Now detected
+ expect(hasCursorMcpConfig()).toBe(true);
+ });
+
+ it('should detect both IDEs independently', () => {
+ const location: InstallLocation = 'local';
+
+ // Install Claude Code first
+ writeMcpConfig(location);
+ expect(hasClaudeMcpConfig(location)).toBe(true);
+ expect(hasCursorMcpConfig()).toBe(false);
+
+ // Install Cursor second
+ writeCursorMcpConfig();
+ expect(hasClaudeMcpConfig(location)).toBe(true);
+ expect(hasCursorMcpConfig()).toBe(true);
+ });
+ });
+
+ describe('Permissions and Hooks', () => {
+ it('should add permissions to Claude Code settings', () => {
+ const location: InstallLocation = 'local';
+
+ writePermissions(location);
+
+ const settingsPath = path.join(process.cwd(), '.claude', 'settings.json');
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
+
+ expect(settings.permissions).toBeDefined();
+ expect(settings.permissions.allow).toBeInstanceOf(Array);
+ expect(settings.permissions.allow).toContain('mcp__codegraph__codegraph_search');
+ expect(settings.permissions.allow).toContain('mcp__codegraph__codegraph_context');
+ });
+
+ it('should add hooks to Claude Code settings', () => {
+ const location: InstallLocation = 'local';
+
+ writeHooks(location);
+
+ const settingsPath = path.join(process.cwd(), '.claude', 'settings.json');
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
+
+ expect(settings.hooks).toBeDefined();
+ expect(settings.hooks.PostToolUse).toBeDefined();
+ expect(settings.hooks.Stop).toBeDefined();
+
+ const hooksJson = JSON.stringify(settings.hooks);
+ expect(hooksJson).toContain('codegraph mark-dirty');
+ expect(hooksJson).toContain('codegraph sync-if-dirty');
+ });
+ });
+});
+
+/**
+ * Non-Interactive Installer Tests
+ *
+ * Tests CLI argument parsing for --ide flag
+ */
+describe('Installer Non-Interactive Mode', () => {
+ let helper: InstallerTestHelper;
+
+ beforeEach(() => {
+ helper = new InstallerTestHelper();
+ helper.setup();
+ });
+
+ afterEach(() => {
+ helper.cleanup();
+ });
+
+ describe('IDE Argument Parsing', () => {
+ it('should parse single IDE: claude', () => {
+ const location: InstallLocation = 'local';
+
+ // Simulate --ide=claude
+ writeMcpConfig(location);
+ writePermissions(location);
+ writeHooks(location);
+ writeClaudeMd(location);
+
+ helper.verifyClaudeInstall(location);
+ helper.verifyCursorNotInstalled();
+ });
+
+ it('should parse single IDE: cursor', () => {
+ // Simulate --ide=cursor
+ writeCursorMcpConfig();
+ writeCursorRules();
+
+ helper.verifyCursorInstall();
+ helper.verifyClaudeNotInstalled('local');
+ });
+
+ it('should parse comma-separated IDEs: claude,cursor', () => {
+ const location: InstallLocation = 'local';
+
+ // Simulate --ide=claude,cursor
+ writeMcpConfig(location);
+ writePermissions(location);
+ writeHooks(location);
+ writeClaudeMd(location);
+ writeCursorMcpConfig();
+ writeCursorRules();
+
+ helper.verifyClaudeInstall(location);
+ helper.verifyCursorInstall();
+ });
+
+ it('should parse "all" as both IDEs', () => {
+ const location: InstallLocation = 'local';
+
+ // Simulate --ide=all
+ writeMcpConfig(location);
+ writePermissions(location);
+ writeHooks(location);
+ writeClaudeMd(location);
+ writeCursorMcpConfig();
+ writeCursorRules();
+
+ helper.verifyClaudeInstall(location);
+ helper.verifyCursorInstall();
+ });
+ });
+
+ describe('Location Argument Parsing', () => {
+ it('should install Claude Code globally with --location=global', () => {
+ const location: InstallLocation = 'global';
+
+ // Simulate --ide=claude --location=global
+ writeMcpConfig(location);
+ writePermissions(location);
+ writeHooks(location);
+ writeClaudeMd(location);
+
+ helper.verifyClaudeInstall(location);
+ });
+
+ it('should install Claude Code locally with --location=local', () => {
+ const location: InstallLocation = 'local';
+
+ // Simulate --ide=claude --location=local
+ writeMcpConfig(location);
+ writePermissions(location);
+ writeHooks(location);
+ writeClaudeMd(location);
+
+ helper.verifyClaudeInstall(location);
+ });
+
+ it('should install all IDEs locally with --ide=all --location=local', () => {
+ const location: InstallLocation = 'local';
+
+ // Simulate --ide=all --location=local
+ writeMcpConfig(location);
+ writePermissions(location);
+ writeHooks(location);
+ writeClaudeMd(location);
+ writeCursorMcpConfig();
+ writeCursorRules();
+
+ helper.verifyClaudeInstall(location);
+ helper.verifyCursorInstall();
+ });
+
+ it('should install all IDEs globally with --ide=all --location=global', () => {
+ const location: InstallLocation = 'global';
+
+ // Simulate --ide=all --location=global
+ writeMcpConfig(location);
+ writePermissions(location);
+ writeHooks(location);
+ writeClaudeMd(location);
+ writeCursorMcpConfig();
+ writeCursorRules();
+
+ helper.verifyClaudeInstall(location);
+ helper.verifyCursorInstall();
+ });
+ });
+});
+
+/**
+ * isInteractive() Tests
+ */
+describe('isInteractive', () => {
+ let origStdinTTY: true | undefined;
+ let origStdoutTTY: true | undefined;
+
+ beforeEach(() => {
+ origStdinTTY = process.stdin.isTTY;
+ origStdoutTTY = process.stdout.isTTY;
+ });
+
+ afterEach(() => {
+ // @ts-expect-error — restoring possibly-undefined TTY flag
+ process.stdin.isTTY = origStdinTTY;
+ // @ts-expect-error
+ process.stdout.isTTY = origStdoutTTY;
+ });
+
+ it('returns false when stdin is not a TTY (typical CI/test env)', () => {
+ // @ts-expect-error
+ process.stdin.isTTY = undefined;
+ // @ts-expect-error
+ process.stdout.isTTY = true;
+ expect(isInteractive()).toBe(false);
+ });
+
+ it('returns false when stdout is not a TTY', () => {
+ // @ts-expect-error
+ process.stdin.isTTY = true;
+ // @ts-expect-error
+ process.stdout.isTTY = undefined;
+ expect(isInteractive()).toBe(false);
+ });
+
+ it('returns false when both stdin and stdout are not TTYs', () => {
+ // @ts-expect-error
+ process.stdin.isTTY = undefined;
+ // @ts-expect-error
+ process.stdout.isTTY = undefined;
+ expect(isInteractive()).toBe(false);
+ });
+
+ it('returns true when both stdin and stdout are TTYs', () => {
+ // @ts-expect-error
+ process.stdin.isTTY = true;
+ // @ts-expect-error
+ process.stdout.isTTY = true;
+ expect(isInteractive()).toBe(true);
+ });
+});
+
+/**
+ * promptIDE() non-interactive behavior
+ *
+ * Tests run without a TTY so isInteractive() returns false,
+ * exercising the non-interactive code path in promptIDE().
+ */
+describe('promptIDE non-interactive', () => {
+ let origCwd: string;
+ let origHome: string | undefined;
+ let tempDir: string;
+
+ beforeEach(() => {
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-prompt-test-'));
+ origCwd = process.cwd();
+ origHome = process.env.HOME;
+ process.chdir(tempDir);
+ process.env.HOME = tempDir;
+ });
+
+ afterEach(() => {
+ process.chdir(origCwd);
+ if (origHome !== undefined) process.env.HOME = origHome;
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ });
+
+ it('returns [DEFAULT_IDE] when no IDEs are detected', async () => {
+ const result = await promptIDE();
+ expect(result).toEqual([DEFAULT_IDE]);
+ });
+
+ it('returns [claude] when Claude config directory exists', async () => {
+ // detectInstalledIDEs checks for .claude dir in cwd or homedir
+ fs.mkdirSync(path.join(tempDir, '.claude'), { recursive: true });
+ const result = await promptIDE();
+ expect(result).toContain('claude');
+ });
+
+ it('returns [cursor] when Cursor config directory exists', async () => {
+ fs.mkdirSync(path.join(tempDir, '.cursor'), { recursive: true });
+ const result = await promptIDE();
+ expect(result).toContain('cursor');
+ });
+
+ it('returns both IDEs when both config directories exist', async () => {
+ fs.mkdirSync(path.join(tempDir, '.claude'), { recursive: true });
+ fs.mkdirSync(path.join(tempDir, '.cursor'), { recursive: true });
+ const result = await promptIDE();
+ expect(result).toContain('claude');
+ expect(result).toContain('cursor');
+ });
+});
+
+/**
+ * promptInstallLocation() non-interactive behavior
+ *
+ * Tests run without a TTY so the non-interactive path is taken.
+ */
+describe('promptInstallLocation non-interactive', () => {
+ it('returns DEFAULT_LOCATION for claude-only', async () => {
+ const result = await promptInstallLocation(['claude']);
+ expect(result).toBe(DEFAULT_LOCATION);
+ expect(result).toBe('local');
+ });
+
+ it('returns local for cursor-only (always local)', async () => {
+ const result = await promptInstallLocation(['cursor']);
+ expect(result).toBe('local');
+ });
+
+ it('returns DEFAULT_LOCATION for both IDEs', async () => {
+ const result = await promptInstallLocation(['claude', 'cursor']);
+ expect(result).toBe(DEFAULT_LOCATION);
+ });
+});
+
+/**
+ * parseIDEArg() Tests
+ */
+describe('parseIDEArg', () => {
+ it('parses "claude" → ["claude"]', () => {
+ expect(parseIDEArg('claude')).toEqual(['claude']);
+ });
+
+ it('parses "cursor" → ["cursor"]', () => {
+ expect(parseIDEArg('cursor')).toEqual(['cursor']);
+ });
+
+ it('parses "all" → ["claude", "cursor"]', () => {
+ expect(parseIDEArg('all')).toEqual(['claude', 'cursor']);
+ });
+
+ it('parses "claude,cursor" → ["claude", "cursor"]', () => {
+ expect(parseIDEArg('claude,cursor')).toEqual(['claude', 'cursor']);
+ });
+
+ it('is case-insensitive', () => {
+ expect(parseIDEArg('Claude')).toEqual(['claude']);
+ expect(parseIDEArg('CURSOR')).toEqual(['cursor']);
+ expect(parseIDEArg('ALL')).toEqual(['claude', 'cursor']);
+ });
+
+ it('trims whitespace in comma-separated values', () => {
+ expect(parseIDEArg(' claude , cursor ')).toEqual(['claude', 'cursor']);
+ });
+
+ it('throws for an unrecognized IDE name', () => {
+ expect(() => parseIDEArg('vscode')).toThrow(/Invalid IDE/);
+ });
+
+ it('throws when all values are unrecognized', () => {
+ expect(() => parseIDEArg('vscode,vim')).toThrow(/Invalid IDE/);
+ });
+
+ it('includes only valid IDEs from a mixed list', () => {
+ // "claude" is valid, "vscode" is not — valid ones are kept, but if zero valid → throws
+ expect(parseIDEArg('claude,vscode')).toEqual(['claude']);
+ });
+});
+
+/**
+ * validateLocation() Tests
+ */
+describe('validateLocation', () => {
+ it('returns "global" for "global"', () => {
+ expect(validateLocation('global')).toBe('global');
+ });
+
+ it('returns "local" for "local"', () => {
+ expect(validateLocation('local')).toBe('local');
+ });
+
+ it('is case-insensitive', () => {
+ expect(validateLocation('Global')).toBe('global');
+ expect(validateLocation('LOCAL')).toBe('local');
+ });
+
+ it('returns undefined for undefined', () => {
+ expect(validateLocation(undefined)).toBeUndefined();
+ });
+
+ it('throws for an invalid location', () => {
+ expect(() => validateLocation('project')).toThrow(/Invalid location/);
+ expect(() => validateLocation('home')).toThrow(/Invalid location/);
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index f0702e5..0a78489 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2317,7 +2317,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts
index d9f8e47..cc285ef 100644
--- a/src/bin/codegraph.ts
+++ b/src/bin/codegraph.ts
@@ -5,18 +5,23 @@
* Command-line interface for CodeGraph code intelligence.
*
* Usage:
- * codegraph Run interactive installer (when no args)
- * codegraph install Run interactive installer
- * codegraph init [path] Initialize CodeGraph in a project
- * codegraph uninit [path] Remove CodeGraph from a project
- * codegraph index [path] Index all files in the project
- * codegraph sync [path] Sync changes since last index
- * codegraph status [path] Show index status
- * codegraph query Search for symbols
- * codegraph files [options] Show project file structure
- * codegraph context Build context for a task
- * codegraph mark-dirty [path] Mark project as needing sync (hooks)
- * codegraph sync-if-dirty [path] Sync if marked dirty (hooks)
+ * codegraph Run interactive installer (when no args)
+ * codegraph install Run interactive installer
+ * codegraph install --ide=claude Install for Claude Code (prompts for location)
+ * codegraph install --ide=claude --location=global Install for Claude Code globally
+ * codegraph install --ide=claude --location=local Install for Claude Code locally
+ * codegraph install --ide=cursor Install for Cursor only (always local)
+ * codegraph install --ide=all --location=local Install for all IDEs locally
+ * codegraph init [path] Initialize CodeGraph in a project
+ * codegraph uninit [path] Remove CodeGraph from a project
+ * codegraph index [path] Index all files in the project
+ * codegraph sync [path] Sync changes since last index
+ * codegraph status [path] Show index status
+ * codegraph query Search for symbols
+ * codegraph files [options] Show project file structure
+ * codegraph context Build context for a task
+ * codegraph mark-dirty [path] Mark project as needing sync (hooks)
+ * codegraph sync-if-dirty [path] Sync if marked dirty (hooks)
*
* Note: Git hooks have been removed. CodeGraph sync is triggered automatically
* through codegraph's Claude Code hooks integration.
@@ -56,7 +61,7 @@ initSentry({ processName: 'codegraph-cli', version: pkgVersion });
if (process.argv.length === 2) {
import('../installer').then(({ runInstaller }) =>
- runInstaller()
+ runInstaller(undefined)
).catch((err) => {
captureException(err);
console.error('Installation failed:', err instanceof Error ? err.message : String(err));
@@ -1041,10 +1046,15 @@ program
*/
program
.command('install')
- .description('Run interactive installer for Claude Code integration')
- .action(async () => {
+ .description('Run interactive installer for IDE integration')
+ .option('--ide ', 'IDE(s) to configure (comma-separated: claude,cursor or "all")')
+ .option('--location ', 'Installation location for Claude Code (global or local)')
+ .action(async (options: { ide?: string; location?: string }) => {
const { runInstaller } = await import('../installer');
- await runInstaller();
+ const installerOptions = options.ide || options.location
+ ? { ide: options.ide, location: options.location as 'global' | 'local' | undefined }
+ : undefined;
+ await runInstaller(installerOptions);
});
// Parse and run
diff --git a/src/installer/banner.ts b/src/installer/banner.ts
index fb70bf3..75068a3 100644
--- a/src/installer/banner.ts
+++ b/src/installer/banner.ts
@@ -77,7 +77,7 @@ export function showBanner(): void {
console.log(chalk.cyan(banner));
console.log();
console.log(` ${chalk.bold('CodeGraph')} v${getVersion()}`);
- console.log(' Semantic code intelligence for Claude Code');
+ console.log(' Semantic code intelligence for Claude Code & Cursor');
console.log(chalk.dim(' Created by: Colby McHenry'));
console.log();
}
@@ -113,9 +113,20 @@ export function warn(message: string): void {
/**
* Show the "next steps" section after installation
*/
-export function showNextSteps(location: 'global' | 'local'): void {
+export function showNextSteps(location: 'global' | 'local', ides: string[] = ['claude']): void {
console.log();
- console.log(chalk.bold(' Done!') + ' Restart Claude Code to use CodeGraph.');
+
+ const hasClaudeCode = ides.includes('claude');
+ const hasCursor = ides.includes('cursor');
+
+ if (hasClaudeCode && hasCursor) {
+ console.log(chalk.bold(' Done!') + ' Restart Claude Code and/or Cursor to use CodeGraph.');
+ } else if (hasCursor) {
+ console.log(chalk.bold(' Done!') + ' Restart Cursor to use CodeGraph MCP tools.');
+ } else {
+ console.log(chalk.bold(' Done!') + ' Restart Claude Code to use CodeGraph.');
+ }
+
console.log();
if (location === 'global') {
@@ -127,6 +138,13 @@ export function showNextSteps(location: 'global' | 'local'): void {
console.log(chalk.dim(' npm uninstall -g @colbymchenry/codegraph'));
} else {
console.log(chalk.dim(' CodeGraph is ready to use in this project!'));
+
+ if (hasCursor) {
+ console.log();
+ console.log(chalk.dim(' Cursor notes:'));
+ console.log(chalk.dim(' - MCP tools are available in Agent mode only (not Composer)'));
+ console.log(chalk.dim(' - Use @ to access codegraph tools in chat'));
+ }
}
console.log();
}
diff --git a/src/installer/config-writer.ts b/src/installer/config-writer.ts
index 2d9e8ab..72bfb6a 100644
--- a/src/installer/config-writer.ts
+++ b/src/installer/config-writer.ts
@@ -1,6 +1,8 @@
/**
* Config file writing for the CodeGraph installer
- * Writes to claude.json, settings.json, and CLAUDE.md
+ * Writes IDE-specific configuration files:
+ * - Claude Code: .claude.json, .claude/settings.json, .claude/CLAUDE.md
+ * - Cursor: .cursor/mcp.json, .cursor/rules/codegraph.md
*/
import * as fs from 'fs';
@@ -8,10 +10,11 @@ import * as path from 'path';
import * as os from 'os';
import { InstallLocation } from './prompts';
import {
- CLAUDE_MD_TEMPLATE,
+ CLAUDE_CODE_TEMPLATE,
CODEGRAPH_SECTION_START,
CODEGRAPH_SECTION_END,
-} from './claude-md-template';
+} from './templates/claude-code';
+import { CURSOR_TEMPLATE } from './templates/cursor';
/**
* Get the path to the Claude config directory
@@ -23,6 +26,14 @@ function getClaudeConfigDir(location: InstallLocation): string {
return path.join(process.cwd(), '.claude');
}
+/**
+ * Get the path to the Cursor config directory
+ * Note: Cursor only supports local configuration
+ */
+function getCursorConfigDir(): string {
+ return path.join(process.cwd(), '.cursor');
+}
+
/**
* Get the path to the claude.json file
* - Global: ~/.claude.json (root level)
@@ -126,6 +137,26 @@ export function writeMcpConfig(location: InstallLocation): void {
writeJsonFile(claudeJsonPath, config);
}
+/**
+ * Write the MCP server configuration to Cursor's mcp.json
+ * Cursor only supports local configuration
+ */
+export function writeCursorMcpConfig(): void {
+ const cursorConfigDir = getCursorConfigDir();
+ const mcpJsonPath = path.join(cursorConfigDir, 'mcp.json');
+ const config = readJsonFile(mcpJsonPath);
+
+ // Ensure mcpServers object exists
+ if (!config.mcpServers) {
+ config.mcpServers = {};
+ }
+
+ // Add or update codegraph server for Cursor
+ config.mcpServers.codegraph = getMcpServerConfig();
+
+ writeJsonFile(mcpJsonPath, config);
+}
+
/**
* Get the list of permissions for CodeGraph tools
*/
@@ -170,14 +201,24 @@ export function writePermissions(location: InstallLocation): void {
}
/**
- * Check if MCP config already exists for CodeGraph
+ * Check if Claude Code MCP config already exists for CodeGraph
*/
-export function hasMcpConfig(location: InstallLocation): boolean {
+export function hasClaudeMcpConfig(location: InstallLocation): boolean {
const claudeJsonPath = getClaudeJsonPath(location);
const config = readJsonFile(claudeJsonPath);
return !!config.mcpServers?.codegraph;
}
+/**
+ * Check if Cursor MCP config already exists for CodeGraph
+ */
+export function hasCursorMcpConfig(): boolean {
+ const cursorConfigDir = getCursorConfigDir();
+ const mcpJsonPath = path.join(cursorConfigDir, 'mcp.json');
+ const config = readJsonFile(mcpJsonPath);
+ return !!config.mcpServers?.codegraph;
+}
+
/**
* Check if permissions already exist for CodeGraph
*/
@@ -324,7 +365,7 @@ export function writeClaudeMd(location: InstallLocation): { created: boolean; up
// Check if file exists
if (!fs.existsSync(claudeMdPath)) {
// Create new file with just the CodeGraph section
- atomicWriteFileSync(claudeMdPath, CLAUDE_MD_TEMPLATE + '\n');
+ atomicWriteFileSync(claudeMdPath, CLAUDE_CODE_TEMPLATE + '\n');
return { created: true, updated: false };
}
@@ -341,7 +382,7 @@ export function writeClaudeMd(location: InstallLocation): { created: boolean; up
// Replace existing marked section
const before = content.substring(0, startIdx);
const after = content.substring(endIdx + CODEGRAPH_SECTION_END.length);
- content = before + CLAUDE_MD_TEMPLATE + after;
+ content = before + CLAUDE_CODE_TEMPLATE + after;
atomicWriteFileSync(claudeMdPath, content);
return { created: false, updated: true };
}
@@ -368,13 +409,58 @@ export function writeClaudeMd(location: InstallLocation): { created: boolean; up
// Replace the section
const before = content.substring(0, sectionStart);
const after = content.substring(sectionEnd);
- content = before + '\n' + CLAUDE_MD_TEMPLATE + after;
+ content = before + '\n' + CLAUDE_CODE_TEMPLATE + after;
atomicWriteFileSync(claudeMdPath, content);
return { created: false, updated: true };
}
// No existing section, append to end
- content = content.trimEnd() + '\n\n' + CLAUDE_MD_TEMPLATE + '\n';
+ content = content.trimEnd() + '\n\n' + CLAUDE_CODE_TEMPLATE + '\n';
atomicWriteFileSync(claudeMdPath, content);
return { created: false, updated: false };
}
+
+/**
+ * Get the path to Cursor rules directory
+ * Note: Cursor only supports local configuration
+ */
+function getCursorRulesDir(): string {
+ return path.join(process.cwd(), '.cursor', 'rules');
+}
+
+/**
+ * Get the path to Cursor CodeGraph rules file
+ */
+function getCursorRulesPath(): string {
+ return path.join(getCursorRulesDir(), 'codegraph.md');
+}
+
+/**
+ * Check if Cursor rules has CodeGraph file
+ */
+export function hasCursorRulesSection(): boolean {
+ const cursorRulesPath = getCursorRulesPath();
+ return fs.existsSync(cursorRulesPath);
+}
+
+/**
+ * Write or update Cursor rules with CodeGraph instructions
+ *
+ * Creates .cursor/rules/codegraph.md file
+ */
+export function writeCursorRules(): { created: boolean; updated: boolean } {
+ const cursorRulesDir = getCursorRulesDir();
+ const cursorRulesPath = getCursorRulesPath();
+
+ // Ensure directory exists
+ if (!fs.existsSync(cursorRulesDir)) {
+ fs.mkdirSync(cursorRulesDir, { recursive: true });
+ }
+
+ const alreadyExists = fs.existsSync(cursorRulesPath);
+
+ // Write the CodeGraph rules file (always overwrite to ensure latest version)
+ atomicWriteFileSync(cursorRulesPath, CURSOR_TEMPLATE + '\n');
+
+ return { created: !alreadyExists, updated: alreadyExists };
+}
diff --git a/src/installer/index.ts b/src/installer/index.ts
index c381489..ab8fa0f 100644
--- a/src/installer/index.ts
+++ b/src/installer/index.ts
@@ -2,13 +2,24 @@
* CodeGraph Interactive Installer
*
* Provides a beautiful interactive CLI experience for setting up CodeGraph
- * with Claude Code.
+ * with supported IDEs (Claude Code, Cursor, etc.).
*/
import { execSync } from 'child_process';
import { showBanner, showNextSteps, success, error, info, chalk } from './banner';
-import { promptInstallLocation, promptAutoAllow, InstallLocation } from './prompts';
-import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks } from './config-writer';
+import { promptIDE, promptInstallLocation, promptAutoAllow, InstallLocation, IDE } from './prompts';
+import {
+ writeMcpConfig,
+ writePermissions,
+ writeClaudeMd,
+ writeHooks,
+ hasClaudeMcpConfig,
+ hasPermissions,
+ hasHooks,
+ writeCursorMcpConfig,
+ writeCursorRules,
+ hasCursorMcpConfig,
+} from './config-writer';
/**
* Format a number with commas
@@ -17,10 +28,53 @@ function formatNumber(n: number): string {
return n.toLocaleString();
}
+/**
+ * Installer options for non-interactive mode
+ */
+export interface InstallerOptions {
+ ide?: string; // Comma-separated list or "all"
+ location?: 'global' | 'local';
+}
+
+/**
+ * Parse IDE string from CLI argument
+ * Exported for testing.
+ */
+export function parseIDEArg(ideArg: string): IDE {
+ if (ideArg.toLowerCase() === 'all') {
+ return ['claude', 'cursor'];
+ }
+ const ides = ideArg.split(',').map(s => s.trim().toLowerCase());
+ const valid: IDE = [];
+ for (const ide of ides) {
+ if (ide === 'claude' || ide === 'cursor') {
+ valid.push(ide);
+ }
+ }
+ if (valid.length === 0) {
+ throw new Error(`Invalid IDE(s): ${ideArg}. Use "claude", "cursor", or "all"`);
+ }
+ return valid;
+}
+
+/**
+ * Validate location option
+ * Exported for testing.
+ */
+export function validateLocation(location?: string): InstallLocation | undefined {
+ if (!location) return undefined;
+
+ const normalized = location.toLowerCase();
+ if (normalized !== 'global' && normalized !== 'local') {
+ throw new Error(`Invalid location: ${location}. Use "global" or "local"`);
+ }
+ return normalized as InstallLocation;
+}
+
/**
* Run the interactive installer
*/
-export async function runInstaller(): Promise {
+export async function runInstaller(options?: InstallerOptions): Promise {
// Show the banner
showBanner();
@@ -38,64 +92,31 @@ export async function runInstaller(): Promise {
}
console.log();
- // Step 2: Ask for installation location
- const location = await promptInstallLocation();
+ // Step 2: Ask which IDE(s) to configure (or use provided)
+ const ide = options?.ide ? parseIDEArg(options.ide) : await promptIDE();
console.log();
- // Step 3: Write MCP configuration (always uses npx for reliability)
- const alreadyHasMcp = hasMcpConfig(location);
- writeMcpConfig(location);
-
- if (alreadyHasMcp) {
- success(`Updated MCP server in ${location === 'global' ? '~/.claude.json' : './.claude.json'}`);
- } else {
- success(`Added MCP server to ${location === 'global' ? '~/.claude.json' : './.claude.json'}`);
- }
-
- // Step 4: Ask about auto-allow permissions
- const autoAllow = await promptAutoAllow();
+ // Step 3: Ask for installation location (or use provided)
+ const providedLocation = validateLocation(options?.location);
+ const location = providedLocation || await promptInstallLocation(ide);
console.log();
- if (autoAllow) {
- const alreadyHasPerms = hasPermissions(location);
- writePermissions(location);
-
- if (alreadyHasPerms) {
- success(`Updated permissions in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
- } else {
- success(`Added permissions to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
+ // Step 4: Configure selected IDEs
+ for (const ideName of ide) {
+ if (ideName === 'claude') {
+ await installForClaude(location);
+ } else if (ideName === 'cursor') {
+ await installForCursor();
}
}
- // Step 5: Write auto-sync hooks
- const alreadyHasHooks = hasHooks(location);
- writeHooks(location);
-
- if (alreadyHasHooks) {
- success(`Updated auto-sync hooks in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
- } else {
- success(`Added auto-sync hooks to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
- }
-
- // Step 6: Write CLAUDE.md instructions
- const claudeMdResult = writeClaudeMd(location);
- const claudeMdPath = location === 'global' ? '~/.claude/CLAUDE.md' : './.claude/CLAUDE.md';
-
- if (claudeMdResult.created) {
- success(`Created ${claudeMdPath} with CodeGraph instructions`);
- } else if (claudeMdResult.updated) {
- success(`Updated CodeGraph section in ${claudeMdPath}`);
- } else {
- success(`Added CodeGraph instructions to ${claudeMdPath}`);
- }
-
- // Step 7: For local install, initialize the project
+ // Step 6: For local install, initialize the project
if (location === 'local') {
await initializeLocalProject();
}
// Show next steps
- showNextSteps(location);
+ showNextSteps(location, ide);
} catch (err) {
console.log();
if (err instanceof Error && err.message.includes('readline was closed')) {
@@ -167,5 +188,85 @@ async function initializeLocalProject(): Promise {
cg.close();
}
+/**
+ * Install and configure for Claude Code
+ */
+async function installForClaude(location: InstallLocation): Promise {
+ // Write MCP configuration (always uses npx for reliability)
+ const alreadyHasMcp = hasClaudeMcpConfig(location);
+ writeMcpConfig(location);
+
+ if (alreadyHasMcp) {
+ success(`Updated MCP server in ${location === 'global' ? '~/.claude.json' : './.claude.json'}`);
+ } else {
+ success(`Added MCP server to ${location === 'global' ? '~/.claude.json' : './.claude.json'}`);
+ }
+
+ // Ask about auto-allow permissions
+ const autoAllow = await promptAutoAllow();
+ console.log();
+
+ if (autoAllow) {
+ const alreadyHasPerms = hasPermissions(location);
+ writePermissions(location);
+
+ if (alreadyHasPerms) {
+ success(`Updated permissions in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
+ } else {
+ success(`Added permissions to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
+ }
+ }
+
+ // Write auto-sync hooks
+ const alreadyHasHooks = hasHooks(location);
+ writeHooks(location);
+
+ if (alreadyHasHooks) {
+ success(`Updated auto-sync hooks in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
+ } else {
+ success(`Added auto-sync hooks to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
+ }
+
+ // Write CLAUDE.md instructions
+ const claudeMdResult = writeClaudeMd(location);
+ const claudeMdPath = location === 'global' ? '~/.claude/CLAUDE.md' : './.claude/CLAUDE.md';
+
+ if (claudeMdResult.created) {
+ success(`Created ${claudeMdPath} with CodeGraph instructions`);
+ } else if (claudeMdResult.updated) {
+ success(`Updated CodeGraph section in ${claudeMdPath}`);
+ } else {
+ success(`Added CodeGraph instructions to ${claudeMdPath}`);
+ }
+}
+
+/**
+ * Install and configure for Cursor
+ * Note: Cursor only supports local configuration
+ */
+async function installForCursor(): Promise {
+ // Write MCP configuration
+ const alreadyHasMcp = hasCursorMcpConfig();
+ writeCursorMcpConfig();
+
+ if (alreadyHasMcp) {
+ success('Updated MCP server in ./.cursor/mcp.json');
+ } else {
+ success('Added MCP server to ./.cursor/mcp.json');
+ }
+
+ // Write Cursor rules file
+ const cursorRulesResult = writeCursorRules();
+
+ if (cursorRulesResult.created) {
+ success('Created .cursor/rules/codegraph.md with instructions');
+ } else if (cursorRulesResult.updated) {
+ success('Updated .cursor/rules/codegraph.md');
+ }
+
+ console.log();
+ info('Note: MCP tools in Cursor are only available in Agent mode, not Composer');
+}
+
// Export for use in CLI
-export { InstallLocation };
+export { InstallLocation, IDE };
diff --git a/src/installer/prompts.ts b/src/installer/prompts.ts
index fa22daf..e1e1a4c 100644
--- a/src/installer/prompts.ts
+++ b/src/installer/prompts.ts
@@ -4,9 +4,20 @@
*/
import * as readline from 'readline';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as os from 'os';
import { chalk } from './banner';
export type InstallLocation = 'global' | 'local';
+export type IDEName = 'claude' | 'cursor';
+export type IDE = IDEName[]; // Array of selected IDEs
+
+/**
+ * Default values for installer
+ */
+export const DEFAULT_IDE: IDEName = 'claude';
+export const DEFAULT_LOCATION: InstallLocation = 'local';
/**
* Create a readline interface for prompts
@@ -18,6 +29,28 @@ function createInterface(): readline.Interface {
});
}
+/**
+ * Detect which IDEs are installed by checking for their config directories
+ */
+function detectInstalledIDEs(): IDEName[] {
+ const detected: IDEName[] = [];
+
+ // Check for Claude Code config (global or local)
+ const hasGlobalClaude = fs.existsSync(path.join(os.homedir(), '.claude'));
+ const hasLocalClaude = fs.existsSync(path.join(process.cwd(), '.claude'));
+ if (hasGlobalClaude || hasLocalClaude) {
+ detected.push('claude');
+ }
+
+ // Check for Cursor config (local only)
+ const hasCursor = fs.existsSync(path.join(process.cwd(), '.cursor'));
+ if (hasCursor) {
+ detected.push('cursor');
+ }
+
+ return detected;
+}
+
/**
* Prompt the user with a question and return their answer
*/
@@ -29,28 +62,129 @@ function prompt(rl: readline.Interface, question: string): Promise {
});
}
+/**
+ * Prompt for IDE selection with checkbox-style input
+ * Users can select multiple IDEs by entering comma-separated numbers (e.g., "1,2")
+ * Auto-detects installed IDEs and uses them as defaults
+ *
+ * For non-interactive shells:
+ * - If IDEs are detected, uses detected IDEs
+ * - Otherwise defaults to Claude Code only
+ */
+export async function promptIDE(): Promise {
+ // Detect installed IDEs
+ const detected = detectInstalledIDEs();
+
+ // Non-interactive: use detected IDEs or default
+ if (!isInteractive()) {
+ if (detected.length > 0) {
+ return detected;
+ }
+ return [DEFAULT_IDE];
+ }
+
+ const rl = createInterface();
+
+ // Build default selection string
+ const defaultSelections: string[] = [];
+ if (detected.includes('claude')) defaultSelections.push('1');
+ if (detected.includes('cursor')) defaultSelections.push('2');
+ const defaultStr = defaultSelections.length > 0 ? defaultSelections.join(',') : '1';
+
+ console.log(chalk.bold(' Which IDE(s) would you like to configure?'));
+ console.log(chalk.dim(' (Enter comma-separated numbers, e.g., "1,2" for both)'));
+ console.log();
+
+ // Show Claude Code with detection indicator
+ if (detected.includes('claude')) {
+ console.log(' 1) Claude Code ' + chalk.green('✓ Detected'));
+ } else {
+ console.log(' 1) Claude Code');
+ }
+
+ // Show Cursor with detection indicator
+ if (detected.includes('cursor')) {
+ console.log(' 2) Cursor ' + chalk.green('✓ Detected'));
+ } else {
+ console.log(' 2) Cursor');
+ }
+ console.log();
+
+ const answer = await prompt(rl, ` Selection [${defaultStr}]: `);
+ rl.close();
+
+ // Parse comma-separated selections
+ const selections = (answer === '' ? defaultStr : answer)
+ .split(',')
+ .map(s => s.trim())
+ .filter(s => s !== '');
+
+ const ides: IDEName[] = [];
+
+ for (const selection of selections) {
+ if (selection === '1') {
+ if (!ides.includes('claude')) ides.push('claude');
+ } else if (selection === '2') {
+ if (!ides.includes('cursor')) ides.push('cursor');
+ }
+ }
+
+ // If no valid selections, use default
+ if (ides.length === 0) {
+ ides.push(DEFAULT_IDE);
+ }
+
+ return ides;
+}
+
+/**
+ * Check if running in an interactive terminal
+ */
+export function isInteractive(): boolean {
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
+}
+
/**
* Prompt for installation location (global or local)
+ * Defaults to 'local' for non-interactive shells
*/
-export async function promptInstallLocation(): Promise {
+export async function promptInstallLocation(ides: IDE): Promise {
+ const hasClaudeCode = ides.includes('claude');
+ const hasCursor = ides.includes('cursor');
+ const cursorOnly = hasCursor && !hasClaudeCode;
+
+ // Cursor only supports local installation
+ if (cursorOnly) {
+ return 'local';
+ }
+
+ // Non-interactive: use default location
+ if (!isInteractive()) {
+ return DEFAULT_LOCATION;
+ }
+
const rl = createInterface();
console.log(chalk.bold(' Where would you like to install?'));
console.log();
- console.log(' 1) Global (~/.claude) - available in all projects');
- console.log(' 2) Local (./.claude) - this project only');
+
+ console.log(' 1) Local (./.claude) - this project only');
+ console.log(' 2) Global (~/.claude) - available in all projects');
+ if (hasCursor) {
+ console.log(chalk.dim(' Note: Cursor will be configured locally regardless'));
+ }
console.log();
- const answer = await prompt(rl, ' Choice [1]: ');
+ const answer = await prompt(rl, ' Selection [1]: ');
rl.close();
- // Default to '1' if empty, parse the answer
+ // Default to '1' (local) if empty, parse the answer
const choice = answer === '' ? '1' : answer;
if (choice === '2') {
- return 'local';
+ return 'global';
}
- return 'global';
+ return 'local';
}
/**
diff --git a/src/installer/claude-md-template.ts b/src/installer/templates/claude-code.ts
similarity index 78%
rename from src/installer/claude-md-template.ts
rename to src/installer/templates/claude-code.ts
index 6b73c03..9dfb363 100644
--- a/src/installer/claude-md-template.ts
+++ b/src/installer/templates/claude-code.ts
@@ -1,15 +1,18 @@
/**
- * CLAUDE.md template for CodeGraph instructions
+ * Template for Claude Code's CLAUDE.md file
*
- * This template is injected into ~/.claude/CLAUDE.md (global) or ./.claude/CLAUDE.md (local)
- * Keep this in sync with the README.md "Recommended: Add Global Instructions" section
+ * This template is injected into ~/.claude/CLAUDE.md (global) or ./.claude/CLAUDE.md (local).
+ * It instructs Claude Code on when and how to use CodeGraph MCP tools.
+ *
+ * Since CLAUDE.md is a shared file that may contain other instructions, we use HTML comment
+ * markers to identify and update just the CodeGraph section.
*/
-// Markers to identify CodeGraph section for updates
+// Markers to identify CodeGraph section for updates in CLAUDE.md
export const CODEGRAPH_SECTION_START = '';
export const CODEGRAPH_SECTION_END = '';
-export const CLAUDE_MD_TEMPLATE = `${CODEGRAPH_SECTION_START}
+export const CLAUDE_CODE_TEMPLATE = `${CODEGRAPH_SECTION_START}
## CodeGraph
CodeGraph builds a semantic knowledge graph of codebases for faster, smarter code exploration.
diff --git a/src/installer/templates/cursor.ts b/src/installer/templates/cursor.ts
new file mode 100644
index 0000000..60d3473
--- /dev/null
+++ b/src/installer/templates/cursor.ts
@@ -0,0 +1,45 @@
+/**
+ * Template for Cursor's rules file
+ *
+ * This template is written to .cursor/rules/codegraph.md (local only).
+ * It instructs Cursor Agent on when and how to use CodeGraph MCP tools.
+ *
+ * Unlike Claude Code, Cursor uses a dedicated file per rule in the .cursor/rules/ directory,
+ * so no section markers are needed - the entire file is our template.
+ *
+ * Note: MCP tools are only available in Cursor's Agent mode, not Composer.
+ */
+
+export const CURSOR_TEMPLATE = `## CodeGraph
+
+CodeGraph builds a semantic knowledge graph of codebases for faster, smarter code exploration.
+
+### If \`.codegraph/\` exists in the project
+
+**Use MCP codegraph tools for faster exploration.** These tools provide instant lookups via the code graph instead of scanning files:
+
+| MCP Tool | Use For |
+|----------|---------|
+| \`codegraph_search\` | Find symbols by name (functions, classes, types) |
+| \`codegraph_context\` | Get relevant code context for a task |
+| \`codegraph_callers\` | Find what calls a function |
+| \`codegraph_callees\` | Find what a function calls |
+| \`codegraph_impact\` | See what's affected by changing a symbol |
+| \`codegraph_node\` | Get details + source code for a symbol |
+
+**Usage in Agent mode:**
+- Use \`codegraph_search\` instead of grep/find for locating symbols
+- Use \`codegraph_callers\`/\`codegraph_callees\` to trace code flow
+- Use \`codegraph_impact\` before making changes to see dependencies
+- Use \`codegraph_context\` to get relevant code for implementing features
+
+**Important:**
+- CodeGraph provides **code context**, not product requirements
+- For new features, still ask the user about UX, edge cases, and acceptance criteria
+- MCP tools are only available in Agent mode (not Composer)
+
+### If \`.codegraph/\` does NOT exist
+
+At the start of a session, suggest initializing CodeGraph:
+
+"I notice this project doesn't have CodeGraph initialized. Would you like me to run \`codegraph init -i\` to build a code knowledge graph for faster exploration?"`;