From 30b4340d1512a6c02543be8a8c160fcada710b3c Mon Sep 17 00:00:00 2001 From: RubyJ Date: Wed, 3 Jun 2026 11:30:05 -0400 Subject: [PATCH] feat: add Factory Droid installer target Adds Factory Droid (the Factory CLI) as a first-class installer target so `codegraph install` auto-detects and configures it instead of requiring the manual `droid mcp add codegraph` workaround. Changes: - src/installer/targets/droid.ts (new): FactoryDroidTarget writes ~/.factory/mcp.json (global) or ./.factory/mcp.json (local) with the standard mcpServers.codegraph stdio entry. No permissions concept, no instructions file (guidance ships via MCP initialize per issue #529). Idempotent installs, surgical uninstall preserving sibling servers. - src/installer/targets/types.ts: adds 'droid' to TargetId union. - src/installer/targets/registry.ts: imports and registers droidTarget. - src/installer/index.ts: adds ~/.factory / ./.factory to the install/uninstall location hint strings. - __tests__/installer-targets.test.ts: droidTarget is automatically covered by the parameterized contract suite; adds four dedicated tests for the global/local mcp.json path, no instructions file, sibling preservation, and surgical uninstall. - Docs: README.md, site integrations.md, site installation.md, and CHANGELOG.md [Unreleased] updated to include Factory Droid. --- CHANGELOG.md | 1 + README.md | 12 +- __tests__/installer-targets.test.ts | 46 +++++++ .../docs/getting-started/installation.md | 4 +- .../content/docs/reference/integrations.md | 1 + src/installer/index.ts | 8 +- src/installer/targets/droid.ts | 125 ++++++++++++++++++ src/installer/targets/registry.ts | 2 + src/installer/targets/types.ts | 2 +- 9 files changed, 189 insertions(+), 12 deletions(-) create mode 100644 src/installer/targets/droid.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ef5f5aa..d9d2e7bf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 1a9800ee3..4e9987804 100644 --- a/README.md +++ b/README.md @@ -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** @@ -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) @@ -57,7 +58,7 @@ In a **new terminal**, run the installer to connect CodeGraph to the agents you codegraph install ``` -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.) +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.) ### 3. Initialize each project @@ -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.) @@ -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 @@ -600,6 +601,7 @@ is written): - **Gemini CLI** - **Antigravity IDE** - **Kiro** +- **Factory Droid** ## Supported Languages @@ -661,7 +663,7 @@ MIT
-**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) diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index 27fcbd6e8..355991444 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -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 }); diff --git a/site/src/content/docs/getting-started/installation.md b/site/src/content/docs/getting-started/installation.md index 4e8a0e2ce..d3bef5e73 100644 --- a/site/src/content/docs/getting-started/installation.md +++ b/site/src/content/docs/getting-started/installation.md @@ -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`). @@ -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 diff --git a/site/src/content/docs/reference/integrations.md b/site/src/content/docs/reference/integrations.md index 67e7b96e4..65fd38a66 100644 --- a/site/src/content/docs/reference/integrations.md +++ b/site/src/content/docs/reference/integrations.md @@ -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. diff --git a/src/installer/index.ts b/src/installer/index.ts index edd48ecaf..fbb3de399 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -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, }); @@ -317,8 +317,8 @@ export async function runUninstaller(opts: RunUninstallerOptions): Promise 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, }); diff --git a/src/installer/targets/droid.ts b/src/installer/targets/droid.ts new file mode 100644 index 000000000..ae36f3eb9 --- /dev/null +++ b/src/installer/targets/droid.ts @@ -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(); diff --git a/src/installer/targets/registry.ts b/src/installer/targets/registry.ts index 5e929d468..1df677a0c 100644 --- a/src/installer/targets/registry.ts +++ b/src/installer/targets/registry.ts @@ -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, @@ -26,6 +27,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ geminiTarget, antigravityTarget, kiroTarget, + droidTarget, ]); export function getTarget(id: string): AgentTarget | undefined { diff --git a/src/installer/targets/types.ts b/src/installer/targets/types.ts index 4b3267e97..f06ab1ca1 100644 --- a/src/installer/targets/types.ts +++ b/src/installer/targets/types.ts @@ -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)`.