diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index 6db793d65..60a3539a8 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -1229,6 +1229,7 @@ describe('Installer targets — registry', () => { expect(getTarget('gemini')?.id).toBe('gemini'); expect(getTarget('antigravity')?.id).toBe('antigravity'); expect(getTarget('kiro')?.id).toBe('kiro'); + expect(getTarget('jcode')?.id).toBe('jcode'); expect(getTarget('not-a-real-target')).toBeUndefined(); }); @@ -1244,6 +1245,148 @@ describe('Installer targets — registry', () => { }); }); +describe('Installer targets — jcode specifics', () => { + let tmpHome: string; + let tmpCwd: string; + let origCwd: string; + let homeRestore: { restore: () => void }; + + beforeEach(() => { + tmpHome = mkTmpDir('home'); + tmpCwd = mkTmpDir('cwd'); + origCwd = process.cwd(); + process.chdir(tmpCwd); + homeRestore = setHome(tmpHome); + }); + + afterEach(() => { + homeRestore.restore(); + process.chdir(origCwd); + fs.rmSync(tmpHome, { recursive: true, force: true }); + fs.rmSync(tmpCwd, { recursive: true, force: true }); + }); + + it('global install writes mcp.json, AGENTS.md, and the codegraph skill', () => { + const jcode = getTarget('jcode')!; + const result = jcode.install('global', { autoAllow: true }); + + const mcp = path.join(tmpHome, '.jcode', 'mcp.json'); + const agents = path.join(tmpHome, 'AGENTS.md'); + const skill = path.join(tmpHome, '.jcode', 'skills', 'codegraph', 'SKILL.md'); + + expect(fs.existsSync(mcp)).toBe(true); + expect(fs.existsSync(agents)).toBe(true); + expect(fs.existsSync(skill)).toBe(true); + + expect(result.files.some((f) => f.path === mcp)).toBe(true); + expect(result.files.some((f) => f.path === agents)).toBe(true); + expect(result.files.some((f) => f.path === skill)).toBe(true); + }); + + it('local install writes .jcode/mcp.json and ./AGENTS.md, but no skill file', () => { + const jcode = getTarget('jcode')!; + const result = jcode.install('local', { autoAllow: true }); + + const mcp = path.join(tmpCwd, '.jcode', 'mcp.json'); + const agents = path.join(tmpCwd, 'AGENTS.md'); + const skill = path.join(tmpHome, '.jcode', 'skills', 'codegraph', 'SKILL.md'); + + expect(fs.existsSync(mcp)).toBe(true); + expect(fs.existsSync(agents)).toBe(true); + expect(fs.existsSync(skill)).toBe(false); + + expect(result.files.some((f) => f.path === mcp)).toBe(true); + expect(result.files.some((f) => f.path === agents)).toBe(true); + expect(result.files.some((f) => f.path === skill)).toBe(false); + }); + + it('MCP entry omits the type field and uses mcpServers', () => { + const jcode = getTarget('jcode')!; + jcode.install('global', { autoAllow: true }); + + const cfg = JSON.parse(fs.readFileSync(path.join(tmpHome, '.jcode', 'mcp.json'), 'utf-8')); + expect(cfg.mcpServers.codegraph).toEqual({ command: 'codegraph', args: ['serve', '--mcp'] }); + expect(cfg.mcpServers.codegraph.type).toBeUndefined(); + expect(cfg.servers).toBeUndefined(); + }); + + it('AGENTS.md block is the jcode-optimized variant', () => { + const jcode = getTarget('jcode')!; + jcode.install('global', { autoAllow: true }); + + const body = fs.readFileSync(path.join(tmpHome, 'AGENTS.md'), 'utf-8'); + expect(body).toContain('codegraph_explore'); + expect(body).toContain('codegraph impact'); + expect(body).toContain('codegraph affected'); + expect(body).toContain('codegraph status'); + expect(body).toContain('codegraph sync'); + }); + + it('skill file contains the token-efficient playbook', () => { + const jcode = getTarget('jcode')!; + jcode.install('global', { autoAllow: true }); + + const body = fs.readFileSync(path.join(tmpHome, '.jcode', 'skills', 'codegraph', 'SKILL.md'), 'utf-8'); + expect(body).toContain('# CodeGraph skill'); + expect(body).toContain('codegraph_explore'); + expect(body).toContain('codegraph impact'); + expect(body).toContain('codegraph affected'); + expect(body).toContain('codegraph_node'); + expect(body).toContain('codegraph_callers'); + expect(body).toContain('codegraph_callees'); + }); + + it('uninstall removes only codegraph, preserving sibling MCP servers and user AGENTS.md content', () => { + const jcode = getTarget('jcode')!; + const mcp = path.join(tmpHome, '.jcode', 'mcp.json'); + const agents = path.join(tmpHome, 'AGENTS.md'); + const skill = path.join(tmpHome, '.jcode', 'skills', 'codegraph', 'SKILL.md'); + + fs.mkdirSync(path.dirname(mcp), { recursive: true }); + fs.writeFileSync(mcp, JSON.stringify({ + mcpServers: { + other: { command: 'other', args: ['serve'] }, + }, + }, null, 2) + '\n'); + fs.writeFileSync(agents, '# My global rules\n\nAlways use tabs.\n'); + + jcode.install('global', { autoAllow: true }); + expect(fs.existsSync(skill)).toBe(true); + jcode.uninstall('global'); + + const afterMcp = JSON.parse(fs.readFileSync(mcp, 'utf-8')); + expect(afterMcp.mcpServers.other).toBeDefined(); + expect(afterMcp.mcpServers.codegraph).toBeUndefined(); + + const afterAgents = fs.readFileSync(agents, 'utf-8'); + expect(afterAgents).toContain('# My global rules'); + expect(afterAgents).toContain('Always use tabs.'); + expect(afterAgents).not.toContain('CODEGRAPH_START'); + + expect(fs.existsSync(skill)).toBe(false); + }); + + it('re-running install is idempotent', () => { + const jcode = getTarget('jcode')!; + jcode.install('global', { autoAllow: true }); + const second = jcode.install('global', { autoAllow: true }); + for (const f of second.files) { + expect(f.action).toBe('unchanged'); + } + }); + + it('detect recognizes alreadyConfigured when mcpServers.codegraph exists', () => { + const jcode = getTarget('jcode')!; + expect(jcode.detect('global').alreadyConfigured).toBe(false); + + jcode.install('global', { autoAllow: true }); + expect(jcode.detect('global').alreadyConfigured).toBe(true); + + jcode.uninstall('global'); + expect(jcode.detect('global').alreadyConfigured).toBe(false); + }); +}); + describe('Installer targets — TOML serializer (Codex backbone)', () => { it('builds a [mcp_servers.codegraph] block with command + args', () => { const block = buildTomlTable('mcp_servers.codegraph', { diff --git a/docs/plans/jcode-integration-and-architecture-review.md b/docs/plans/jcode-integration-and-architecture-review.md new file mode 100644 index 000000000..4be78d7df --- /dev/null +++ b/docs/plans/jcode-integration-and-architecture-review.md @@ -0,0 +1,262 @@ +# CodeGraph jcode Integration Plan & Architecture Review + +**Status:** Plan / design document — no source code has been modified yet. +**Date:** 2026-07-30 +**Author:** Jcode (analysis agent) + +--- + +## 1. Executive Summary + +This document records the critical architecture problems found in the `D:\project\codegraph` repository and proposes a concrete integration plan for **jcode** (the Rust-based coding-agent harness at https://jcode.sh) that matches the existing Claude Code integration surface. + +The primary goal is to make CodeGraph a first-class jcode citizen while **optimizing token consumption** (fewer, higher-quality tool calls) and **improving coding quality** (impact-aware edits, test-aware changes, and graph-driven context instead of blind grep/read loops). + +--- + +## 2. Architecture Problems Found (Recorded, Not Fixed) + +These were identified by static source analysis. `node_modules` was not present, so builds and tests were not executed. + +### 2.1 Runtime dependency mismatch — hard crash on Node 20/21 +- **Location:** `src/db/sqlite-adapter.ts` requires `node:sqlite`. +- **Issue:** `node:sqlite` is only available in Node **22.5+**, but `package.json` declares `"engines": { "node": ">=20.0.0 <25.0.0" }`. +- **Impact:** The package installs successfully on Node 20 and 21, then crashes at runtime when the database layer loads. +- **Severity:** Critical. + +### 2.2 Circular module dependencies between the public API and MCP layer +- **Cycle:** `src/index.ts` exports `MCPServer` from `./mcp`; `src/mcp/engine.ts` and `src/mcp/tools.ts` import `CodeGraph` from `../index`. +- **Issue:** The public API and the MCP server layer are mutually dependent. While the imports are `type`-only today, the architecture is fragile and makes load-order reasoning and refactoring risky. +- **Severity:** High. + +### 2.3 Massive god classes / files violating single-responsibility + +| File | Approx. lines | Concerns mixed in one file | +|------|--------------|---------------------------| +| `src/extraction/tree-sitter.ts` | ~6,375 | All-language parsing, node/edge extraction, WASM grammar coordination | +| `src/mcp/tools.ts` | ~4,400 | Every MCP tool handler | +| `src/resolution/callback-synthesizer.ts` | ~3,591 | Multiple synthesis strategies | +| `src/extraction/index.ts` | ~2,574 | File scanning, git integration, ignore logic, framework detection, sync, store writing | +| `src/db/queries.ts` | ~2,321 | Entire SQL surface | +| `src/resolution/index.ts` | ~2,264 | Reference resolution, batching, caching | +| `src/resolution/name-matcher.ts` | ~2,181 | Name matching heuristics | +| `src/bin/codegraph.ts` | ~2,216 | All CLI commands in one function | +| `src/index.ts` | ~1,671 | `CodeGraph` facade that owns DB, extraction, resolution, graph, context, search | + +- **Impact:** The codebase is hard to test, review, and extend. A single bug in one concern often requires understanding the whole file. +- **Severity:** High (maintainability crisis). + +### 2.4 Multi-package repo without workspace tooling +- **Issue:** The repository contains three packages (`root`, `site`, `telemetry-worker`) each with their own `package.json` and `package-lock.json`. There is no `workspaces` field, no shared scripts, and no shared dependency management. +- **Impact:** The docs site and telemetry worker can drift out of sync with the core package; routine upgrades and security patches are manual. +- **Severity:** High. + +### 2.5 Non-portable build script +- **Location:** `package.json` `"build:kernel": "bash scripts/build-kernel.sh"`. +- **Issue:** Requires a POSIX shell; fails on Windows without WSL. The current environment is Windows. +- **Severity:** Medium. + +### 2.6 Storage layer typed as `any` +- **Location:** `src/db/sqlite-adapter.ts` defines `SqliteStatement`/`SqliteDatabase` with `any` for parameters, rows, and the private `_db` field. +- **Issue:** With `strict: true` enabled project-wide, this leaks unsafe typing into the entire persistence layer and undermines static analysis. +- **Severity:** Medium. + +### 2.7 Misleading dependency declaration +- **Location:** `package.json` lists `@types/better-sqlite3` as a dev dependency but does not list `better-sqlite3` itself. +- **Issue:** The project uses `node:sqlite`, not `better-sqlite3`, so the types package is unnecessary and confusing. +- **Severity:** Medium. + +--- + +## 3. jcode Integration Plan + +### 3.1 Goal + +Add jcode as a native installer target so that `codegraph install --target jcode` (or `--target=all`) produces the same first-class integration that already exists for Claude Code, Cursor, Codex CLI, opencode, etc. + +In addition, optimize the integration for **jcode's specific design** to reduce token spend and raise code-change quality: + +- jcode reads project instructions from **`AGENTS.md`** (repo root) and global instructions from **`~/AGENTS.md`**. +- jcode reads MCP servers from **`.jcode/mcp.json`** (project) and **`~/.jcode/mcp.json`** (global). +- jcode supports **stdio MCP servers only** (CodeGraph already ships as a stdio server). +- jcode has a **skill system** under `~/.jcode/skills//SKILL.md` that auto-injects via semantic embedding when the conversation matches the skill topic. +- jcode renders **mermaid diagrams inline** and has a **side panel**, which can display structured CodeGraph output. +- jcode has slash commands (`/review`, `/test`, `/commit`) that naturally pair with CodeGraph's impact, affected-tests, and callers/callees tools. + +### 3.2 Files the installer should write + +For a **global** jcode install: +- `~/.jcode/mcp.json` — MCP server entry for `codegraph`. +- `~/AGENTS.md` — short, token-efficient CodeGraph instructions. +- `~/.jcode/skills/codegraph/SKILL.md` — optional skill for semantic auto-injection. + +For a **project-local** jcode install: +- `.jcode/mcp.json` — project-local MCP server entry. +- `AGENTS.md` — project-root instructions (same content, but scoped to the repo). + +### 3.3 Proposed code changes + +1. **Extend the installer target abstraction** + - Add `'jcode'` to the `TargetId` union in `src/installer/targets/types.ts`. + +2. **Create `src/installer/targets/jcode.ts`** + - Implement `AgentTarget` for jcode. + - Write JSON MCP config to `~/.jcode/mcp.json` and `.jcode/mcp.json` using the `mcpServers` key. + - Write the CodeGraph instructions block to `~/AGENTS.md` and `AGENTS.md` using the existing marker-delimited section helpers. + - Optionally install/update the `~/.jcode/skills/codegraph/SKILL.md` skill file. + - Implement `detect`, `install`, `uninstall`, `printConfig`, `describePaths`, and `supportsLocation` (jcode supports both global and local). + +3. **Register the target** + - Import `jcodeTarget` in `src/installer/targets/registry.ts` and add it to `ALL_TARGETS`. + +4. **Add a jcode-optimized instructions block** + - Either add a new exported block in `src/installer/instructions-template.ts` or make `upsertInstructionsEntry` accept a custom body. + - The jcode block should be even shorter than the generic one and explicitly mention slash-command pairings (`/review`, `/test`, `/commit`) and the skill auto-injection behavior. + +5. **Add tests** + - Mirror the existing installer tests in `__tests__/` for the new target. + +6. **Update documentation** + - Add jcode to the README install matrix and `install --target` help. + +### 3.4 Token-consumption optimization strategy + +1. **One-shot tool preference** + - Instruct jcode to call `codegraph_explore` once instead of chaining many `grep`/`read` calls. `explore` returns relevant symbols, source, and call paths in a single response. + +2. **Bounded output by default** + - Encourage use of `maxFiles`, `limit`, and `depth` parameters so responses fit in the context window without truncation. + +3. **Avoid redundant reads** + - Tell jcode that `codegraph_explore` and `codegraph_node` already return verbatim source with line numbers, so it should not re-read the same file to confirm line numbers. + +4. **Skill-based conditional injection** + - The `codegraph` skill is only injected when the conversation is about code exploration, impact, refactoring, or debugging. This prevents the instructions from being loaded into every unrelated turn. + +5. **Concise instructions** + - Keep the `AGENTS.md` block under ~40 lines and the `SKILL.md` under ~60 lines. jcode reads `AGENTS.md` every turn, so every extra line costs tokens. + +### 3.5 Coding-quality optimization strategy + +1. **Impact-first edits** + - Before modifying a symbol, use `codegraph_impact` to identify affected callers and tests. + +2. **Test-aware changes** + - Use `codegraph_affected` (or the `affected` CLI) to find tests that should be run or updated after a change. + +3. **Dependency-aware navigation** + - Use `codegraph_callers` and `codegraph_callees` to understand how a symbol is used instead of guessing from imports. + +4. **Freshness check** + - Check `codegraph status` (or the MCP status tool) before relying on the graph; if the index is stale, run `codegraph sync` or `codegraph index`. + +5. **Daemon/watch mode** + - When running as an MCP server, CodeGraph's file watcher keeps the graph fresh. The instructions should tell jcode to rely on the daemon for incremental updates rather than re-triggering full indexing. + +### 3.6 Optional future enhancements + +1. **Mermaid output format** + - Add a `format: 'mermaid'` option to `codegraph_impact` and `codegraph_callers`/`callees` so jcode can render call graphs and impact diagrams in the side panel. This is high-value for visual reasoning but is a new feature, not just installer wiring. + +2. **jcode skill auto-endorsement** + - If jcode exposes a way to endorse skills programmatically, the installer could register the `codegraph` skill so it appears in `/skills` recommendations. + +3. **Memory-graph integration** + - jcode already maintains a semantic memory graph. A future integration could push CodeGraph summaries into jcode memory so cross-session context survives without re-indexing. + +--- + +## 4. Proposed Instruction Content + +### 4.1 `AGENTS.md` block (loaded every turn — must be short) + +```markdown + +## CodeGraph + +In repositories with a `.codegraph/` directory at the root, use CodeGraph before grep or file reads when you need to understand, locate, or modify code: + +- **Explore:** `codegraph_explore` returns the relevant symbols, their source, and the call paths between them in one call. Prefer it over separate grep/read chains. +- **Symbol focus:** `codegraph_node` gives one symbol's source plus its callers and callees. +- **Impact:** `codegraph_impact` before editing shows what a change would break. +- **Tests:** `codegraph_affected` finds test files that depend on changed source. +- **Status:** `codegraph_status` reports whether the index is fresh; if it is stale, run `codegraph sync` before exploring. + +If no `.codegraph/` directory exists, skip CodeGraph entirely — indexing is the user's decision. + +``` + +### 4.2 `~/.jcode/skills/codegraph/SKILL.md` (auto-injected when relevant) + +```markdown +# CodeGraph skill + +Use when the user asks about code structure, impact, callers/callees, symbol search, refactoring, debugging, or finding tests for a change. + +## One-shot exploration +- Prefer `codegraph_explore ` over a chain of `grep` + `read` calls. It returns ranked symbols, source snippets, and call paths in a single response. +- Use `maxFiles` to keep the response bounded. + +## Before modifying code +1. Run `codegraph_impact ` to see affected callers and files. +2. Run `codegraph_affected ` to find tests to update or run. +3. After the change, use `codegraph_status` to confirm the index is still fresh. + +## Navigation +- `codegraph_node ` — source, callers, and callees for one symbol. +- `codegraph_callers ` / `codegraph_callees ` — focused traversal. +- `codegraph_status` — check index freshness and last indexed time. + +## Avoid +- Do not re-read a file just to get line numbers when `codegraph_explore` or `codegraph_node` already returned the source. +- Do not run a full `codegraph index` unless the index is missing or the user explicitly asks; prefer `codegraph sync` for incremental updates. +``` + +--- + +## 5. Implementation Steps (Ready to Execute) + +1. **Create `src/installer/targets/jcode.ts`** + - Implement `JcodeTarget` class matching the `AgentTarget` interface. + - Use `getMcpServerConfig()` from `./shared.ts` but omit the `type` field when writing to jcode's JSON config (jcode's example does not include `type`). + - Use `upsertInstructionsEntry` (or a new jcode-specific helper) to write the `AGENTS.md` block. + - Add a helper to write the skill file under `~/.jcode/skills/codegraph/SKILL.md`. + +2. **Update `src/installer/targets/types.ts`** + - Add `'jcode'` to `TargetId`. + +3. **Update `src/installer/targets/registry.ts`** + - Import and include `jcodeTarget` in `ALL_TARGETS`. + +4. **Update `src/installer/instructions-template.ts` (optional)** + - Add a `JCODE_CODEGRAPH_INSTRUCTIONS_BLOCK` that is shorter and slash-command aware. + +5. **Add tests** + - Add `__tests__/installer-jcode.test.ts` (or similar) covering install, uninstall, re-install idempotency, and `printConfig`. + +6. **Update README and CLI help** + - Add jcode to the supported agents list and `install --target` examples. + +--- + +## 6. Risks & Dependencies + +- **jcode only supports stdio MCP servers.** CodeGraph is already stdio-based, so this is compatible. HTTP/SSE variants are not needed. +- **jcode imports Claude Code configs on first run.** If a user already has CodeGraph configured via Claude Code, jcode may pick it up automatically. The installer should still write native `.jcode/mcp.json` for clarity and project scoping. +- **Skill auto-injection is semantic.** The `codegraph` skill will not fire on every turn, which is good for token savings, but the agent must still remember to use the MCP tools from `AGENTS.md` when the skill is not loaded. The `AGENTS.md` block therefore remains the primary driver. +- **AGENTS.md length matters.** Because jcode loads it every turn, the instructions block must stay concise. Over-instruction will directly increase token spend. +- **No permissions/approval layer in jcode.** Unlike Claude Code's `settings.json` permissions, jcode does not appear to require an explicit allowlist. The installer only needs to write MCP config and instructions. + +--- + +## 7. Next Step / Decision + +The architecture problems are recorded above. The jcode integration plan is ready to implement. + +**Recommended execution order:** +1. Implement the jcode installer target (high value, low risk). +2. Add the jcode-specific instructions block and skill file. +3. Add tests and documentation. +4. After the integration is merged, tackle the recorded architecture issues in order of severity: Node engine mismatch, circular dependencies, workspace tooling, and then the large-scale god-class refactoring. + +**Awaiting approval to proceed with implementation.** diff --git a/src/installer/index.ts b/src/installer/index.ts index ace88614b..3c0dfb4de 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -3,7 +3,7 @@ * * Multi-target: writes MCP server config + instructions for the * agents the user picks (Claude Code, Cursor, Codex CLI, opencode, - * Hermes Agent, Gemini CLI, Antigravity IDE). + * Hermes Agent, Gemini CLI, Antigravity IDE, jcode). * Defaults to the Claude-only behavior for backwards compatibility * when no targets are explicitly chosen and nothing else is detected. * @@ -467,8 +467,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, ~/.jcode' }, + { value: 'local' as const, label: 'Just this project (local)', hint: './.claude, ./.cursor, ./opencode.jsonc, ./.gemini, ./.kiro, ./.jcode' }, ], initialValue: 'global' as const, }); diff --git a/src/installer/instructions-template.ts b/src/installer/instructions-template.ts index 924749190..da58b0bd0 100644 --- a/src/installer/instructions-template.ts +++ b/src/installer/instructions-template.ts @@ -49,3 +49,26 @@ In repositories indexed by CodeGraph (a \`.codegraph/\` directory exists at the If there is no \`.codegraph/\` directory, skip CodeGraph entirely — indexing is the user's decision. ${CODEGRAPH_SECTION_END}`; + +/** + * The jcode variant of the instructions block. jcode reads `AGENTS.md` every + * turn, so it is kept even shorter and explicitly pairs the graph with jcode's + * slash-command affordances (`/review`, `/test`, `/commit`) by nudging the agent + * to run `codegraph impact` / `codegraph affected` before editing. + * + * The shell commands (`codegraph impact`, `codegraph affected`, `codegraph status`, + * `codegraph sync`) are always available even when the hidden-by-default MCP tools + * are not exposed. This stays safely within the jcode context budget without + * duplicating the full server initialize instructions. + */ +export const JCODE_CODEGRAPH_INSTRUCTIONS_BLOCK = `${CODEGRAPH_SECTION_START} +## CodeGraph + +In repositories with a \`.codegraph/\` directory, use CodeGraph before grep or file reads: +- **MCP:** \`codegraph_explore\` answers most questions in one call. +- **Shell:** \`codegraph explore ""\` prints the same output. +- **Before edits:** \`codegraph impact \` and \`codegraph affected \` show what to update and test. +- **Freshness:** \`codegraph status\` checks the index; run \`codegraph sync\` if stale. + +If no \`.codegraph/\` directory exists, skip CodeGraph. +${CODEGRAPH_SECTION_END}`; diff --git a/src/installer/targets/jcode.ts b/src/installer/targets/jcode.ts new file mode 100644 index 000000000..34482105f --- /dev/null +++ b/src/installer/targets/jcode.ts @@ -0,0 +1,246 @@ +/** + * jcode target (https://jcode.sh). + * + * - MCP server entry to `~/.jcode/mcp.json` (global) or `./.jcode/mcp.json` + * (project-local). jcode reads both the canonical `mcpServers` key and the + * historical `servers` key, but we write `mcpServers` for clarity. + * - Instructions to `~/AGENTS.md` (global) or `./AGENTS.md` (project-local). + * jcode loads `AGENTS.md` at the repo root as project instructions and + * `~/AGENTS.md` as global agent instructions, both on every turn. + * - A jcode skill to `~/.jcode/skills/codegraph/SKILL.md` (global only). The + * skill auto-injects when the conversation is about code structure, + * impact, refactoring, or debugging — so the full playbook is only paid for + * when it is relevant. + * + * jcode supports stdio MCP servers only; CodeGraph is already stdio-based, so no + * `type` field is written in the MCP entry (the docs example omits it). + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { + AgentTarget, + DetectionResult, + InstallOptions, + Location, + WriteResult, +} from './types'; +import { + jsonDeepEqual, + readJsonFile, + removeMarkedSection, + replaceOrAppendMarkedSection, + writeJsonFile, +} from './shared'; +import { + CODEGRAPH_SECTION_END, + CODEGRAPH_SECTION_START, + JCODE_CODEGRAPH_INSTRUCTIONS_BLOCK, +} from '../instructions-template'; + +const SKILL_SECTION_START = ''; +const SKILL_SECTION_END = ''; + +const SKILL_BODY = `${SKILL_SECTION_START} +# CodeGraph skill + +Use when the user asks about code structure, impact, callers/callees, symbol search, refactoring, debugging, or finding tests for a change. + +## One-shot exploration +- Prefer \`codegraph_explore \` over chains of \`grep\` + \`read\`. It returns ranked symbols, source snippets, and call paths in one response. +- Use \`maxFiles\` to keep the response bounded. + +## Before modifying code +1. Run \`codegraph impact \` to see affected callers and files. +2. Run \`codegraph affected \` to find tests to update or run. +3. After the change, use \`codegraph status\` to confirm the index is fresh. + +## Navigation +- \`codegraph_node \` — source, callers, and callees for one symbol. +- \`codegraph_callers \` / \`codegraph_callees \` — focused traversal. +- \`codegraph_status\` — check index freshness and last indexed time. + +## Avoid +- Do not re-read a file just to get line numbers when \`codegraph_explore\` or \`codegraph_node\` already returned the source. +- Do not run a full \`codegraph index\` unless the index is missing or the user explicitly asks; prefer \`codegraph sync\` for incremental updates. +${SKILL_SECTION_END}`; + +function globalConfigDir(): string { + return path.join(os.homedir(), '.jcode'); +} + +function localConfigDir(): string { + return path.join(process.cwd(), '.jcode'); +} + +function configDir(loc: Location): string { + return loc === 'global' ? globalConfigDir() : localConfigDir(); +} + +function mcpJsonPath(loc: Location): string { + return path.join(configDir(loc), 'mcp.json'); +} + +function instructionsPath(loc: Location): string { + return loc === 'global' + ? path.join(os.homedir(), 'AGENTS.md') + : path.join(process.cwd(), 'AGENTS.md'); +} + +function skillDir(): string { + return path.join(globalConfigDir(), 'skills', 'codegraph'); +} + +function skillPath(): string { + return path.join(skillDir(), 'SKILL.md'); +} + +class JcodeTarget implements AgentTarget { + readonly id = 'jcode' as const; + readonly displayName = 'jcode'; + readonly docsUrl = 'https://jcode.sh/docs/config'; + + supportsLocation(_loc: Location): boolean { + return true; + } + + detect(loc: Location): DetectionResult { + const mcpPath = mcpJsonPath(loc); + const config = readJsonFile(mcpPath); + const alreadyConfigured = !!config.mcpServers?.codegraph || !!config.servers?.codegraph; + const dir = configDir(loc); + const installed = loc === 'global' + ? fs.existsSync(dir) || fs.existsSync(mcpPath) + : fs.existsSync(dir) || fs.existsSync(mcpPath); + return { installed, alreadyConfigured, configPath: mcpPath }; + } + + install(loc: Location, _opts: InstallOptions): WriteResult { + const files: WriteResult['files'] = []; + + files.push(writeMcpEntry(loc)); + files.push(upsertInstructionsEntry(loc)); + + if (loc === 'global') { + files.push(upsertSkillEntry()); + } + + return { files }; + } + + uninstall(loc: Location): WriteResult { + const files: WriteResult['files'] = []; + + const mcpPath = mcpJsonPath(loc); + const config = readJsonFile(mcpPath); + if (config.mcpServers?.codegraph || config.servers?.codegraph) { + if (config.mcpServers?.codegraph) delete config.mcpServers.codegraph; + if (config.mcpServers && Object.keys(config.mcpServers).length === 0) { + delete config.mcpServers; + } + if (config.servers?.codegraph) delete config.servers.codegraph; + if (config.servers && Object.keys(config.servers).length === 0) { + delete config.servers; + } + writeJsonFile(mcpPath, config); + files.push({ path: mcpPath, action: 'removed' }); + } else { + files.push({ path: mcpPath, action: 'not-found' }); + } + + files.push(removeInstructionsEntry(loc)); + + if (loc === 'global') { + files.push(removeSkillEntry()); + } + + return { files }; + } + + printConfig(loc: Location): string { + const target = mcpJsonPath(loc); + const snippet = JSON.stringify({ mcpServers: { codegraph: getJcodeServerEntry() } }, null, 2); + return `# Add to ${target}\n\n${snippet}\n`; + } + + describePaths(loc: Location): string[] { + const paths = [mcpJsonPath(loc), instructionsPath(loc)]; + if (loc === 'global') { + paths.push(skillPath()); + } + return paths; + } +} + +function getJcodeServerEntry(): { command: string; args: string[] } { + return { + command: 'codegraph', + args: ['serve', '--mcp'], + }; +} + +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 = getJcodeServerEntry(); + + if (jsonDeepEqual(before, after)) { + return { path: file, action: 'unchanged' }; + } + + if (!existing.mcpServers) existing.mcpServers = {}; + existing.mcpServers.codegraph = after; + // Drop any stale historical `servers` entry so the config is unambiguous. + if (existing.servers?.codegraph) delete existing.servers.codegraph; + if (existing.servers && Object.keys(existing.servers).length === 0) { + delete existing.servers; + } + + const action: 'created' | 'updated' = before ? 'updated' : (fs.existsSync(file) ? 'updated' : 'created'); + writeJsonFile(file, existing); + return { path: file, action }; +} + +function upsertInstructionsEntry(loc: Location): WriteResult['files'][number] { + const file = instructionsPath(loc); + const action = replaceOrAppendMarkedSection( + file, + JCODE_CODEGRAPH_INSTRUCTIONS_BLOCK, + CODEGRAPH_SECTION_START, + CODEGRAPH_SECTION_END, + ); + return { path: file, action: action === 'appended' ? 'updated' : action }; +} + +function removeInstructionsEntry(loc: Location): WriteResult['files'][number] { + const file = instructionsPath(loc); + const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END); + return { path: file, action }; +} + +function upsertSkillEntry(): WriteResult['files'][number] { + const file = skillPath(); + const dir = path.dirname(file); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + + const action = replaceOrAppendMarkedSection( + file, + SKILL_BODY, + SKILL_SECTION_START, + SKILL_SECTION_END, + ); + return { path: file, action: action === 'appended' ? 'updated' : action }; +} + +function removeSkillEntry(): WriteResult['files'][number] { + const file = skillPath(); + const action = removeMarkedSection(file, SKILL_SECTION_START, SKILL_SECTION_END); + return { path: file, action }; +} + +export const jcodeTarget: AgentTarget = new JcodeTarget(); diff --git a/src/installer/targets/registry.ts b/src/installer/targets/registry.ts index 5e929d468..55b00abbc 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 { jcodeTarget } from './jcode'; export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ claudeTarget, @@ -26,6 +27,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ geminiTarget, antigravityTarget, kiroTarget, + jcodeTarget, ]); export function getTarget(id: string): AgentTarget | undefined { diff --git a/src/installer/targets/types.ts b/src/installer/targets/types.ts index 833a801ae..08627429d 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' | 'jcode'; /** * Result of `target.detect(location)`.