fix(installer): stop duplicating agent instructions; MCP server is the single source of truth (#529) (#538)
The installer wrote a `## CodeGraph` usage block into each agent's
instructions file (CLAUDE.md / AGENTS.md / GEMINI.md / .cursor/rules /
Kiro steering) that duplicated, almost verbatim, the guidance the MCP
server already emits in its `initialize` response — so agents that
surface MCP instructions (Claude Code) read the same playbook twice
every turn.
All 6 instruction-writing targets (claude, cursor, codex, opencode,
gemini, kiro) now stop writing the block. install self-heals by
stripping a block a previous version wrote (uninstall already did), so
the next `codegraph install`/`uninstall` cleans up existing installs;
upgrading the package alone does not (the leftover block is harmless).
server-instructions.ts is now the single source of truth — the two
steers unique to the old template ("trust codegraph, don't re-verify
with grep" and the not-initialized -> `init -i` hint) are ported there.
Removes the now-dead INSTRUCTIONS_TEMPLATE / CLAUDE_MD_TEMPLATE,
claude-md-template.ts, writeClaudeMd / hasClaudeMdSection, and the
Cursor-only wireProjectSurfaces bootstrap. The install log learned a
"Removed" verb. Tests rewritten to the new contract + self-heal
coverage (140/140 installer tests pass).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cea78ceb1b
commit
a9c9e76d8c
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Backwards-compat re-export shim.
|
||||
*
|
||||
* The instructions template moved to `instructions-template.ts` so it
|
||||
* can be shared across all agent targets (Claude Code, Cursor, Codex
|
||||
* CLI, opencode). This file is preserved purely so existing imports
|
||||
* (`@colbymchenry/codegraph` consumers, downstream tooling) keep
|
||||
* working unchanged. New code should import from
|
||||
* `./instructions-template` directly.
|
||||
*
|
||||
* @deprecated Import from `./instructions-template` instead.
|
||||
*/
|
||||
|
||||
export {
|
||||
CODEGRAPH_SECTION_START,
|
||||
CODEGRAPH_SECTION_END,
|
||||
CLAUDE_MD_TEMPLATE,
|
||||
INSTRUCTIONS_TEMPLATE,
|
||||
} from './instructions-template';
|
||||
@@ -11,13 +11,11 @@
|
||||
* abstraction instead.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import {
|
||||
writeMcpEntry,
|
||||
writePermissionsEntry,
|
||||
writeInstructionsEntry,
|
||||
} from './targets/claude';
|
||||
import { readJsonFile } from './targets/shared';
|
||||
|
||||
@@ -25,9 +23,13 @@ export type InstallLocation = 'global' | 'local';
|
||||
|
||||
/**
|
||||
* Each shim calls ONLY the named per-file helper — writeMcpConfig
|
||||
* writes only the MCP JSON, writePermissions only settings.json,
|
||||
* writeClaudeMd only CLAUDE.md. The full multi-file install lives
|
||||
* in `claudeTarget.install()` which the new orchestrator uses.
|
||||
* writes only the MCP JSON, writePermissions only settings.json. The
|
||||
* full multi-file install lives in `claudeTarget.install()` which the
|
||||
* new orchestrator uses.
|
||||
*
|
||||
* There is no `writeClaudeMd` shim anymore: codegraph stopped writing a
|
||||
* CLAUDE.md instructions block (issue #529) now that the MCP server's
|
||||
* `initialize` instructions are the single source of truth.
|
||||
*/
|
||||
export function writeMcpConfig(location: InstallLocation): void {
|
||||
writeMcpEntry(location);
|
||||
@@ -37,14 +39,6 @@ export function writePermissions(location: InstallLocation): void {
|
||||
writePermissionsEntry(location);
|
||||
}
|
||||
|
||||
export function writeClaudeMd(location: InstallLocation): { created: boolean; updated: boolean } {
|
||||
const file = writeInstructionsEntry(location);
|
||||
return {
|
||||
created: file.action === 'created',
|
||||
updated: file.action === 'updated',
|
||||
};
|
||||
}
|
||||
|
||||
export function hasMcpConfig(location: InstallLocation): boolean {
|
||||
// local scope lives in ./.mcp.json (project scope); global is the
|
||||
// user-scope ~/.claude.json. Mirrors the Claude target's paths.
|
||||
@@ -64,16 +58,3 @@ export function hasPermissions(location: InstallLocation): boolean {
|
||||
if (!Array.isArray(allow)) return false;
|
||||
return allow.some((p: string) => p.startsWith('mcp__codegraph__'));
|
||||
}
|
||||
|
||||
export function hasClaudeMdSection(location: InstallLocation): boolean {
|
||||
const file = location === 'global'
|
||||
? path.join(os.homedir(), '.claude', 'CLAUDE.md')
|
||||
: path.join(process.cwd(), '.claude', 'CLAUDE.md');
|
||||
try {
|
||||
if (!fs.existsSync(file)) return false;
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
return content.includes('<!-- CODEGRAPH_START -->') || content.includes('## CodeGraph');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-36
@@ -21,7 +21,7 @@ import {
|
||||
getTarget,
|
||||
resolveTargetFlag,
|
||||
} from './targets/registry';
|
||||
import type { AgentTarget, Location, TargetId, WriteResult } from './targets/types';
|
||||
import type { AgentTarget, Location, TargetId } from './targets/types';
|
||||
import { getGlyphs } from '../ui/glyphs';
|
||||
// Import the lightweight submodules directly (not the ../sync barrel, which
|
||||
// re-exports FileWatcher and would transitively pull in ../extraction — the
|
||||
@@ -35,10 +35,8 @@ import { isGitRepo, isSyncHookInstalled, installGitSyncHook } from '../sync/git-
|
||||
export {
|
||||
writeMcpConfig,
|
||||
writePermissions,
|
||||
writeClaudeMd,
|
||||
hasMcpConfig,
|
||||
hasPermissions,
|
||||
hasClaudeMdSection,
|
||||
} from './config-writer';
|
||||
export type { InstallLocation } from './config-writer';
|
||||
|
||||
@@ -194,7 +192,9 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
|
||||
for (const file of result.files) {
|
||||
const verb = file.action === 'unchanged'
|
||||
? 'Unchanged'
|
||||
: file.action === 'created' ? 'Created' : 'Updated';
|
||||
: file.action === 'created' ? 'Created'
|
||||
: file.action === 'removed' ? 'Removed'
|
||||
: 'Updated';
|
||||
clack.log.success(`${target.displayName}: ${verb} ${tildify(file.path)}`);
|
||||
}
|
||||
for (const note of result.notes ?? []) {
|
||||
@@ -378,38 +378,6 @@ export async function runUninstaller(opts: RunUninstallerOptions): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For every target that has a global config and exposes
|
||||
* `wireProjectSurfaces`, write its project-local surfaces (e.g.
|
||||
* Cursor's `.cursor/rules/codegraph.mdc`). Idempotent — runs
|
||||
* silently when there's nothing to write.
|
||||
*
|
||||
* Called by `codegraph init` so that a user who ran
|
||||
* `codegraph install` once globally doesn't have to re-run it per
|
||||
* project to get full agent support.
|
||||
*
|
||||
* Returns the list of `(target, file)` pairs that were created or
|
||||
* updated — caller decides how to surface them.
|
||||
*/
|
||||
export function wireProjectSurfacesForGlobalAgents(): Array<{
|
||||
target: AgentTarget;
|
||||
file: WriteResult['files'][number];
|
||||
}> {
|
||||
const written: Array<{ target: AgentTarget; file: WriteResult['files'][number] }> = [];
|
||||
for (const target of ALL_TARGETS) {
|
||||
if (typeof target.wireProjectSurfaces !== 'function') continue;
|
||||
const detection = target.detect('global');
|
||||
if (!detection.alreadyConfigured) continue;
|
||||
const result = target.wireProjectSurfaces();
|
||||
for (const file of result.files) {
|
||||
if (file.action === 'created' || file.action === 'updated') {
|
||||
written.push({ target, file });
|
||||
}
|
||||
}
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace home-directory prefix in a path with `~/` for cleaner log
|
||||
* lines. Pure cosmetic.
|
||||
|
||||
@@ -1,64 +1,18 @@
|
||||
/**
|
||||
* Agent-instructions template — the markdown body each agent target
|
||||
* writes into its conventional instructions file (CLAUDE.md /
|
||||
* AGENTS.md / codegraph.mdc / etc.).
|
||||
* Marker constants for the legacy agent-instructions block.
|
||||
*
|
||||
* The body content is identical across agents because the codegraph
|
||||
* usage advice is agent-agnostic — only the destination filename and
|
||||
* any optional frontmatter (Cursor `.mdc`) varies per target.
|
||||
* Codegraph used to write a `## CodeGraph` usage guide into each
|
||||
* agent's instructions file (CLAUDE.md / AGENTS.md / GEMINI.md /
|
||||
* codegraph.mdc / Kiro steering doc). That duplicated the guidance the
|
||||
* MCP server already emits in its `initialize` response — every agent
|
||||
* read the same playbook twice each turn (issue #529). The installer no
|
||||
* longer writes an instructions file; the MCP server instructions in
|
||||
* `mcp/server-instructions.ts` are the single source of truth.
|
||||
*
|
||||
* The legacy `claude-md-template.ts` re-exports these names for
|
||||
* backwards compatibility with downstream importers.
|
||||
* These markers are retained so install (self-heal on upgrade) and
|
||||
* uninstall can find and strip the block a previous install wrote.
|
||||
*/
|
||||
|
||||
/** Markers used by the marker-based section replacement. */
|
||||
/** Markers used by the marker-based section removal. */
|
||||
export const CODEGRAPH_SECTION_START = '<!-- CODEGRAPH_START -->';
|
||||
export const CODEGRAPH_SECTION_END = '<!-- CODEGRAPH_END -->';
|
||||
|
||||
/**
|
||||
* The full marker-delimited block written into each agent's
|
||||
* instructions file. Includes the start/end markers so the section
|
||||
* can be detected and replaced on re-install.
|
||||
*/
|
||||
export const INSTRUCTIONS_TEMPLATE = `${CODEGRAPH_SECTION_START}
|
||||
## CodeGraph
|
||||
|
||||
This project has a CodeGraph MCP server (\`codegraph_*\` tools) configured. CodeGraph is a tree-sitter-parsed knowledge graph of every symbol, edge, and file. Reads are sub-millisecond and return structural information grep cannot.
|
||||
|
||||
### When to prefer codegraph over native search
|
||||
|
||||
Use codegraph for **structural** questions — what calls what, what would break, where is X defined, what is X's signature. Use native grep/read only for **literal text** queries (string contents, comments, log messages) or after you already have a specific file open.
|
||||
|
||||
| Question | Tool |
|
||||
|---|---|
|
||||
| "Where is X defined?" / "Find symbol named X" | \`codegraph_search\` |
|
||||
| "What calls function Y?" | \`codegraph_callers\` |
|
||||
| "What does Y call?" | \`codegraph_callees\` |
|
||||
| "How does X reach/become Y? / trace the flow from X to Y" | \`codegraph_trace\` (one call = the whole path, incl. callback/React/JSX dynamic hops) |
|
||||
| "What would break if I changed Z?" | \`codegraph_impact\` |
|
||||
| "Show me Y's signature / source / docstring" | \`codegraph_node\` |
|
||||
| "Give me focused context for a task/area" | \`codegraph_context\` |
|
||||
| "See several related symbols' source at once" | \`codegraph_explore\` |
|
||||
| "What files exist under path/" | \`codegraph_files\` |
|
||||
| "Is the index healthy?" | \`codegraph_status\` |
|
||||
|
||||
### Rules of thumb
|
||||
|
||||
- **Answer directly — don't delegate exploration.** For "how does X work" / architecture questions, answer with 2-3 codegraph calls: \`codegraph_context\` first, then ONE \`codegraph_explore\` for the source of the symbols it surfaces. For a specific **flow** ("how does X reach Y") start with \`codegraph_trace\` from→to — one call returns the whole path with dynamic hops bridged — then ONE \`codegraph_explore\` for the bodies; don't rebuild the path with \`codegraph_search\` + \`codegraph_callers\`. Codegraph IS the pre-built index, so spawning a separate file-reading sub-task/agent — or running a grep + read loop — repeats work codegraph already did and costs more for the same answer.
|
||||
- **Trust codegraph results.** They come from a full AST parse. Do NOT re-verify them with grep — that's slower, less accurate, and wastes context.
|
||||
- **Don't grep first** when looking up a symbol by name. \`codegraph_search\` is faster and returns kind + location + signature in one call.
|
||||
- **Don't chain \`codegraph_search\` + \`codegraph_node\`** when you just want context — \`codegraph_context\` is one call.
|
||||
- **Don't loop \`codegraph_node\` over many symbols** — one \`codegraph_explore\` call returns several symbols' source grouped in a single capped call, while each separate node/Read call re-reads the whole context and costs far more.
|
||||
- **Index lag — check the staleness banner, don't guess a wait.** When a codegraph response starts with "⚠️ Some files referenced below were edited since the last index sync…", the listed files are pending re-index — Read those specific files for accurate content. Files NOT in that banner are fresh and codegraph is authoritative for them. \`codegraph_status\` also lists pending files under "Pending sync".
|
||||
|
||||
### If \`.codegraph/\` doesn't exist
|
||||
|
||||
The MCP server returns "not initialized." Ask the user: *"I notice this project doesn't have CodeGraph initialized. Want me to run \`codegraph init -i\` to build the index?"*
|
||||
${CODEGRAPH_SECTION_END}`;
|
||||
|
||||
/**
|
||||
* Backwards-compat alias. Existing downstream code may import
|
||||
* `CLAUDE_MD_TEMPLATE` from this module via the re-export shim in
|
||||
* `claude-md-template.ts`.
|
||||
*/
|
||||
export const CLAUDE_MD_TEMPLATE = INSTRUCTIONS_TEMPLATE;
|
||||
|
||||
@@ -28,19 +28,16 @@ import {
|
||||
WriteResult,
|
||||
} from './types';
|
||||
import {
|
||||
atomicWriteFileSync,
|
||||
getCodeGraphPermissions,
|
||||
getMcpServerConfig,
|
||||
jsonDeepEqual,
|
||||
readJsonFile,
|
||||
removeMarkedSection,
|
||||
replaceOrAppendMarkedSection,
|
||||
writeJsonFile,
|
||||
} from './shared';
|
||||
import {
|
||||
CODEGRAPH_SECTION_END,
|
||||
CODEGRAPH_SECTION_START,
|
||||
INSTRUCTIONS_TEMPLATE,
|
||||
} from '../instructions-template';
|
||||
|
||||
function configDir(loc: Location): string {
|
||||
@@ -123,8 +120,15 @@ class ClaudeCodeTarget implements AgentTarget {
|
||||
const hookCleanup = cleanupLegacyHooks(loc);
|
||||
if (hookCleanup.action === 'removed') files.push(hookCleanup);
|
||||
|
||||
// 3. CLAUDE.md instructions
|
||||
files.push(writeInstructionsEntry(loc));
|
||||
// 3. CLAUDE.md instructions — no longer written. The codegraph
|
||||
// usage guidance now ships solely in the MCP server's `initialize`
|
||||
// response (see `mcp/server-instructions.ts`), which Claude Code
|
||||
// surfaces in the system prompt automatically. Writing it into
|
||||
// CLAUDE.md as well meant the agent read the same playbook twice
|
||||
// every turn (issue #529). Strip any block a previous install left
|
||||
// behind so an upgrade self-heals — same idiom as the hook cleanup.
|
||||
const instrCleanup = removeInstructionsEntry(loc);
|
||||
if (instrCleanup.action === 'removed') files.push(instrCleanup);
|
||||
|
||||
return { files };
|
||||
}
|
||||
@@ -185,10 +189,8 @@ class ClaudeCodeTarget implements AgentTarget {
|
||||
const hookCleanup = cleanupLegacyHooks(loc);
|
||||
if (hookCleanup.action === 'removed') files.push(hookCleanup);
|
||||
|
||||
// 3. Instructions
|
||||
const instr = instructionsPath(loc);
|
||||
const action = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
|
||||
files.push({ path: instr, action });
|
||||
// 3. Instructions — strip the legacy CodeGraph block if present.
|
||||
files.push(removeInstructionsEntry(loc));
|
||||
|
||||
return { files };
|
||||
}
|
||||
@@ -359,48 +361,19 @@ export function writePermissionsEntry(loc: Location): WriteResult['files'][numbe
|
||||
return { path: file, action: created ? 'created' : 'updated' };
|
||||
}
|
||||
|
||||
export function writeInstructionsEntry(loc: Location): WriteResult['files'][number] {
|
||||
/**
|
||||
* Strip the marker-delimited CodeGraph block from CLAUDE.md if a prior
|
||||
* install wrote one. Codegraph no longer maintains an instructions file
|
||||
* (issue #529) — the MCP server's `initialize` instructions are the
|
||||
* single source of truth — so both install (self-heal on upgrade) and
|
||||
* uninstall call this. `removeMarkedSection` returns `not-found`/`kept`
|
||||
* when there's nothing to strip; the install caller drops those from
|
||||
* the report so a fresh install stays quiet.
|
||||
*/
|
||||
export function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
|
||||
const file = instructionsPath(loc);
|
||||
// Ensure config dir exists (for global ~/.claude/).
|
||||
const dir = path.dirname(file);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
// Honor the legacy "unmarked ## CodeGraph" rewrite path that the
|
||||
// original installer supported (some users hand-pasted a section
|
||||
// before markers existed). Detect first and migrate inline.
|
||||
if (fs.existsSync(file)) {
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
if (!content.includes(CODEGRAPH_SECTION_START)) {
|
||||
const headerMatch = content.match(/\n## CodeGraph\n/);
|
||||
if (headerMatch && headerMatch.index !== undefined) {
|
||||
const sectionStart = headerMatch.index;
|
||||
const after = content.substring(sectionStart + 1);
|
||||
const nextHeader = after.match(/\n## (?!#)/);
|
||||
const sectionEnd = nextHeader && nextHeader.index !== undefined
|
||||
? sectionStart + 1 + nextHeader.index
|
||||
: content.length;
|
||||
const merged =
|
||||
content.substring(0, sectionStart) +
|
||||
'\n' + INSTRUCTIONS_TEMPLATE +
|
||||
content.substring(sectionEnd);
|
||||
atomicWriteFileSync(file, merged);
|
||||
return { path: file, action: 'updated' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const action = replaceOrAppendMarkedSection(
|
||||
file,
|
||||
INSTRUCTIONS_TEMPLATE,
|
||||
CODEGRAPH_SECTION_START,
|
||||
CODEGRAPH_SECTION_END,
|
||||
);
|
||||
// Map the four-state action to WriteResult's action vocabulary.
|
||||
const mapped: 'created' | 'updated' | 'unchanged' =
|
||||
action === 'created' ? 'created'
|
||||
: action === 'unchanged' ? 'unchanged'
|
||||
: 'updated';
|
||||
return { path: file, action: mapped };
|
||||
const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
|
||||
return { path: file, action };
|
||||
}
|
||||
|
||||
export const claudeTarget: AgentTarget = new ClaudeCodeTarget();
|
||||
|
||||
@@ -28,12 +28,10 @@ import {
|
||||
atomicWriteFileSync,
|
||||
getMcpServerConfig,
|
||||
removeMarkedSection,
|
||||
replaceOrAppendMarkedSection,
|
||||
} from './shared';
|
||||
import {
|
||||
CODEGRAPH_SECTION_END,
|
||||
CODEGRAPH_SECTION_START,
|
||||
INSTRUCTIONS_TEMPLATE,
|
||||
} from '../instructions-template';
|
||||
import { buildTomlTable, removeTomlTable, upsertTomlTable } from './toml';
|
||||
|
||||
@@ -84,7 +82,12 @@ class CodexTarget implements AgentTarget {
|
||||
const files: WriteResult['files'] = [];
|
||||
|
||||
files.push(writeMcpEntry());
|
||||
files.push(writeInstructionsEntry());
|
||||
|
||||
// AGENTS.md is no longer written — the codegraph usage guidance
|
||||
// ships in the MCP server's `initialize` response (issue #529).
|
||||
// Strip a block a previous install left so an upgrade self-heals.
|
||||
const instrCleanup = removeInstructionsEntry();
|
||||
if (instrCleanup.action === 'removed') files.push(instrCleanup);
|
||||
|
||||
return { files };
|
||||
}
|
||||
@@ -111,9 +114,7 @@ class CodexTarget implements AgentTarget {
|
||||
files.push({ path: tomlPath, action: 'not-found' });
|
||||
}
|
||||
|
||||
const instr = instructionsPath();
|
||||
const instrAction = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
|
||||
files.push({ path: instr, action: instrAction });
|
||||
files.push(removeInstructionsEntry());
|
||||
|
||||
return { files };
|
||||
}
|
||||
@@ -160,22 +161,15 @@ function writeMcpEntry(): WriteResult['files'][number] {
|
||||
return { path: file, action: created ? 'created' : 'updated' };
|
||||
}
|
||||
|
||||
function writeInstructionsEntry(): WriteResult['files'][number] {
|
||||
/**
|
||||
* Strip the marker-delimited CodeGraph block from `~/.codex/AGENTS.md`
|
||||
* if a prior install wrote one. Used by both install (self-heal on
|
||||
* upgrade) and uninstall — see issue #529.
|
||||
*/
|
||||
function removeInstructionsEntry(): WriteResult['files'][number] {
|
||||
const file = instructionsPath();
|
||||
const dir = path.dirname(file);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
const action = replaceOrAppendMarkedSection(
|
||||
file,
|
||||
INSTRUCTIONS_TEMPLATE,
|
||||
CODEGRAPH_SECTION_START,
|
||||
CODEGRAPH_SECTION_END,
|
||||
);
|
||||
const mapped: 'created' | 'updated' | 'unchanged' =
|
||||
action === 'created' ? 'created'
|
||||
: action === 'unchanged' ? 'unchanged'
|
||||
: 'updated';
|
||||
return { path: file, action: mapped };
|
||||
const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
|
||||
return { path: file, action };
|
||||
}
|
||||
|
||||
export const codexTarget: AgentTarget = new CodexTarget();
|
||||
|
||||
@@ -46,13 +46,11 @@ import {
|
||||
getMcpServerConfig,
|
||||
jsonDeepEqual,
|
||||
readJsonFile,
|
||||
replaceOrAppendMarkedSection,
|
||||
writeJsonFile,
|
||||
} from './shared';
|
||||
import {
|
||||
CODEGRAPH_SECTION_END,
|
||||
CODEGRAPH_SECTION_START,
|
||||
INSTRUCTIONS_TEMPLATE,
|
||||
} from '../instructions-template';
|
||||
|
||||
function mcpJsonPath(loc: Location): string {
|
||||
@@ -112,8 +110,13 @@ class CursorTarget implements AgentTarget {
|
||||
|
||||
files.push(writeMcpEntry(loc));
|
||||
|
||||
// We no longer write `.cursor/rules/codegraph.mdc` — the codegraph
|
||||
// usage guidance ships in the MCP server's `initialize` response,
|
||||
// the single source of truth (issue #529). Strip a rules file a
|
||||
// previous install created so an upgrade self-heals.
|
||||
if (loc === 'local') {
|
||||
files.push(writeRulesEntry());
|
||||
const rulesCleanup = removeRulesEntry();
|
||||
if (rulesCleanup.action === 'removed') files.push(rulesCleanup);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -156,16 +159,6 @@ class CursorTarget implements AgentTarget {
|
||||
? [mcpJsonPath(loc), rulesPath()]
|
||||
: [mcpJsonPath(loc)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the project-local `.cursor/rules/codegraph.mdc` file. Used
|
||||
* by `codegraph init` to bootstrap projects that have only the
|
||||
* global `~/.cursor/mcp.json` — without the rules file, the Cursor
|
||||
* agent has no signal to prefer codegraph over native grep.
|
||||
*/
|
||||
wireProjectSurfaces(): WriteResult {
|
||||
return { files: [writeRulesEntry()] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,45 +190,9 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
|
||||
return { path: file, action };
|
||||
}
|
||||
|
||||
function writeRulesEntry(): WriteResult['files'][number] {
|
||||
const file = rulesPath();
|
||||
const dir = path.dirname(file);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
// Body is frontmatter + the shared instructions block. The
|
||||
// marker-based replacement targets only the marker block, so the
|
||||
// frontmatter is preserved across re-runs.
|
||||
const body = MDC_FRONTMATTER + INSTRUCTIONS_TEMPLATE;
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
atomicWriteFileSync(file, body + '\n');
|
||||
return { path: file, action: 'created' };
|
||||
}
|
||||
|
||||
// For .mdc files we own outright, do byte-equality first.
|
||||
const existing = fs.readFileSync(file, 'utf-8');
|
||||
const wantWithNL = body + '\n';
|
||||
if (existing === wantWithNL) {
|
||||
return { path: file, action: 'unchanged' };
|
||||
}
|
||||
|
||||
// Otherwise, marker-based section swap (preserves any user-added
|
||||
// content outside the markers).
|
||||
const action = replaceOrAppendMarkedSection(
|
||||
file,
|
||||
INSTRUCTIONS_TEMPLATE,
|
||||
CODEGRAPH_SECTION_START,
|
||||
CODEGRAPH_SECTION_END,
|
||||
);
|
||||
const mapped: 'created' | 'updated' | 'unchanged' =
|
||||
action === 'created' ? 'created'
|
||||
: action === 'unchanged' ? 'unchanged'
|
||||
: 'updated';
|
||||
return { path: file, action: mapped };
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the Cursor rules file on uninstall.
|
||||
* Remove the Cursor rules file on uninstall (and as a self-heal on
|
||||
* install — see issue #529).
|
||||
*
|
||||
* Unlike the shared CLAUDE.md / AGENTS.md files (where codegraph owns
|
||||
* only a marker-delimited section), `.cursor/rules/codegraph.mdc` is a
|
||||
|
||||
@@ -37,13 +37,11 @@ import {
|
||||
jsonDeepEqual,
|
||||
readJsonFile,
|
||||
removeMarkedSection,
|
||||
replaceOrAppendMarkedSection,
|
||||
writeJsonFile,
|
||||
} from './shared';
|
||||
import {
|
||||
CODEGRAPH_SECTION_END,
|
||||
CODEGRAPH_SECTION_START,
|
||||
INSTRUCTIONS_TEMPLATE,
|
||||
} from '../instructions-template';
|
||||
|
||||
function configDir(loc: Location): string {
|
||||
@@ -85,7 +83,13 @@ class GeminiTarget implements AgentTarget {
|
||||
install(loc: Location, _opts: InstallOptions): WriteResult {
|
||||
const files: WriteResult['files'] = [];
|
||||
files.push(writeMcpEntry(loc));
|
||||
files.push(writeInstructionsEntry(loc));
|
||||
|
||||
// GEMINI.md is no longer written — the codegraph usage guidance
|
||||
// ships in the MCP server's `initialize` response (issue #529).
|
||||
// Strip a block a previous install left so an upgrade self-heals.
|
||||
const instrCleanup = removeInstructionsEntry(loc);
|
||||
if (instrCleanup.action === 'removed') files.push(instrCleanup);
|
||||
|
||||
return { files };
|
||||
}
|
||||
|
||||
@@ -108,9 +112,7 @@ class GeminiTarget implements AgentTarget {
|
||||
files.push({ path: file, action: 'not-found' });
|
||||
}
|
||||
|
||||
const instr = instructionsPath(loc);
|
||||
const action = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
|
||||
files.push({ path: instr, action });
|
||||
files.push(removeInstructionsEntry(loc));
|
||||
|
||||
return { files };
|
||||
}
|
||||
@@ -146,22 +148,15 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
|
||||
return { path: file, action };
|
||||
}
|
||||
|
||||
function writeInstructionsEntry(loc: Location): WriteResult['files'][number] {
|
||||
/**
|
||||
* Strip the marker-delimited CodeGraph block from GEMINI.md if a prior
|
||||
* install wrote one. Used by both install (self-heal on upgrade) and
|
||||
* uninstall — see issue #529.
|
||||
*/
|
||||
function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
|
||||
const file = instructionsPath(loc);
|
||||
const dir = path.dirname(file);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
const action = replaceOrAppendMarkedSection(
|
||||
file,
|
||||
INSTRUCTIONS_TEMPLATE,
|
||||
CODEGRAPH_SECTION_START,
|
||||
CODEGRAPH_SECTION_END,
|
||||
);
|
||||
const mapped: 'created' | 'updated' | 'unchanged' =
|
||||
action === 'created' ? 'created'
|
||||
: action === 'unchanged' ? 'unchanged'
|
||||
: 'updated';
|
||||
return { path: file, action: mapped };
|
||||
const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
|
||||
return { path: file, action };
|
||||
}
|
||||
|
||||
export const geminiTarget: AgentTarget = new GeminiTarget();
|
||||
|
||||
@@ -34,13 +34,11 @@ import {
|
||||
WriteResult,
|
||||
} from './types';
|
||||
import {
|
||||
atomicWriteFileSync,
|
||||
getMcpServerConfig,
|
||||
jsonDeepEqual,
|
||||
readJsonFile,
|
||||
writeJsonFile,
|
||||
} from './shared';
|
||||
import { INSTRUCTIONS_TEMPLATE } from '../instructions-template';
|
||||
|
||||
function configDir(loc: Location): string {
|
||||
return loc === 'global'
|
||||
@@ -76,7 +74,14 @@ class KiroTarget implements AgentTarget {
|
||||
install(loc: Location, _opts: InstallOptions): WriteResult {
|
||||
const files: WriteResult['files'] = [];
|
||||
files.push(writeMcpEntry(loc));
|
||||
files.push(writeSteeringEntry(loc));
|
||||
|
||||
// The steering doc is no longer written — the codegraph usage
|
||||
// guidance ships in the MCP server's `initialize` response (issue
|
||||
// #529). Delete a `codegraph.md` a previous install created so an
|
||||
// upgrade self-heals.
|
||||
const steeringCleanup = removeSteeringEntry(loc);
|
||||
if (steeringCleanup.action === 'removed') files.push(steeringCleanup);
|
||||
|
||||
return {
|
||||
files,
|
||||
// The IDE-only enable-MCP step is load-bearing: Kiro IDE ships
|
||||
@@ -143,37 +148,12 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
|
||||
return { path: file, action };
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the dedicated steering file. Unlike CLAUDE.md / GEMINI.md
|
||||
* (shared files where codegraph owns a marker-delimited section),
|
||||
* Kiro's steering dir loads every `*.md` as a discrete document — so
|
||||
* `codegraph.md` is ours outright. Byte-equality short-circuits
|
||||
* idempotent re-runs; mismatched content gets a clean rewrite.
|
||||
*/
|
||||
function writeSteeringEntry(loc: Location): WriteResult['files'][number] {
|
||||
const file = steeringPath(loc);
|
||||
const dir = path.dirname(file);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
const body = INSTRUCTIONS_TEMPLATE + '\n';
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
atomicWriteFileSync(file, body);
|
||||
return { path: file, action: 'created' };
|
||||
}
|
||||
const existing = fs.readFileSync(file, 'utf-8');
|
||||
if (existing === body) {
|
||||
return { path: file, action: 'unchanged' };
|
||||
}
|
||||
atomicWriteFileSync(file, body);
|
||||
return { path: file, action: 'updated' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the steering file we own. If a user has hand-edited the file
|
||||
* out of recognition we still remove it — codegraph.md is a name we
|
||||
* claim, and a partial install leaving the file behind is worse than
|
||||
* a clean delete.
|
||||
* a clean delete. Used by both install (self-heal on upgrade — see
|
||||
* issue #529) and uninstall.
|
||||
*/
|
||||
function removeSteeringEntry(loc: Location): WriteResult['files'][number] {
|
||||
const file = steeringPath(loc);
|
||||
|
||||
@@ -41,12 +41,10 @@ import {
|
||||
atomicWriteFileSync,
|
||||
jsonDeepEqual,
|
||||
removeMarkedSection,
|
||||
replaceOrAppendMarkedSection,
|
||||
} from './shared';
|
||||
import {
|
||||
CODEGRAPH_SECTION_END,
|
||||
CODEGRAPH_SECTION_START,
|
||||
INSTRUCTIONS_TEMPLATE,
|
||||
} from '../instructions-template';
|
||||
|
||||
function globalConfigDir(): string {
|
||||
@@ -128,7 +126,13 @@ class OpencodeTarget implements AgentTarget {
|
||||
install(loc: Location, _opts: InstallOptions): WriteResult {
|
||||
const files: WriteResult['files'] = [];
|
||||
files.push(writeMcpEntry(loc));
|
||||
files.push(writeInstructionsEntry(loc));
|
||||
|
||||
// AGENTS.md is no longer written — the codegraph usage guidance
|
||||
// ships in the MCP server's `initialize` response (issue #529).
|
||||
// Strip a block a previous install left so an upgrade self-heals.
|
||||
const instrCleanup = removeInstructionsEntry(loc);
|
||||
if (instrCleanup.action === 'removed') files.push(instrCleanup);
|
||||
|
||||
return { files };
|
||||
}
|
||||
|
||||
@@ -163,9 +167,7 @@ class OpencodeTarget implements AgentTarget {
|
||||
}
|
||||
}
|
||||
|
||||
const instr = instructionsPath(loc);
|
||||
const instrAction = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
|
||||
files.push({ path: instr, action: instrAction });
|
||||
files.push(removeInstructionsEntry(loc));
|
||||
|
||||
return { files };
|
||||
}
|
||||
@@ -223,22 +225,15 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
|
||||
return { path: file, action: existed ? 'updated' : 'created' };
|
||||
}
|
||||
|
||||
function writeInstructionsEntry(loc: Location): WriteResult['files'][number] {
|
||||
/**
|
||||
* Strip the marker-delimited CodeGraph block from AGENTS.md if a prior
|
||||
* install wrote one. Used by both install (self-heal on upgrade) and
|
||||
* uninstall — see issue #529.
|
||||
*/
|
||||
function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
|
||||
const file = instructionsPath(loc);
|
||||
const dir = path.dirname(file);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
const action = replaceOrAppendMarkedSection(
|
||||
file,
|
||||
INSTRUCTIONS_TEMPLATE,
|
||||
CODEGRAPH_SECTION_START,
|
||||
CODEGRAPH_SECTION_END,
|
||||
);
|
||||
const mapped: 'created' | 'updated' | 'unchanged' =
|
||||
action === 'created' ? 'created'
|
||||
: action === 'unchanged' ? 'unchanged'
|
||||
: 'updated';
|
||||
return { path: file, action: mapped };
|
||||
const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
|
||||
return { path: file, action };
|
||||
}
|
||||
|
||||
export const opencodeTarget: AgentTarget = new OpencodeTarget();
|
||||
|
||||
@@ -103,19 +103,4 @@ export interface AgentTarget {
|
||||
printConfig(loc: Location): string;
|
||||
/** Filesystem paths this target would write to at this location. */
|
||||
describePaths(loc: Location): string[];
|
||||
/**
|
||||
* Optional. Write any project-local surfaces this target needs in
|
||||
* order to work fully when its MCP config is configured globally.
|
||||
* Called by `codegraph init` to bootstrap new projects without
|
||||
* forcing the user to re-run `codegraph install` per project.
|
||||
*
|
||||
* Most targets need nothing here — their global config is complete.
|
||||
* Cursor is the notable exception: its rules system
|
||||
* (`.cursor/rules/*.mdc`) is project-scoped only, and is what makes
|
||||
* Cursor's agent prefer codegraph over its built-in grep.
|
||||
*
|
||||
* Must be idempotent. Targets that have nothing project-local omit
|
||||
* the method entirely.
|
||||
*/
|
||||
wireProjectSurfaces?(): WriteResult;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user