diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ab6b0c4b..acdbcd099 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,15 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- The background file watcher no longer exhausts your machine's file-descriptor budget. On macOS it previously kept **one open file handle per watched file**, so on a large project the running MCP server could pile up tens of thousands of handles and blow past the system-wide limit — at which point *unrelated* apps (your shell, editor, Docker, browser) started failing with "too many open files" until the codegraph process was killed. The watcher now uses a single recursive watch on macOS and Windows, and bounded per-directory watches on Linux, so its cost stays flat no matter how large the project is. (#644, #496, #555, #628, #579)
- Indexing a project with very symbol-dense files (tens of thousands of functions or methods in a single file) no longer runs out of memory. The step that links dynamic call relationships used to load every function and method into memory at once, which could exhaust the heap and abort indexing with "JavaScript heap out of memory" on large or generated codebases; it now streams them, so memory stays flat no matter how many symbols the project has. (#610)
+- Indexing a very large repository no longer aborts during its first sync with a "too many SQL variables" error. (#540)
+- Files under directories with non-ASCII names (for example CJK characters) are no longer silently skipped during indexing. (#541)
+- The `.codegraph/` index folder no longer clutters `git status`: its generated ignore file now excludes everything in the folder except itself, so the database, `daemon.pid`, sockets, and logs stop showing up as untracked changes. (#492, #484)
+- SAP HANA `.xsjs` / `.xsjslib` files are now indexed as JavaScript. (#556)
+- TypeScript `.mts` and `.cts` module files are now indexed instead of being skipped. (#366)
+- JavaScript modules that wrap their code in an anonymous function — AMD/RequireJS, NetSuite SuiteScript, IIFE bundles — now have their inner functions and calls indexed, instead of the file coming up nearly empty. (#528)
+- Go methods declared on generic types (e.g. `func (s *Stack[T]) Push(...)`) are now correctly attached to their type, so callers, callees, and impact include them. (#583)
+- Asking what a symbol impacts no longer drags in every unrelated sibling method of its class — impact now follows real dependencies instead of the structural "contains" relationship, keeping the result focused on what actually depends on the symbol. (#536)
+- CodeGraph's MCP server now answers an agent's `resources/list` and `prompts/list` probes with an empty list instead of an error, clearing the `-32601` messages some clients (opencode, Codex) logged on connect. (#621)
## [0.9.9] - 2026-06-02
diff --git a/README.md b/README.md
index 9bf05dd1d..1a9800ee3 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,8 @@
## Get Started
+### 1. Install the CLI
+
**No Node.js required** — one command grabs the right build for your OS:
```bash
@@ -42,13 +44,22 @@ irm https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.ps1 |
Already have Node? Use npm instead (works on any version):
```bash
-npx @colbymchenry/codegraph # zero-install, or:
npm i -g @colbymchenry/codegraph
```
-CodeGraph bundles its own runtime — nothing to compile, no native build, works the same everywhere. The interactive installer auto-configures your agent(s) — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro.
+CodeGraph bundles its own runtime — nothing to compile, no native build, works the same everywhere. The installer puts `codegraph` on your PATH but **doesn't change your current shell** — open a new terminal before the next step so the command resolves.
+
+### 2. Wire up your agent(s)
+
+In a **new terminal**, run the installer to connect CodeGraph to the agents you use:
+
+```bash
+codegraph install
+```
+
+Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)
-### Initialize Projects
+### 3. Initialize each project
```bash
cd your-project
diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts
index b497af6a9..69fadbe1d 100644
--- a/__tests__/extraction.test.ts
+++ b/__tests__/extraction.test.ts
@@ -10,7 +10,7 @@ import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { extractFromSource, scanDirectory } from '../src/extraction';
-import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars } from '../src/extraction/grammars';
+import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars, isSourceFile } from '../src/extraction/grammars';
import { normalizePath } from '../src/utils';
beforeAll(async () => {
@@ -4387,3 +4387,49 @@ void helperFunction(int count) {
expect(getSupportedLanguages()).toContain('objc');
});
});
+
+describe('Regression: issue-specific extraction fixes', () => {
+ it('indexes inner functions of an anonymous AMD/CommonJS module wrapper (#528)', () => {
+ const code = `
+define(['dep'], function (dep) {
+ function innerHelper(x) { return x + 1; }
+ function compute(y) { return innerHelper(y); }
+ return { compute: compute };
+});
+`;
+ const result = extractFromSource('amd-module.js', code);
+ const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
+ expect(fns).toContain('innerHelper');
+ expect(fns).toContain('compute');
+ });
+
+ it('attaches Go methods on generic receivers to their type (#583)', () => {
+ const code = `
+package main
+
+type Stack[T any] struct { items []T }
+
+func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
+func (s Stack[T]) Len() int { return len(s.items) }
+`;
+ const result = extractFromSource('stack.go', code);
+ const methods = result.nodes.filter((n) => n.kind === 'method');
+ expect(methods.find((m) => m.name === 'Push')?.qualifiedName).toBe('Stack::Push');
+ expect(methods.find((m) => m.name === 'Len')?.qualifiedName).toBe('Stack::Len');
+ });
+
+ it('indexes new module extensions: .mts/.cts (TS) and .xsjs/.xsjslib (JS) (#366, #556)', () => {
+ expect(isSourceFile('mod.mts')).toBe(true);
+ expect(isSourceFile('mod.cts')).toBe(true);
+ expect(isSourceFile('service.xsjs')).toBe(true);
+ expect(isSourceFile('lib.xsjslib')).toBe(true);
+ expect(detectLanguage('mod.mts')).toBe('typescript');
+ expect(detectLanguage('service.xsjs')).toBe('javascript');
+
+ // End-to-end: a .mts file is parsed as TS, a .xsjs file as JS.
+ const ts = extractFromSource('mod.mts', 'export function hello(): number { return 1; }');
+ expect(ts.nodes.find((n) => n.name === 'hello' && n.kind === 'function')).toBeDefined();
+ const js = extractFromSource('service.xsjs', 'function handleRequest() { return 1; }');
+ expect(js.nodes.find((n) => n.name === 'handleRequest' && n.kind === 'function')).toBeDefined();
+ });
+});
diff --git a/__tests__/foundation.test.ts b/__tests__/foundation.test.ts
index 78ebfce4f..9933cf8c5 100644
--- a/__tests__/foundation.test.ts
+++ b/__tests__/foundation.test.ts
@@ -54,7 +54,10 @@ describe('CodeGraph Foundation', () => {
expect(fs.existsSync(gitignorePath)).toBe(true);
const content = fs.readFileSync(gitignorePath, 'utf-8');
- expect(content).toContain('*.db');
+ // Ignore everything in .codegraph/ except this file itself, so transient
+ // files (db, daemon.pid, sockets, logs) never show up in git. (#492, #484)
+ expect(content).toContain('*');
+ expect(content).toContain('!.gitignore');
cg.close();
});
diff --git a/__tests__/graph.test.ts b/__tests__/graph.test.ts
index 7c771af0b..e8aab821a 100644
--- a/__tests__/graph.test.ts
+++ b/__tests__/graph.test.ts
@@ -309,6 +309,19 @@ export { main };
expect(impact.nodes.size).toBeGreaterThan(0);
expect(impact.nodes.has(formatValue.id)).toBe(true);
});
+
+ it('does not drag in sibling members via the structural contains edge (#536)', () => {
+ const getName = cg.getNodesByKind('method').find((n) => n.name === 'getName');
+ const derived = cg.getNodesByKind('class').find((n) => n.name === 'DerivedClass');
+ expect(getName).toBeDefined();
+ expect(derived).toBeDefined();
+
+ const impact = cg.getImpactRadius(getName!.id, 3);
+ // The containing class must NOT be pulled into impact just because it
+ // *contains* getName — climbing that contains edge would re-expand every
+ // sibling method and explode impact for a leaf symbol. (#536)
+ expect(impact.nodes.has(derived!.id)).toBe(false);
+ });
});
describe('findPath()', () => {
diff --git a/__tests__/mcp-initialize.test.ts b/__tests__/mcp-initialize.test.ts
index 31899aa7c..0a320773d 100644
--- a/__tests__/mcp-initialize.test.ts
+++ b/__tests__/mcp-initialize.test.ts
@@ -154,4 +154,30 @@ describe('MCP initialize handshake (issue #172)', () => {
expect(json.id).toBe(0);
expect(json.result.serverInfo.name).toBe('codegraph');
}, 20000);
+
+ it('answers resources/list and prompts/list with empty lists, not -32601 (issue #621)', async () => {
+ child = spawnServer(tempDir);
+ const events = tagStreams(child);
+ sendInitialize(child, tempDir);
+ await waitFor(events, (e) => e.stream === 'stdout', 5000); // initialize reply
+
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'resources/list', params: {} }) + '\n');
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'prompts/list', params: {} }) + '\n');
+
+ const replyFor = async (id: number) => {
+ const ev = await waitFor(events, (e) => {
+ if (e.stream !== 'stdout') return false;
+ try { return JSON.parse(e.text).id === id; } catch { return false; }
+ }, 5000);
+ return JSON.parse(ev.text);
+ };
+
+ const resources = await replyFor(1);
+ expect(resources.error).toBeUndefined();
+ expect(resources.result.resources).toEqual([]);
+
+ const prompts = await replyFor(2);
+ expect(prompts.error).toBeUndefined();
+ expect(prompts.result.prompts).toEqual([]);
+ }, 15000);
});
diff --git a/src/db/queries.ts b/src/db/queries.ts
index d6470d0d7..d7c9173d7 100644
--- a/src/db/queries.ts
+++ b/src/db/queries.ts
@@ -1588,10 +1588,19 @@ export class QueryBuilder {
getUnresolvedReferencesByFiles(filePaths: string[]): UnresolvedReference[] {
if (filePaths.length === 0) return [];
- const placeholders = filePaths.map(() => '?').join(',');
- const rows = this.db
- .prepare(`SELECT * FROM unresolved_refs WHERE file_path IN (${placeholders})`)
- .all(...filePaths) as UnresolvedRefRow[];
+ // Chunk under SQLite's parameter limit: the first sync of a very large repo
+ // passes every changed file here, which an unbounded `IN (...)` would bind
+ // as one parameter each — exceeding MAX_VARIABLE_NUMBER and aborting with
+ // "too many SQL variables". (#540)
+ const rows: UnresolvedRefRow[] = [];
+ for (let i = 0; i < filePaths.length; i += SQLITE_PARAM_CHUNK_SIZE) {
+ const chunk = filePaths.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
+ const placeholders = chunk.map(() => '?').join(',');
+ const chunkRows = this.db
+ .prepare(`SELECT * FROM unresolved_refs WHERE file_path IN (${placeholders})`)
+ .all(...chunk) as UnresolvedRefRow[];
+ rows.push(...chunkRows);
+ }
return rows.map((row) => ({
fromNodeId: row.from_node_id,
diff --git a/src/directory.ts b/src/directory.ts
index 588911c14..3a5c91d93 100644
--- a/src/directory.ts
+++ b/src/directory.ts
@@ -83,22 +83,11 @@ export function createDirectory(projectRoot: string): void {
// Create .gitignore inside .codegraph (if it doesn't exist)
const gitignorePath = path.join(codegraphDir, '.gitignore');
if (!fs.existsSync(gitignorePath)) {
- const gitignoreContent = `# CodeGraph data files
-# These are local to each machine and should not be committed
-
-# Database
-*.db
-*.db-wal
-*.db-shm
-
-# Cache
-cache/
-
-# Logs
-*.log
-
-# Hook markers
-.dirty
+ const gitignoreContent = `# CodeGraph data files — local to each machine, not for committing.
+# Ignore everything in .codegraph/ except this file itself, so transient
+# files (the database, daemon.pid, sockets, logs) never show up in git.
+*
+!.gitignore
`;
fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
@@ -245,7 +234,7 @@ export function validateDirectory(projectRoot: string): {
const gitignorePath = path.join(codegraphDir, '.gitignore');
if (!fs.existsSync(gitignorePath)) {
try {
- const gitignoreContent = `# CodeGraph data files\n# These are local to each machine and should not be committed\n\n# Database\n*.db\n*.db-wal\n*.db-shm\n\n# Cache\ncache/\n\n# Logs\n*.log\n\n# Hook markers\n.dirty\n`;
+ const gitignoreContent = `# CodeGraph data files — local to each machine, not for committing.\n# Ignore everything in .codegraph/ except this file itself, so transient\n# files (the database, daemon.pid, sockets, logs) never show up in git.\n*\n!.gitignore\n`;
fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
} catch {
// Non-fatal: warn but don't block
diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts
index c9a2bcb37..576845e20 100644
--- a/src/extraction/grammars.ts
+++ b/src/extraction/grammars.ts
@@ -46,9 +46,15 @@ const WASM_GRAMMAR_FILES: Record = {
export const EXTENSION_MAP: Record = {
'.ts': 'typescript',
'.tsx': 'tsx',
+ // ESM/CJS TypeScript module extensions — parsed as TS (no JSX). (#366)
+ '.mts': 'typescript',
+ '.cts': 'typescript',
'.js': 'javascript',
'.mjs': 'javascript',
'.cjs': 'javascript',
+ // SAP HANA XS Classic server-side JavaScript. (#556)
+ '.xsjs': 'javascript',
+ '.xsjslib': 'javascript',
'.jsx': 'jsx',
'.py': 'python',
'.pyw': 'python',
diff --git a/src/extraction/index.ts b/src/extraction/index.ts
index 42037d7f6..36569258e 100644
--- a/src/extraction/index.ts
+++ b/src/extraction/index.ts
@@ -198,32 +198,32 @@ function collectGitFiles(repoDir: string, prefix: string, files: Set): v
// Without this, monorepos using submodules index 0 files. (See issue #147.)
// Note: --recurse-submodules only supports -c/--cached and --stage modes — it
// can't be combined with -o, so untracked files are gathered separately below.
- const tracked = execFileSync('git', ['ls-files', '-c', '--recurse-submodules'], gitOpts);
- for (const line of tracked.split('\n')) {
- const trimmed = line.trim();
- if (trimmed) {
- files.add(normalizePath(prefix + trimmed));
- }
+ // -z gives NUL-separated, unquoted output so non-ASCII (e.g. CJK) paths
+ // survive verbatim. Without it git octal-escapes and double-quotes such paths
+ // (the core.quotepath default), and the quoted form never matches a real file
+ // on disk → those files are silently dropped from the index. (#541)
+ const tracked = execFileSync('git', ['ls-files', '-z', '-c', '--recurse-submodules'], gitOpts);
+ for (const rel of tracked.split('\0')) {
+ if (rel) files.add(normalizePath(prefix + rel));
}
// Untracked files (submodules manage their own untracked state). Embedded git
// repos surface here as a single "subdir/" entry that git refuses to descend
// into — recurse into those as their own repos so their source gets indexed.
- const untracked = execFileSync('git', ['ls-files', '-o', '--exclude-standard'], gitOpts);
- for (const line of untracked.split('\n')) {
- const trimmed = line.trim();
- if (!trimmed) continue;
- if (trimmed.endsWith('/')) {
+ const untracked = execFileSync('git', ['ls-files', '-z', '-o', '--exclude-standard'], gitOpts);
+ for (const rel of untracked.split('\0')) {
+ if (!rel) continue;
+ if (rel.endsWith('/')) {
// git only emits a trailing-slash directory entry for an embedded repo.
// Guard with a .git check anyway, and skip anything else exactly as git
// itself skips it (we never descend into a non-repo opaque dir).
- const childDir = path.join(repoDir, trimmed);
+ const childDir = path.join(repoDir, rel);
if (fs.existsSync(path.join(childDir, '.git'))) {
- collectGitFiles(childDir, prefix + trimmed, files);
+ collectGitFiles(childDir, prefix + rel, files);
}
continue;
}
- files.add(normalizePath(prefix + trimmed));
+ files.add(normalizePath(prefix + rel));
}
}
diff --git a/src/extraction/languages/go.ts b/src/extraction/languages/go.ts
index 5b4d7975d..d6df2680b 100644
--- a/src/extraction/languages/go.ts
+++ b/src/extraction/languages/go.ts
@@ -56,8 +56,12 @@ export const goExtractor: LanguageExtractor = {
if (!receiver) return undefined;
// Find the type identifier inside the receiver
const text = getNodeText(receiver, source);
- // Extract type name from patterns like "(sl *Type)", "(sl Type)", "(*Type)", "(Type)"
- const match = text.match(/\*?\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/);
+ // Extract type name from "(sl *Type)", "(sl Type)", "(*Type)", "(Type)" and
+ // generic receivers "(s *Stack[T])". Anchor on the opening "(" and skip an
+ // optional receiver var name; the old `name)`-anchored pattern never matched
+ // the `[T])` suffix, so generic-type methods were orphaned from their type
+ // (no struct→method `contains` edge). (#583)
+ const match = text.match(/\(\s*(?:[A-Za-z_]\w*\s+)?\*?\s*([A-Za-z_]\w*)/);
return match?.[1];
},
};
diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts
index 7db606234..c6eb93ac9 100644
--- a/src/extraction/tree-sitter.ts
+++ b/src/extraction/tree-sitter.ts
@@ -630,7 +630,19 @@ export class TreeSitterExtractor {
}
}
}
- if (name === '') return; // Skip anonymous functions
+ if (name === '') {
+ // Don't emit a node for the anonymous wrapper itself, but still visit its
+ // body: AMD/RequireJS and CommonJS module wrappers (`define([], function(){…})`,
+ // `(function(){…})()`) hold named inner functions and calls that would
+ // otherwise be lost — the dispatcher set skipChildren, so nothing else
+ // descends into this subtree. (#528)
+ const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
+ ?? getChildByField(node, this.extractor.bodyField);
+ if (body) {
+ this.visitFunctionBody(body, '');
+ }
+ return;
+ }
// Check for misparse artifacts (e.g. C++ macros causing "namespace detail" functions)
// Skip the node but still visit the body for calls and structural nodes
diff --git a/src/graph/traversal.ts b/src/graph/traversal.ts
index c366721b8..82fc208d3 100644
--- a/src/graph/traversal.ts
+++ b/src/graph/traversal.ts
@@ -521,8 +521,11 @@ export class GraphTraverser {
}
}
- // Get all incoming edges (things that depend on this node)
- const incomingEdges = this.queries.getIncomingEdges(nodeId);
+ // Get all incoming edges (things that depend on this node). Exclude
+ // `contains`: a container "contains" its members but does not *depend* on
+ // them, so following it upward would climb to the parent class and then
+ // re-expand every sibling member — exploding impact for a leaf symbol. (#536)
+ const incomingEdges = this.queries.getIncomingEdges(nodeId).filter((e) => e.kind !== 'contains');
if (incomingEdges.length === 0) return;
const sources = this.queries.getNodesByIds(incomingEdges.map((e) => e.source));
diff --git a/src/mcp/proxy.ts b/src/mcp/proxy.ts
index 36f72e2d4..ceea34a67 100644
--- a/src/mcp/proxy.ts
+++ b/src/mcp/proxy.ts
@@ -230,6 +230,14 @@ export async function runLocalHandshakeProxy(deps: LocalHandshakeDeps): Promise<
routeToDaemon(line); // prime the daemon so it resolves the project (its reply is suppressed below)
} else if (msg.method === 'tools/list') {
writeClient({ jsonrpc: '2.0', id: msg.id, result: { tools: getStaticTools() } });
+ } else if (msg.method === 'resources/list') {
+ // No resources exposed — answer the probe locally so it never reaches
+ // the daemon as an unhandled method and logs `-32601`. (#621)
+ writeClient({ jsonrpc: '2.0', id: msg.id, result: { resources: [] } });
+ } else if (msg.method === 'resources/templates/list') {
+ writeClient({ jsonrpc: '2.0', id: msg.id, result: { resourceTemplates: [] } });
+ } else if (msg.method === 'prompts/list') {
+ writeClient({ jsonrpc: '2.0', id: msg.id, result: { prompts: [] } });
} else {
routeToDaemon(line);
}
diff --git a/src/mcp/session.ts b/src/mcp/session.ts
index beb957bb4..3b83e24a5 100644
--- a/src/mcp/session.ts
+++ b/src/mcp/session.ts
@@ -132,6 +132,19 @@ export class MCPSession {
case 'ping':
if (isRequest) this.transport.sendResult((message as JsonRpcRequest).id, {});
break;
+ case 'resources/list':
+ // We expose no MCP resources, but some clients (opencode, Codex) probe
+ // for them on connect; reply with an empty list instead of a
+ // MethodNotFound error that surfaces as a scary `-32601` log line. (#621)
+ if (isRequest) this.transport.sendResult((message as JsonRpcRequest).id, { resources: [] });
+ break;
+ case 'resources/templates/list':
+ if (isRequest) this.transport.sendResult((message as JsonRpcRequest).id, { resourceTemplates: [] });
+ break;
+ case 'prompts/list':
+ // Likewise — no prompts exposed, but answer the probe cleanly. (#621)
+ if (isRequest) this.transport.sendResult((message as JsonRpcRequest).id, { prompts: [] });
+ break;
default:
if (isRequest) {
this.transport.sendError(