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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### New Features

- `codegraph install` now detects and configures **Factory Droid** (the Factory CLI). Running `codegraph install` on a machine with Droid CLI installs the MCP server entry into `~/.factory/mcp.json` (global) or `.factory/mcp.json` (local), so Droid users no longer need to run `droid mcp add codegraph` manually. Use `--target=droid` to target it explicitly.
- `codegraph status --json` now also reports the running CLI `version`, the index directory (`indexPath`), and a `lastIndexed` timestamp (ISO-8601, or null when nothing's indexed yet), so CI and scripts can pin the CLI version and check index freshness from a single command. A matching `CodeGraph.getLastIndexedAt()` library method exposes the same freshness check without shelling out. Thanks @12122J and @eddieran. (#329)

### Fixes
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# CodeGraph

### Supercharge Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini, Antigravity, and Kiro with Semantic Code Intelligence
### Supercharge Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini, Antigravity, Kiro, and Factory Droid with Semantic Code Intelligence

**~16% cheaper · ~58% fewer tool calls · 100% local**

Expand All @@ -24,6 +24,7 @@
[![Gemini](https://img.shields.io/badge/Gemini-supported-blueviolet.svg)](#supported-agents)
[![Antigravity](https://img.shields.io/badge/Antigravity-supported-blueviolet.svg)](#supported-agents)
[![Kiro](https://img.shields.io/badge/Kiro-supported-blueviolet.svg)](#supported-agents)
[![Factory Droid](https://img.shields.io/badge/Factory_Droid-supported-blueviolet.svg)](#supported-agents)

</div>

Expand Down Expand Up @@ -57,7 +58,7 @@ In a **new terminal**, run the installer to connect CodeGraph to the agents you
codegraph install
```

<sub>Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)</sub>
<sub>Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, and Factory Droid — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)</sub>

### 3. Initialize each project

Expand Down Expand Up @@ -310,7 +311,7 @@ npx @colbymchenry/codegraph
```

The installer will:
- Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, **Kiro**
- Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, **Kiro**, **Factory Droid**
- Prompt to install `codegraph` on your PATH (so agents can launch the MCP server)
- Ask whether configs apply to all your projects or just this one
- Write each chosen agent's MCP server config (the codegraph usage guide is delivered by the MCP server itself, so no instructions file is added to `CLAUDE.md` / `AGENTS.md` / etc.)
Expand All @@ -336,7 +337,7 @@ codegraph install --print-config codex # print snippet, no file wr

### 2. Restart Your Agent

Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro) for the MCP server to load.
Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro / Factory Droid) for the MCP server to load.

### 3. Initialize Projects

Expand Down Expand Up @@ -600,6 +601,7 @@ is written):
- **Gemini CLI**
- **Antigravity IDE**
- **Kiro**
- **Factory Droid**

## Supported Languages

Expand Down Expand Up @@ -661,7 +663,7 @@ MIT

<div align="center">

**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro**
**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, and Factory Droid**

[Report Bug](https://github.com/colbymchenry/codegraph/issues) · [Request Feature](https://github.com/colbymchenry/codegraph/issues)

Expand Down
46 changes: 46 additions & 0 deletions __tests__/installer-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,52 @@ describe('Installer targets — partial-state idempotency', () => {
expect(paths.some((p) => p.endsWith('/.kiro/steering/codegraph.md'))).toBe(false);
});

it('droid: global install writes ~/.factory/mcp.json (mcpServers.codegraph) and no instructions file (#529)', () => {
const droid = getTarget('droid')!;
const result = droid.install('global', { autoAllow: true });
const mcp = path.join(tmpHome, '.factory', 'mcp.json');
expect(fs.existsSync(mcp)).toBe(true);
const cfg = JSON.parse(fs.readFileSync(mcp, 'utf-8'));
expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'] });
// No markdown instructions file should be written.
expect(result.files.every((f) => !f.path.endsWith('.md'))).toBe(true);
});

it('droid: local install writes ./.factory/mcp.json and no instructions file (#529)', () => {
const droid = getTarget('droid')!;
const result = droid.install('local', { autoAllow: true });
const paths = result.files.map((f) => f.path.replace(/\\/g, '/'));
expect(paths.some((p) => p.endsWith('/.factory/mcp.json'))).toBe(true);
expect(result.files.every((f) => !f.path.endsWith('.md'))).toBe(true);
});

it('droid: install preserves pre-existing sibling MCP servers in mcp.json', () => {
const droid = getTarget('droid')!;
const mcp = path.join(tmpHome, '.factory', 'mcp.json');
fs.mkdirSync(path.dirname(mcp), { recursive: true });
fs.writeFileSync(mcp, JSON.stringify({ mcpServers: { other: { command: 'x' } } }, null, 2) + '\n');

droid.install('global', { autoAllow: true });

const after = JSON.parse(fs.readFileSync(mcp, 'utf-8'));
expect(after.mcpServers.other).toBeDefined();
expect(after.mcpServers.codegraph).toBeDefined();
});

it('droid: uninstall strips codegraph but leaves sibling MCP servers intact', () => {
const droid = getTarget('droid')!;
const mcp = path.join(tmpHome, '.factory', 'mcp.json');
fs.mkdirSync(path.dirname(mcp), { recursive: true });
fs.writeFileSync(mcp, JSON.stringify({ mcpServers: { other: { command: 'x' } } }, null, 2) + '\n');

droid.install('global', { autoAllow: true });
droid.uninstall('global');

const after = JSON.parse(fs.readFileSync(mcp, 'utf-8'));
expect(after.mcpServers.other).toBeDefined();
expect(after.mcpServers.codegraph).toBeUndefined();
});

it('antigravity: install writes to LEGACY ~/.gemini/antigravity/mcp_config.json when no migration marker', () => {
const antigravity = getTarget('antigravity')!;
antigravity.install('global', { autoAllow: true });
Expand Down
4 changes: 2 additions & 2 deletions site/src/content/docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ npx @colbymchenry/codegraph

The installer will:

- Ask which agent(s) to configure — auto-detecting installed ones from **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, and **Kiro**.
- Ask which agent(s) to configure — auto-detecting installed ones from **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, **Kiro**, and **Factory Droid**.
- Prompt to install `codegraph` on your `PATH` (so agents can launch the MCP server).
- Ask whether configs apply to all your projects or just this one.
- Write each chosen agent's MCP server config plus an instructions file (e.g. `CLAUDE.md`, `.cursor/rules/codegraph.mdc`, `~/.codex/AGENTS.md`).
Expand All @@ -37,7 +37,7 @@ codegraph install --print-config codex # print snippet, no file wr

## 2. Restart your agent

Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro) for the MCP server to load.
Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro / Factory Droid) for the MCP server to load.

## 3. Initialize projects

Expand Down
1 change: 1 addition & 0 deletions site/src/content/docs/reference/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The interactive installer auto-detects and configures each supported agent — w
- **Gemini CLI**
- **Antigravity IDE**
- **Kiro**
- **Factory Droid**

Run `npx @colbymchenry/codegraph` and pick your agent(s); see [Installation](/codegraph/getting-started/installation/) for the non-interactive flags.

Expand Down
8 changes: 4 additions & 4 deletions src/installer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
const sel = await clack.select({
message: 'Apply agent configs to all your projects, or just this one?',
options: [
{ value: 'global' as const, label: 'All projects', hint: '~/.claude, ~/.cursor, etc.' },
{ value: 'local' as const, label: 'Just this project', hint: './.claude, ./.cursor, etc.' },
{ value: 'global' as const, label: 'All projects', hint: '~/.claude, ~/.cursor, ~/.factory, etc.' },
{ value: 'local' as const, label: 'Just this project', hint: './.claude, ./.cursor, ./.factory, etc.' },
],
initialValue: 'global' as const,
});
Expand Down Expand Up @@ -317,8 +317,8 @@ export async function runUninstaller(opts: RunUninstallerOptions): Promise<void>
const sel = await clack.select({
message: 'Remove CodeGraph from all your projects, or just this one?',
options: [
{ value: 'global' as const, label: 'All projects (global)', hint: '~/.claude, ~/.cursor, ~/.codex, ~/.config/opencode, ~/.hermes, ~/.gemini, ~/.kiro' },
{ value: 'local' as const, label: 'Just this project (local)', hint: './.claude, ./.cursor, ./opencode.jsonc, ./.gemini, ./.kiro' },
{ value: 'global' as const, label: 'All projects (global)', hint: '~/.claude, ~/.cursor, ~/.codex, ~/.config/opencode, ~/.hermes, ~/.gemini, ~/.kiro, ~/.factory' },
{ value: 'local' as const, label: 'Just this project (local)', hint: './.claude, ./.cursor, ./opencode.jsonc, ./.gemini, ./.kiro, ./.factory' },
],
initialValue: 'global' as const,
});
Expand Down
125 changes: 125 additions & 0 deletions src/installer/targets/droid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Factory Droid target. Writes:
*
* - MCP server entry to `~/.factory/mcp.json` (global = user scope,
* loads in every project) or `./.factory/mcp.json` (local = project
* scope, committed to the repo). Standard `mcpServers.codegraph`
* shape, same as Claude / Cursor / Gemini / Kiro.
*
* No permissions concept — Droid gates tool runs through its own
* autonomy levels, not an external allowlist. `autoAllow` is ignored.
*
* No instructions file — the codegraph usage guidance ships in the MCP
* server's `initialize` response (issue #529), which Droid surfaces
* automatically.
*
* Docs: https://docs.factory.ai/cli/configuration/mcp
*/

import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import {
AgentTarget,
DetectionResult,
InstallOptions,
Location,
WriteResult,
} from './types';
import {
getMcpServerConfig,
jsonDeepEqual,
readJsonFile,
writeJsonFile,
} from './shared';

function configDir(loc: Location): string {
return loc === 'global'
? path.join(os.homedir(), '.factory')
: path.join(process.cwd(), '.factory');
}

function mcpJsonPath(loc: Location): string {
return path.join(configDir(loc), 'mcp.json');
}

class FactoryDroidTarget implements AgentTarget {
readonly id = 'droid' as const;
readonly displayName = 'Factory Droid';
readonly docsUrl = 'https://docs.factory.ai/cli/configuration/mcp';

supportsLocation(_loc: Location): boolean {
return true;
}

detect(loc: Location): DetectionResult {
const file = mcpJsonPath(loc);
const config = readJsonFile(file);
const alreadyConfigured = !!config.mcpServers?.codegraph;
const installed =
loc === 'global'
? fs.existsSync(configDir('global')) || fs.existsSync(file)
: fs.existsSync(file) || fs.existsSync(configDir('local'));
return { installed, alreadyConfigured, configPath: file };
}

install(loc: Location, _opts: InstallOptions): WriteResult {
return {
files: [writeMcpEntry(loc)],
notes: ['Restart Droid for MCP changes to take effect.'],
};
}

uninstall(loc: Location): WriteResult {
const file = mcpJsonPath(loc);
const config = readJsonFile(file);

if (!config.mcpServers?.codegraph) {
return { files: [{ path: file, action: 'not-found' }] };
}

delete config.mcpServers.codegraph;
if (Object.keys(config.mcpServers).length === 0) {
delete config.mcpServers;
}
writeJsonFile(file, config);
return { files: [{ path: file, action: 'removed' }] };
}

printConfig(loc: Location): string {
const target = mcpJsonPath(loc);
const snippet = JSON.stringify(
{ mcpServers: { codegraph: getMcpServerConfig() } },
null,
2,
);
return `# Add to ${target}\n\n${snippet}\n`;
}

describePaths(loc: Location): string[] {
return [mcpJsonPath(loc)];
}
}

function writeMcpEntry(loc: Location): WriteResult['files'][number] {
const file = mcpJsonPath(loc);
const dir = path.dirname(file);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

const existing = readJsonFile(file);
const before = existing.mcpServers?.codegraph;
const after = getMcpServerConfig();

if (jsonDeepEqual(before, after)) {
return { path: file, action: 'unchanged' };
}

const action: 'created' | 'updated' =
before ? 'updated' : fs.existsSync(file) ? 'updated' : 'created';
if (!existing.mcpServers) existing.mcpServers = {};
existing.mcpServers.codegraph = after;
writeJsonFile(file, existing);
return { path: file, action };
}

export const droidTarget: AgentTarget = new FactoryDroidTarget();
2 changes: 2 additions & 0 deletions src/installer/targets/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { hermesTarget } from './hermes';
import { geminiTarget } from './gemini';
import { antigravityTarget } from './antigravity';
import { kiroTarget } from './kiro';
import { droidTarget } from './droid';

export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
claudeTarget,
Expand All @@ -26,6 +27,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
geminiTarget,
antigravityTarget,
kiroTarget,
droidTarget,
]);

export function getTarget(id: string): AgentTarget | undefined {
Expand Down
2 changes: 1 addition & 1 deletion src/installer/targets/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type Location = 'global' | 'local';
* lookup. New targets add a value here when they're added to the
* registry. Keep these short and lowercase.
*/
export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'gemini' | 'antigravity' | 'kiro';
export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'gemini' | 'antigravity' | 'kiro' | 'droid';

/**
* Result of `target.detect(location)`.
Expand Down