Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
238dbc5
fix(explore): accurately resolve query file paths and find camelCase …
colbymchenry Aug 20, 2026
d806317
fix(explore): improve query accuracy for file paths, camelCase, and v…
colbymchenry Aug 20, 2026
26045b3
fix(extraction): decode kernel results in indexAll retry passes; self…
colbymchenry Aug 20, 2026
d8f2eea
fix(db): loop-append dense unresolved-ref result rows; make stripped-…
colbymchenry Aug 20, 2026
81e1f4a
fix: harden daemon and large-index recovery paths (#1562)
danusha2345 Aug 20, 2026
ccb0295
fix(explore): reliably pin extension-less kebab-case file basenames i…
colbymchenry Aug 22, 2026
340d4b0
fix(swift): remove catastrophic backtracking in Vapor route regex (#1…
maxmilian Aug 22, 2026
cc9ce09
fix(extraction): detect untracked files inside untracked directories …
maxmilian Aug 22, 2026
a740291
fix(resolution): resolve ES imports targeting .xsjs/.xsjslib files (#…
maxmilian Aug 22, 2026
9219967
perf(search): seek the name index for exact-name lookups (#1542)
maxmilian Aug 22, 2026
474f051
fix(resolution): load path aliases through tsconfig `extends` and bas…
maxmilian Aug 22, 2026
bc89480
feat(installer): support project-local Codex installs (#1531) (#1551)
maxmilian Aug 22, 2026
1d9de88
feat(config): add codegraph.json "deprioritize" for ranking-only path…
maxmilian Aug 22, 2026
44e1812
changelog: cover the merged contributor batch (#1547, #1215, #594, #1…
colbymchenry Aug 22, 2026
12f7a59
fix(rust): qualify generic/lifetime impl methods by the implementing …
colbymchenry Aug 26, 2026
7963672
fix(rust): resolve `self.field.method()` on the field's declared type…
colbymchenry Aug 26, 2026
278a8ed
fix(resolution): resolve calls to object-literal namespace members (#…
colbymchenry Aug 26, 2026
0d17dfd
feat(cli): `install --init` and `init --yes` for a one-shot, non-inte…
colbymchenry Aug 26, 2026
838006c
fix(kernel): guard the native walkers against stack overflow and defe…
colbymchenry Aug 26, 2026
cf1b0e3
fix(sync): refresh the watcher's scope when codegraph.json or a .giti…
colbymchenry Aug 26, 2026
d618d94
fix(extraction): detect a plain `struct Derived : Base` base clause i…
colbymchenry Aug 26, 2026
a5c2709
fix(mcp): adopt a single indexed sub-project below the server root + …
colbymchenry Aug 26, 2026
c382225
fix(cli): register the documented `context` command (#1611) (#1613)
colbymchenry Aug 26, 2026
41c1075
fix(erlang): give same-name different-arity functions separate arity-…
colbymchenry Aug 26, 2026
60f920a
docs(changelog): open [Unreleased] with a Highlights block and group …
colbymchenry Aug 26, 2026
b59023f
chore(release): bump version to 1.6.0
colbymchenry Aug 26, 2026
dfccdf6
docs(changelog): promote [Unreleased] into [1.6.0]
github-actions[bot] Aug 26, 2026
623a9ec
Merge upstream v1.6.0 into fork main
Dshuishui Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 124 additions & 11 deletions CHANGELOG.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ Formatting rules for any entry (anywhere — `[Unreleased]` or otherwise):
3. **Strip the internals.** No internal file paths (`src/...`), no internal symbol / function / class names, no benchmark numbers / percentages / node-or-edge counts. **Keep:** language & framework names (Go, Spring, NestJS, …), things a user types or sets (`codegraph install`, `codegraph_explore`, the `CODEGRAPH_*` env vars), agent / IDE names (Claude Code, Cursor, opencode, Kiro, …), and a brief `Thanks @user` when a contributor is credited.
4. Issue / PR references in entries are by number (`(#403)` etc.); the GitHub renderer auto-links them in the published release notes.
5. **Don't add a `[X.Y.Z]: https://...` link reference yourself** — `prepare-release.mjs` appends it automatically when it promotes the version (idempotent: a re-run is a no-op if it already exists).
6. **Every release opens with a `### Highlights` block — the only part most people read.** At most ~8 one-line bullets, in plain language for someone who doesn't read code, ordered by what a typical user notices first (new agent/IDE support and setup changes, then answer quality, then reliability), plus a one-sentence upgrade note when a re-index is needed. Write or refresh it in `[Unreleased]` when a release is being prepared — not per PR — and keep the detailed `### New Features` / `### Fixes` entries below it. When `### Fixes` grows past ~15 entries, group them under `####` sub-headings (`Better answers from codegraph_explore`, `Finding your project, live updates, and the CLI`, `Indexing reliability and disk usage`, `Language and framework accuracy`) so a skimmer can find their area.

Multi-word headings like `### New Features` are safe on the normal release path: `prepare-release.mjs` **Case A** moves the whole `[Unreleased]` body verbatim into `[X.Y.Z]`. (Only its rarely-used **Case B** *merge* splits sub-sections with a single-word `^### (\w+)$` regex that wouldn't match them — and Case B fires only if a `[X.Y.Z]` block was pre-created, which rule above already forbids.)

Expand Down
22 changes: 12 additions & 10 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
FM-Agent's maintenance fork of
[colbymchenry/codegraph](https://github.com/colbymchenry/codegraph).

**Pinned base:** upstream `main` at `c6aaa20358cd6adcd04b87bdef8e5803ad146f3a`
(2026-08-07). That is *past* `v1.5.0` and before whatever upstream tags next:
`v1.5.0` was 104 commits behind, and the incremental-sync convergence work, the
first-class `union` nodes and the WAL growth fix in between all matter to how
FM-Agent reads the graph. The version marker keeps saying `1.5.0-fmagent.N`
because it is the last upstream *release* this descends from — the exact base is
this commit.
**Pinned base:** upstream `v1.6.0` (2026-08-26) — a tagged release, as the policy
below prefers. It carries fixes for the three issues reported upstream from this
project since the previous base: Rust field-receiver resolution
([#1585](https://github.com/colbymchenry/codegraph/issues/1585)), generic `impl`
ownership ([#1588](https://github.com/colbymchenry/codegraph/issues/1588)) and
Erlang per-arity identity
([#1610](https://github.com/colbymchenry/codegraph/issues/1610)). The previous
base was `c6aaa20` (upstream `main`, 2026-08-07), 27 commits behind this tag; see
issue #10 for the full rationale of this sync.

Upstream shipped the C macro-attribute extraction fix (issue #1211, PR #1311) in
v1.5.0, so the base carries it natively; the fork no longer needs its own patch
Expand All @@ -31,9 +33,9 @@ tree or changing how the runner discovers it: our file would stay where it is, q
stop being collected, and nothing would fail. Check the suite's file count after a
sync, not just that it is green.

**Version marker:** `codegraph --version` → `1.5.0-fmagent.N` identifies a build
from this fork. Note this is a SemVer pre-release of `1.5.0`, so it sorts *below*
plain `1.5.0`; the updater must therefore point at this fork (see below), never
**Version marker:** `codegraph --version` → `1.6.0-fmagent.N` identifies a build
from this fork. Note this is a SemVer pre-release of `1.6.0`, so it sorts *below*
plain `1.6.0`; the updater must therefore point at this fork (see below), never
upstream, or it would advertise a "downgrade to upstream" as an upgrade.

**All install/upgrade entry points point at this fork,** so a fork install never
Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ The installer **wires up your agents only — it does not index your code.** Aft

```bash
codegraph install --yes # auto-detect agents, install global
codegraph install --yes --init # same, then build the current project's index (one-shot bootstrap)
codegraph install --target=cursor,claude --yes # explicit target list
codegraph install --target=auto --location=local # detected agents, project-local
codegraph install --target=copilot-vscode,copilot-cli,copilot-jetbrains --yes # GitHub Copilot everywhere
Expand All @@ -400,6 +401,7 @@ codegraph install --print-config copilot-vscode # same, for Copilot in VS C
| `--target` | `auto`, `all`, `none`, or csv (`claude,cursor,...`) | prompt |
| `--location` | `global`, `local` | prompt |
| `--yes` | (boolean) | prompt every step |
| `--init` | (boolean) run `codegraph init` in the current directory after wiring agents | — |
| `--no-permissions` | (boolean) skip Claude auto-allow list | permissions on |
| `--print-config <id>` | dump snippet for one agent and exit | — |

Expand All @@ -414,7 +416,7 @@ cd your-project
codegraph init
```

Builds the per-project knowledge graph index, which then auto-syncs on every file change. A single global `codegraph install` works in every project you open — no need to re-run the installer per project.
Builds the per-project knowledge graph index, which then auto-syncs on every file change. A single global `codegraph install` works in every project you open — no need to re-run the installer per project. Add `--yes` to skip every prompt (scripts / CI / container bootstraps).

That's it — your agent will use CodeGraph tools automatically when a `.codegraph/` directory exists.

Expand Down Expand Up @@ -669,6 +671,26 @@ CodeGraph discovers those files off disk, overriding `.gitignore`, on index,
sync, and watch. An explicit `exclude` still wins, and built-in skips
(`node_modules`, `dist`, `.git`) are never re-included.

Sometimes a directory shouldn't leave the index — you still want to find things
in it — it just shouldn't *outrank* your real code. A `scripts/` or
`optional-skills/` tree whose helpers use generic names (`usage`, `status`,
`run`) can win on an exact name match and crowd out the product code that
actually answers the query. Name those trees under `deprioritize`:

```json
{
"deprioritize": ["optional-skills/", "scripts/"]
}
```

This is the ranking counterpart to `exclude`: those paths stay indexed and
findable — searching for them directly still works — they just stop winning
against first-party code. It applies to `query` / `search` and to `explore`'s
ranking. It is *not* a filter: unlike the built-in `example/`, `sample/`,
`fixture/`, `benchmark/` and `demo/` handling — which also drops those files
from some result sets outright — `deprioritize` only ever changes rank. Reach
for `exclude` when you want something gone.

### Custom file extensions

If your project uses a non-standard extension for a [supported
Expand Down
114 changes: 114 additions & 0 deletions __tests__/cli-context-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* `codegraph context` CLI command (#1611).
*
* The usage header has advertised `codegraph context <task> Build context for
* a task` since the first release, and the ContextBuilder behind the public
* `buildContext` API has always shipped in the package — but the command was
* never registered with commander, so external integrations built against the
* documented contract (`codegraph context --path <root> --format json
* --max-nodes 8 --no-code <task>`, e.g. Memorix) got `unknown command
* 'context'` and fell back to their own heuristics.
*
* Exercised end-to-end against the built binary, mirroring
* cli-query-command.test.ts.
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');

const ENV = { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' };

function runContext(cwd: string, extraArgs: string[], taskParts: string[] = ['parseToken', 'expiry', 'handling']): string {
return execFileSync(process.execPath, [BIN, 'context', ...extraArgs, '-p', cwd, ...taskParts], {
encoding: 'utf-8',
env: ENV,
stdio: ['ignore', 'pipe', 'ignore'], // drop stderr (SQLite experimental warning)
});
}

describe('codegraph context — registered CLI command (#1611)', () => {
let tempDir: string;

beforeEach(async () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-context-cmd-'));
fs.mkdirSync(path.join(tempDir, 'src'));
fs.writeFileSync(
path.join(tempDir, 'src/auth.ts'),
'export function parseToken(t: string){ return parseTokenExpiry(t) + t.trim().length; }\n' +
'export function parseTokenExpiry(t: string){ return Date.parse(t); }\n',
);
const cg = CodeGraph.initSync(tempDir);
await cg.indexAll();
cg.close();
});

afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});

it('--format json emits clean machine-parseable JSON on stdout', () => {
const parsed = JSON.parse(runContext(tempDir, ['--format', 'json']));
expect(parsed.query).toBe('parseToken expiry handling');
expect(Array.isArray(parsed.nodes)).toBe(true);
expect(parsed.nodes.length).toBeGreaterThan(0);
expect(Array.isArray(parsed.codeBlocks)).toBe(true);
expect(parsed.codeBlocks.length).toBeGreaterThan(0);
});

it('--max-nodes bounds the returned symbol set', () => {
const parsed = JSON.parse(runContext(tempDir, ['--format', 'json', '--max-nodes', '1']));
expect(parsed.nodes.length).toBeLessThanOrEqual(1);
});

it('--no-code omits code blocks (the Memorix contract shape)', () => {
// The exact documented invocation: --format json --max-nodes 8 --no-code
const parsed = JSON.parse(
runContext(tempDir, ['--format', 'json', '--max-nodes', '8', '--no-code']),
);
expect(parsed.codeBlocks).toEqual([]);
expect(parsed.nodes.length).toBeGreaterThan(0);
});

it('defaults to markdown output', () => {
const out = runContext(tempDir, []);
expect(out).toContain('## Code Context');
expect(out).toContain('**Query:** parseToken expiry handling');
});

it('fails cleanly on an uninitialized project', () => {
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-context-empty-'));
try {
execFileSync(process.execPath, [BIN, 'context', '-p', empty, 'some', 'task'], {
encoding: 'utf-8',
env: ENV,
stdio: ['ignore', 'pipe', 'pipe'],
});
throw new Error('expected non-zero exit');
} catch (err: any) {
expect(err.status).toBe(1);
expect(String(err.stderr)).toContain('not initialized');
} finally {
fs.rmSync(empty, { recursive: true, force: true });
}
});

it('rejects an unknown --format value', () => {
try {
execFileSync(process.execPath, [BIN, 'context', '--format', 'yaml', '-p', tempDir, 'task'], {
encoding: 'utf-8',
env: ENV,
stdio: ['ignore', 'pipe', 'pipe'],
});
throw new Error('expected non-zero exit');
} catch (err: any) {
expect(err.status).toBe(1);
expect(String(err.stderr)).toContain('Unknown format');
}
});
});
108 changes: 108 additions & 0 deletions __tests__/cli-install-init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* `codegraph install --init` and `codegraph init --yes` (#1578): the one-shot,
* non-interactive "wire agents + build this project's index" bootstrap a fresh
* container / CI job needs.
*
* Exercised end-to-end against the built binary so the CLI wiring (the shared
* `runInit` flow, the flag plumbing, exit codes) is what's covered. Every run
* uses `--target none`, so the installer touches no agent config on the
* machine running the suite; the only side effect is the temp project's
* `.codegraph/`.
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');

interface RunResult {
status: number;
stdout: string;
stderr: string;
}

/** Run the CLI with stdin closed — a prompt that blocks would hang / fail here. */
function runCodegraph(args: string[], cwd: string): RunResult {
try {
const stdout = execFileSync(process.execPath, [BIN, ...args], {
cwd,
encoding: 'utf-8',
env: {
...process.env,
CODEGRAPH_NO_DAEMON: '1',
CODEGRAPH_TELEMETRY: '0',
DO_NOT_TRACK: '1',
NO_COLOR: '1',
},
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 120_000,
});
return { status: 0, stdout, stderr: '' };
} catch (err) {
const e = err as { status?: number | null; stdout?: string | Buffer; stderr?: string | Buffer };
return {
status: e.status ?? -1,
stdout: String(e.stdout ?? ''),
stderr: String(e.stderr ?? ''),
};
}
}

describe('codegraph install --init / init --yes (#1578)', () => {
let tempDir: string;

beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-install-init-'));
fs.writeFileSync(
path.join(tempDir, 'a.ts'),
`export function greet(name: string) { return hello(name); }\n` +
`export function hello(n: string) { return 'hi ' + n; }\n`,
);
});

afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});

it('install --yes --target none --init builds the current project\'s index in one command', () => {
const r = runCodegraph(['install', '--yes', '--target', 'none', '--init'], tempDir);
expect(r.status, r.stdout + r.stderr).toBe(0);
// The installer ran (and had nothing to wire) …
expect(r.stdout).toContain('No agent targets selected');
// … and the init ran afterwards, in cwd.
expect(r.stdout).toContain(`Initialized in ${fs.realpathSync(tempDir)}`);
expect(fs.existsSync(path.join(tempDir, '.codegraph', 'codegraph.db'))).toBe(true);
});

it('install --init on an already-initialized project reports that and still exits 0', () => {
expect(runCodegraph(['init', '--yes'], tempDir).status).toBe(0);
const r = runCodegraph(['install', '--yes', '--target', 'none', '--init'], tempDir);
expect(r.status, r.stdout + r.stderr).toBe(0);
expect(r.stdout).toContain('Already initialized');
});

it('install --init refuses an unsafe root (filesystem root) with exit code 1, like init does', () => {
// `/` (or the drive root on Windows) is the canonical unsafe root: the
// refusal fires before anything is created, so nothing is written there.
const root = path.parse(process.cwd()).root;
const r = runCodegraph(['install', '--yes', '--target', 'none', '--init'], root);
expect(r.status).toBe(1);
expect(r.stdout).toContain('Refusing to initialize');
expect(fs.existsSync(path.join(root, '.codegraph'))).toBe(false);
});

it('init --yes runs non-interactively with stdin closed and builds the index', () => {
const r = runCodegraph(['init', '--yes'], tempDir);
expect(r.status, r.stdout + r.stderr).toBe(0);
expect(r.stdout).toContain('Initialized in');
expect(fs.existsSync(path.join(tempDir, '.codegraph', 'codegraph.db'))).toBe(true);
});

it('documents the new flags in --help', () => {
expect(runCodegraph(['init', '--help'], tempDir).stdout).toMatch(/-y, --yes\b/);
expect(runCodegraph(['install', '--help'], tempDir).stdout).toMatch(/-i, --init\b/);
});
});
Loading