Skip to content

Commit 58c1414

Browse files
colbymchenryclaude
andauthored
fix(installer): opencode .jsonc + AGENTS.md (0.7.8) (colbymchenry#163)
* release: 0.7.7 (multi-agent installer — Cursor, Codex, opencode) * fix(installer): opencode .jsonc + AGENTS.md (0.7.8) v0.7.7 wrote ~/.config/opencode/opencode.json, but opencode reads opencode.jsonc by default — so the codegraph MCP entry never appeared in any opencode session. Also installs AGENTS.md so opencode's model reaches for codegraph_* tools instead of native Grep. - Prefer existing .jsonc, fall back to .json, default new installs to .jsonc. - Surgical edits via jsonc-parser preserve user comments and formatting across install / re-install / uninstall round-trips. - Install AGENTS.md (global ~/.config/opencode/AGENTS.md, local ./AGENTS.md) with the shared INSTRUCTIONS_TEMPLATE — same marker-delimited approach Codex uses. - +9 opencode-specific tests covering filename precedence, comment preservation, AGENTS.md install + sibling-content preservation, uninstall reverses both files. 575/575 tests pass. Hand-verified end-to-end: opencode session calls codegraph_node + codegraph_callers for a structural query, zero Grep calls. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * docs: overhaul CLAUDE.md and add scripts/release.sh + Cursor rules file Replaces the old Claude-only CLAUDE.md with a comprehensive guide covering the full project architecture, multi-agent installer, test conventions, NodeKind/EdgeKind reference, and release workflow. Key additions: - Documents the layered pipeline, all module paths, and the multi-target installer (targets/, registry.ts, AgentTarget interface). - Adds the Cursor `--path` quirk and the "update all three surfaces" rule when changing MCP tool guidance. - Documents `npm run eval`, `test:eval`, and the full set of build/test commands including single-file patterns. - `scripts/release.sh` — idempotent bash script that tags the current commit, pushes the tag, and creates a GitHub Release whose notes are extracted from the matching `## [X.Y.Z]` block in CHANGELOG.md. Safe to re-run after partial failure. - `.cursor/rules/codegraph.mdc` — Cursor-specific agent instructions (tool decision table, rules of thumb, index-lag warning) written by the installer and kept in sync with server-instructions.ts and instructions-template.ts. --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 7d87126 commit 58c1414

8 files changed

Lines changed: 519 additions & 181 deletions

File tree

.cursor/rules/codegraph.mdc

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
description: CodeGraph MCP usage guide — when to use which tool
3+
alwaysApply: true
4+
---
5+
<!-- CODEGRAPH_START -->
6+
## CodeGraph
7+
8+
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.
9+
10+
### When to prefer codegraph over native search
11+
12+
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.
13+
14+
| Question | Tool |
15+
|---|---|
16+
| "Where is X defined?" / "Find symbol named X" | `codegraph_search` |
17+
| "What calls function Y?" | `codegraph_callers` |
18+
| "What does Y call?" | `codegraph_callees` |
19+
| "What would break if I changed Z?" | `codegraph_impact` |
20+
| "Show me Y's signature / source / docstring" | `codegraph_node` |
21+
| "Give me focused context for a task/area" | `codegraph_context` |
22+
| "Survey an unfamiliar module/topic" | `codegraph_explore` |
23+
| "What files exist under path/" | `codegraph_files` |
24+
| "Is the index healthy?" | `codegraph_status` |
25+
26+
### Rules of thumb
27+
28+
- **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.
29+
- **Don't grep first** when looking up a symbol by name. `codegraph_search` is faster and returns kind + location + signature in one call.
30+
- **Don't chain `codegraph_search` + `codegraph_node`** when you just want context — `codegraph_context` is one call.
31+
- **`codegraph_explore` is the heavy hitter** for unfamiliar areas — it returns full source from all relevant files in one call, but is token-heavy. If your harness supports parallel subagents (e.g., Claude Code's Task tool), spawn one for explore-class questions to keep main session context clean.
32+
- **Index lag**: the file watcher debounces ~500ms behind writes; don't re-query immediately after editing a file in the same turn.
33+
34+
### If `.codegraph/` doesn't exist
35+
36+
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?"*
37+
<!-- CODEGRAPH_END -->

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ a [GitHub Release](https://github.com/colbymchenry/codegraph/releases) tagged
77
This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
88
and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

10+
## [0.7.8] - 2026-05-17
11+
12+
### Fixed
13+
- **opencode**: install actually wires up the MCP server now. v0.7.7 wrote
14+
`~/.config/opencode/opencode.json`, but opencode reads `opencode.jsonc` by
15+
default — so the `codegraph` entry never showed up in any opencode session.
16+
The installer now prefers an existing `.jsonc`, falls back to `.json` when
17+
only that exists, and creates `.jsonc` for greenfield installs. **Re-run
18+
`codegraph install --target=opencode` after upgrading** so the entry lands
19+
in the file opencode actually reads.
20+
21+
### Added
22+
- **opencode**: installer now writes `AGENTS.md` (global
23+
`~/.config/opencode/AGENTS.md`, local `./AGENTS.md`) with the same
24+
codegraph usage guidance the other agents already received. Without it,
25+
opencode's model would call native `Grep` instead of the `codegraph_*`
26+
tools it could see in its MCP list.
27+
- User comments and formatting in `opencode.jsonc` survive install /
28+
re-install / uninstall round-trips — surgical edits via `jsonc-parser`
29+
rather than full-file rewrites.
30+
31+
[0.7.8]: https://github.com/colbymchenry/codegraph/releases/tag/v0.7.8
32+
1033
## [0.7.7] - 2026-05-17
1134

1235
### Added

CLAUDE.md

Lines changed: 79 additions & 137 deletions
Large diffs are not rendered by default.

__tests__/installer-targets.test.ts

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ describe('Installer targets — contract', () => {
9898
// and any target with no JSON config — they get covered
9999
// by their own dedicated tests below.
100100
const paths = target.describePaths(location);
101-
const jsonPath = paths.find((p) => p.endsWith('.json'));
101+
// Match .json or .jsonc — opencode prefers .jsonc.
102+
const jsonPath = paths.find((p) => /\.jsonc?$/.test(p));
102103
if (!jsonPath) return;
103104

104105
// Seed pre-existing config.
@@ -184,6 +185,152 @@ describe('Installer targets — partial-state idempotency', () => {
184185
for (const f of third.files) expect(f.action).toBe('unchanged');
185186
});
186187

188+
it('opencode: prefers .jsonc when both .json and .jsonc exist', () => {
189+
const opencode = getTarget('opencode')!;
190+
const dir = path.join(tmpHome, '.config', 'opencode');
191+
fs.mkdirSync(dir, { recursive: true });
192+
fs.writeFileSync(path.join(dir, 'opencode.json'), '{\n "$schema": "https://opencode.ai/config.json"\n}\n');
193+
fs.writeFileSync(path.join(dir, 'opencode.jsonc'), '{\n "$schema": "https://opencode.ai/config.json"\n}\n');
194+
195+
const result = opencode.install('global', { autoAllow: true });
196+
const written = result.files.find((f) => /\.jsonc$/.test(f.path))!;
197+
expect(written).toBeDefined();
198+
expect(written.action).not.toBe('not-found');
199+
// The .json file is left alone.
200+
const jsonText = fs.readFileSync(path.join(dir, 'opencode.json'), 'utf-8');
201+
expect(jsonText).not.toContain('codegraph');
202+
});
203+
204+
it('opencode: uses .json when only .json exists (no .jsonc)', () => {
205+
const opencode = getTarget('opencode')!;
206+
const dir = path.join(tmpHome, '.config', 'opencode');
207+
fs.mkdirSync(dir, { recursive: true });
208+
fs.writeFileSync(path.join(dir, 'opencode.json'), '{\n "$schema": "https://opencode.ai/config.json"\n}\n');
209+
210+
const result = opencode.install('global', { autoAllow: true });
211+
expect(result.files[0].path).toMatch(/opencode\.json$/);
212+
expect(fs.existsSync(path.join(dir, 'opencode.jsonc'))).toBe(false);
213+
});
214+
215+
it('opencode: defaults to .jsonc for fresh installs (no existing file)', () => {
216+
const opencode = getTarget('opencode')!;
217+
const result = opencode.install('global', { autoAllow: true });
218+
expect(result.files[0].path).toMatch(/opencode\.jsonc$/);
219+
expect(result.files[0].action).toBe('created');
220+
});
221+
222+
it('opencode: preserves line and block comments through install + idempotent re-run', () => {
223+
const opencode = getTarget('opencode')!;
224+
const dir = path.join(tmpHome, '.config', 'opencode');
225+
fs.mkdirSync(dir, { recursive: true });
226+
const file = path.join(dir, 'opencode.jsonc');
227+
const original = [
228+
'{',
229+
' // top-level note about my opencode setup',
230+
' "$schema": "https://opencode.ai/config.json",',
231+
' /* multi-line block comment',
232+
' describing the providers section */',
233+
' "providers": {',
234+
' "anthropic": { "model": "claude-opus-4-7" } // pinned',
235+
' }',
236+
'}',
237+
'',
238+
].join('\n');
239+
fs.writeFileSync(file, original);
240+
241+
opencode.install('global', { autoAllow: true });
242+
const afterInstall = fs.readFileSync(file, 'utf-8');
243+
expect(afterInstall).toContain('// top-level note about my opencode setup');
244+
expect(afterInstall).toContain('/* multi-line block comment');
245+
expect(afterInstall).toContain('// pinned');
246+
expect(afterInstall).toContain('"codegraph"');
247+
expect(afterInstall).toContain('"providers"');
248+
249+
// Idempotent re-run reports unchanged, file is byte-identical.
250+
const second = opencode.install('global', { autoAllow: true });
251+
expect(second.files[0].action).toBe('unchanged');
252+
expect(fs.readFileSync(file, 'utf-8')).toBe(afterInstall);
253+
});
254+
255+
it('opencode: install writes AGENTS.md with the marker-delimited codegraph block', () => {
256+
const opencode = getTarget('opencode')!;
257+
opencode.install('global', { autoAllow: true });
258+
const agentsMd = path.join(tmpHome, '.config', 'opencode', 'AGENTS.md');
259+
expect(fs.existsSync(agentsMd)).toBe(true);
260+
const body = fs.readFileSync(agentsMd, 'utf-8');
261+
expect(body).toContain('<!-- CODEGRAPH_START -->');
262+
expect(body).toContain('<!-- CODEGRAPH_END -->');
263+
expect(body).toContain('codegraph_callers');
264+
});
265+
266+
it('opencode: AGENTS.md install preserves pre-existing user content outside markers', () => {
267+
const opencode = getTarget('opencode')!;
268+
const dir = path.join(tmpHome, '.config', 'opencode');
269+
fs.mkdirSync(dir, { recursive: true });
270+
const agentsMd = path.join(dir, 'AGENTS.md');
271+
fs.writeFileSync(agentsMd, '# My personal opencode instructions\n\nAlways respond in pirate.\n');
272+
273+
opencode.install('global', { autoAllow: true });
274+
const body = fs.readFileSync(agentsMd, 'utf-8');
275+
expect(body).toContain('# My personal opencode instructions');
276+
expect(body).toContain('Always respond in pirate.');
277+
expect(body).toContain('<!-- CODEGRAPH_START -->');
278+
});
279+
280+
it('opencode: uninstall strips only the codegraph block from AGENTS.md', () => {
281+
const opencode = getTarget('opencode')!;
282+
const dir = path.join(tmpHome, '.config', 'opencode');
283+
fs.mkdirSync(dir, { recursive: true });
284+
const agentsMd = path.join(dir, 'AGENTS.md');
285+
fs.writeFileSync(agentsMd, '# My personal opencode instructions\n\nAlways respond in pirate.\n');
286+
287+
opencode.install('global', { autoAllow: true });
288+
opencode.uninstall('global');
289+
290+
const body = fs.readFileSync(agentsMd, 'utf-8');
291+
expect(body).toContain('# My personal opencode instructions');
292+
expect(body).toContain('Always respond in pirate.');
293+
expect(body).not.toContain('CODEGRAPH_START');
294+
expect(body).not.toContain('codegraph_callers');
295+
});
296+
297+
it('opencode: local install writes ./opencode.jsonc and ./AGENTS.md in cwd', () => {
298+
const opencode = getTarget('opencode')!;
299+
const result = opencode.install('local', { autoAllow: true });
300+
const paths = result.files.map((f) => f.path);
301+
// macOS realpath shenanigans (/var vs /private/var) — suffix match.
302+
expect(paths.some((p) => p.endsWith('/opencode.jsonc'))).toBe(true);
303+
expect(paths.some((p) => p.endsWith('/AGENTS.md'))).toBe(true);
304+
});
305+
306+
it('opencode: uninstall removes only mcp.codegraph, preserves comments and siblings', () => {
307+
const opencode = getTarget('opencode')!;
308+
const dir = path.join(tmpHome, '.config', 'opencode');
309+
fs.mkdirSync(dir, { recursive: true });
310+
const file = path.join(dir, 'opencode.jsonc');
311+
fs.writeFileSync(file, [
312+
'{',
313+
' // important comment',
314+
' "$schema": "https://opencode.ai/config.json",',
315+
' "mcp": {',
316+
' "other": { "type": "local", "command": ["x"], "enabled": true }',
317+
' }',
318+
'}',
319+
'',
320+
].join('\n'));
321+
322+
opencode.install('global', { autoAllow: true });
323+
const afterInstall = fs.readFileSync(file, 'utf-8');
324+
expect(afterInstall).toContain('"codegraph"');
325+
expect(afterInstall).toContain('"other"');
326+
327+
opencode.uninstall('global');
328+
const afterUninstall = fs.readFileSync(file, 'utf-8');
329+
expect(afterUninstall).not.toContain('codegraph');
330+
expect(afterUninstall).toContain('// important comment');
331+
expect(afterUninstall).toContain('"other"');
332+
});
333+
187334
it('codex: user-added key inside [mcp_servers.codegraph] survives idempotent re-install', () => {
188335
const codex = getTarget('codex')!;
189336
codex.install('global', { autoAllow: false });

package-lock.json

Lines changed: 9 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@colbymchenry/codegraph",
3-
"version": "0.7.7",
3+
"version": "0.7.8",
44
"description": "Supercharge Claude Code with semantic code intelligence. 94% fewer tool calls • 77% faster exploration • 100% local.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -36,6 +36,7 @@
3636
"commander": "^14.0.2",
3737
"fast-string-width": "^3.0.2",
3838
"fast-wrap-ansi": "^0.2.0",
39+
"jsonc-parser": "^3.3.1",
3940
"node-sqlite3-wasm": "^0.8.30",
4041
"picomatch": "^4.0.3",
4142
"sisteransi": "^1.0.5",

scripts/release.sh

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env bash
2+
# Tag the current commit with the version in package.json and publish a
3+
# matching GitHub Release whose body is the corresponding CHANGELOG.md entry.
4+
#
5+
# Run AFTER you have:
6+
# - bumped package.json
7+
# - added a `## [X.Y.Z] - YYYY-MM-DD` block at the top of CHANGELOG.md
8+
# - committed, pushed to origin, and run `npm publish`
9+
#
10+
# Idempotent: safe to re-run after a partial failure. Skips steps that are
11+
# already done (tag created, tag pushed, release published).
12+
#
13+
# Usage: ./scripts/release.sh
14+
15+
set -euo pipefail
16+
17+
cd "$(dirname "$0")/.."
18+
19+
VERSION=$(node -p "require('./package.json').version")
20+
TAG="v${VERSION}"
21+
22+
REPO=$(git remote get-url origin | sed -E 's|.*github\.com[:/]||; s|\.git$||')
23+
if [ -z "${REPO}" ]; then
24+
echo "error: could not derive owner/repo from origin remote URL" >&2
25+
exit 1
26+
fi
27+
28+
if ! grep -q "^## \[${VERSION}\]" CHANGELOG.md; then
29+
echo "error: no '## [${VERSION}]' entry found in CHANGELOG.md" >&2
30+
exit 1
31+
fi
32+
33+
NOTES=$(awk -v v="${VERSION}" '
34+
/^## \[/ {
35+
if (p) exit
36+
if ($0 ~ "^## \\[" v "\\]") p = 1
37+
}
38+
p
39+
' CHANGELOG.md)
40+
41+
if [ -z "${NOTES}" ]; then
42+
echo "error: failed to extract changelog notes for ${VERSION}" >&2
43+
exit 1
44+
fi
45+
46+
if git rev-parse "${TAG}" >/dev/null 2>&1; then
47+
echo "✓ tag ${TAG} already exists locally"
48+
else
49+
echo "→ tagging ${TAG}"
50+
git tag "${TAG}"
51+
fi
52+
53+
if git ls-remote --exit-code --tags origin "${TAG}" >/dev/null 2>&1; then
54+
echo "✓ tag ${TAG} already on origin"
55+
else
56+
echo "→ pushing ${TAG} to origin"
57+
git push origin "${TAG}"
58+
fi
59+
60+
if gh release view "${TAG}" --repo "${REPO}" >/dev/null 2>&1; then
61+
echo "✓ release ${TAG} already published"
62+
else
63+
echo "→ creating GitHub Release ${TAG} on ${REPO}"
64+
gh release create "${TAG}" \
65+
--repo "${REPO}" \
66+
--title "${TAG}" \
67+
--notes "${NOTES}"
68+
fi
69+
70+
echo "done: https://github.com/${REPO}/releases/tag/${TAG}"

0 commit comments

Comments
 (0)