context-mcp-server 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/codegraph/__pycache__/callflow_html.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/export.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/report.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/scanner.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/tree_html.cpython-313.pyc +0 -0
- package/codegraph/callflow_html.py +6 -4
- package/codegraph/export.py +25 -10
- package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/ast_extractor.py +37 -8
- package/codegraph/graph/__pycache__/builder.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/clustering.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/symbol_resolution.cpython-313.pyc +0 -0
- package/codegraph/graph/builder.py +27 -23
- package/codegraph/graph/clustering.py +5 -3
- package/codegraph/graph/query.py +5 -4
- package/codegraph/graph/symbol_resolution.py +14 -3
- package/codegraph/report.py +1 -1
- package/codegraph/scanner.py +1 -1
- package/codegraph/server.py +26 -5
- package/codegraph/tree_html.py +28 -30
- package/package.json +2 -2
- package/pyproject.toml +72 -72
- package/src/cli.js +12 -42
- package/src/db.js +26 -14
- package/src/http.js +1 -1
- package/src/search.js +4 -9
- package/src/server.js +16 -7
- package/src/templates/antigravity/GEMINI.md +18 -6
- package/src/templates/claude/CLAUDE.md +14 -1
- package/src/templates/claude/skills/SKILL.md +15 -3
- package/src/templates/codex/AGENTS.md +9 -2
- package/src/templates/cursor/cursor-rules.mdc +13 -4
- package/src/templates/gemini/GEMINI.md +14 -3
- package/src/templates/windsurf/windsurf-rules.md +14 -3
- package/src/tools/codegraph.js +4 -1
- package/src/tools/context.js +6 -6
- package/src/tools/errorCheck.js +3 -3
- package/src/tools/fileTools.js +2 -2
- package/src/tools/gitTools.js +1 -1
- package/src/tools/search.js +1 -1
- package/src/tools/symbolDetail.js +74 -0
- package/src/tools/toolRegistry.js +77 -0
- package/src/vector.js +7 -2
- package/uv.lock +3 -3
package/pyproject.toml
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "codegraph-mcp"
|
|
7
|
-
version = "1.1.
|
|
8
|
-
description = "Codebase knowledge graph MCP server — AST extraction, graph queries, community detection"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.11"
|
|
11
|
-
license = { text = "MIT" }
|
|
12
|
-
keywords = ["mcp", "ai", "codegraph", "knowledge-graph", "ast"]
|
|
13
|
-
dependencies = [
|
|
14
|
-
"mcp>=1.0.0",
|
|
15
|
-
"networkx>=3.0",
|
|
16
|
-
"pymupdf>=1.24",
|
|
17
|
-
"tree-sitter>=0.23.0",
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
[project.optional-dependencies]
|
|
21
|
-
leiden = [
|
|
22
|
-
"graspologic>=3.3",
|
|
23
|
-
]
|
|
24
|
-
treesitter = [
|
|
25
|
-
"tree-sitter>=0.23.0",
|
|
26
|
-
"tree-sitter-python",
|
|
27
|
-
"tree-sitter-javascript",
|
|
28
|
-
"tree-sitter-typescript",
|
|
29
|
-
"tree-sitter-go",
|
|
30
|
-
"tree-sitter-rust",
|
|
31
|
-
"tree-sitter-java",
|
|
32
|
-
"tree-sitter-c",
|
|
33
|
-
"tree-sitter-cpp",
|
|
34
|
-
"tree-sitter-c-sharp",
|
|
35
|
-
"tree-sitter-ruby",
|
|
36
|
-
"tree-sitter-php",
|
|
37
|
-
"tree-sitter-swift",
|
|
38
|
-
"tree-sitter-lua",
|
|
39
|
-
"tree-sitter-kotlin",
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
[project.scripts]
|
|
43
|
-
codegraph-mcp = "codegraph.server:main"
|
|
44
|
-
|
|
45
|
-
[tool.hatch.build.targets.wheel]
|
|
46
|
-
packages = ["codegraph"]
|
|
47
|
-
|
|
48
|
-
# ── uv ────────────────────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
[dependency-groups]
|
|
51
|
-
dev = [
|
|
52
|
-
"pytest>=8.0",
|
|
53
|
-
"pytest-asyncio>=0.23",
|
|
54
|
-
"ruff>=0.4",
|
|
55
|
-
"mypy>=1.10",
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
# ── ruff ─────────────────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
[tool.ruff]
|
|
61
|
-
target-version = "py311"
|
|
62
|
-
line-length = 100
|
|
63
|
-
|
|
64
|
-
[tool.ruff.lint]
|
|
65
|
-
select = ["E", "F", "I"]
|
|
66
|
-
ignore = ["E501"]
|
|
67
|
-
|
|
68
|
-
# ── pytest ────────────────────────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
[tool.pytest.ini_options]
|
|
71
|
-
asyncio_mode = "auto"
|
|
72
|
-
testpaths = ["tests"]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codegraph-mcp"
|
|
7
|
+
version = "1.1.4"
|
|
8
|
+
description = "Codebase knowledge graph MCP server — AST extraction, graph queries, community detection"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["mcp", "ai", "codegraph", "knowledge-graph", "ast"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"mcp>=1.0.0",
|
|
15
|
+
"networkx>=3.0",
|
|
16
|
+
"pymupdf>=1.24",
|
|
17
|
+
"tree-sitter>=0.23.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
leiden = [
|
|
22
|
+
"graspologic>=3.3",
|
|
23
|
+
]
|
|
24
|
+
treesitter = [
|
|
25
|
+
"tree-sitter>=0.23.0",
|
|
26
|
+
"tree-sitter-python",
|
|
27
|
+
"tree-sitter-javascript",
|
|
28
|
+
"tree-sitter-typescript",
|
|
29
|
+
"tree-sitter-go",
|
|
30
|
+
"tree-sitter-rust",
|
|
31
|
+
"tree-sitter-java",
|
|
32
|
+
"tree-sitter-c",
|
|
33
|
+
"tree-sitter-cpp",
|
|
34
|
+
"tree-sitter-c-sharp",
|
|
35
|
+
"tree-sitter-ruby",
|
|
36
|
+
"tree-sitter-php",
|
|
37
|
+
"tree-sitter-swift",
|
|
38
|
+
"tree-sitter-lua",
|
|
39
|
+
"tree-sitter-kotlin",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
codegraph-mcp = "codegraph.server:main"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["codegraph"]
|
|
47
|
+
|
|
48
|
+
# ── uv ────────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = [
|
|
52
|
+
"pytest>=8.0",
|
|
53
|
+
"pytest-asyncio>=0.23",
|
|
54
|
+
"ruff>=0.4",
|
|
55
|
+
"mypy>=1.10",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# ── ruff ─────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
target-version = "py311"
|
|
62
|
+
line-length = 100
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E", "F", "I"]
|
|
66
|
+
ignore = ["E501"]
|
|
67
|
+
|
|
68
|
+
# ── pytest ────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
asyncio_mode = "auto"
|
|
72
|
+
testpaths = ["tests"]
|
package/src/cli.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import readline from 'node:readline';
|
|
8
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, chmodSync } from 'node:fs';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
@@ -414,7 +414,6 @@ const _GLOBAL_GITIGNORE_ENTRIES = [
|
|
|
414
414
|
'.gemini/',
|
|
415
415
|
'.codex/',
|
|
416
416
|
'.windsurf/',
|
|
417
|
-
'.agents/',
|
|
418
417
|
// Build outputs and session artifacts
|
|
419
418
|
'codegraph-cache/',
|
|
420
419
|
'.mcp.json',
|
|
@@ -429,7 +428,6 @@ function _graphForProject(graphs, projectName) {
|
|
|
429
428
|
|
|
430
429
|
const _PROJECT_GITIGNORE_ENTRIES = [
|
|
431
430
|
'.claude/', '.cursor/', '.vscode/', '.gemini/', '.codex/',
|
|
432
|
-
'.agents/',
|
|
433
431
|
'codegraph-cache/', '.mcp.json', 'CLAUDE.md', 'GEMINI.md', 'AGENTS.md',
|
|
434
432
|
];
|
|
435
433
|
|
|
@@ -498,7 +496,12 @@ function _copyHooks(platform, dotDir, dir, hookFiles) {
|
|
|
498
496
|
for (const file of hookFiles) {
|
|
499
497
|
const src = join(hooksSrc, file);
|
|
500
498
|
if (existsSync(src)) {
|
|
501
|
-
|
|
499
|
+
const dest = join(hooksDest, file);
|
|
500
|
+
_writeFile(dest, readFileSync(src, 'utf8'), `${dotDir}/hooks/${file}`);
|
|
501
|
+
// Make executable on Unix so shells can run it without explicit `node` prefix
|
|
502
|
+
if (process.platform !== 'win32') {
|
|
503
|
+
try { chmodSync(dest, 0o755); } catch {}
|
|
504
|
+
}
|
|
502
505
|
}
|
|
503
506
|
}
|
|
504
507
|
}
|
|
@@ -618,11 +621,11 @@ const PLATFORMS = {
|
|
|
618
621
|
_mergeHooksIntoSettings(settingsPath, {
|
|
619
622
|
PreToolUse: [{
|
|
620
623
|
matcher: 'Bash',
|
|
621
|
-
hooks: [{ type: 'command', command: preHook
|
|
624
|
+
hooks: [{ type: 'command', command: `node "${preHook}"`, timeout: 30, statusMessage: 'Checking shell command' }],
|
|
622
625
|
}],
|
|
623
626
|
PostToolUse: [{
|
|
624
627
|
matcher: 'Bash',
|
|
625
|
-
hooks: [{ type: 'command', command: postHook
|
|
628
|
+
hooks: [{ type: 'command', command: `node "${postHook}"`, timeout: 30, statusMessage: 'Saving failed shell context' }],
|
|
626
629
|
}],
|
|
627
630
|
}, scope === 'project' ? '.claude/settings.json' : '~/.claude/settings.json');
|
|
628
631
|
// Register MCP server via claude CLI
|
|
@@ -732,11 +735,11 @@ const PLATFORMS = {
|
|
|
732
735
|
);
|
|
733
736
|
settings.hooks.BeforeTool = stripOld(settings.hooks.BeforeTool).concat([{
|
|
734
737
|
matcher: 'run_shell_command',
|
|
735
|
-
hooks: [{ type: 'command', command: `node ${beforeHook}`, timeout: 30 }],
|
|
738
|
+
hooks: [{ type: 'command', command: `node "${beforeHook}"`, timeout: 30 }],
|
|
736
739
|
}]);
|
|
737
740
|
settings.hooks.AfterTool = stripOld(settings.hooks.AfterTool).concat([{
|
|
738
741
|
matcher: 'run_shell_command',
|
|
739
|
-
hooks: [{ type: 'command', command: `node ${afterHook}`, timeout: 30 }],
|
|
742
|
+
hooks: [{ type: 'command', command: `node "${afterHook}"`, timeout: 30 }],
|
|
740
743
|
}]);
|
|
741
744
|
_writeFile(settingsPath,
|
|
742
745
|
JSON.stringify(settings, null, 2),
|
|
@@ -829,39 +832,6 @@ const PLATFORMS = {
|
|
|
829
832
|
}
|
|
830
833
|
},
|
|
831
834
|
},
|
|
832
|
-
antigravity: {
|
|
833
|
-
label: 'Antigravity IDE',
|
|
834
|
-
restartNote: 'Restart your Antigravity session to pick up hooks and rules.',
|
|
835
|
-
install(dir, scope) {
|
|
836
|
-
// Antigravity uses stdio-incompatible MCP transport — integrate via ctx CLI + GEMINI.md instead.
|
|
837
|
-
if (scope === 'project') {
|
|
838
|
-
// Post-tool hook — saves failed tool calls to context-mcp via ctx CLI
|
|
839
|
-
_copyHooks('antigravity', '.agents', dir, ['context-mcp-post-tool-use.js']);
|
|
840
|
-
const hookPath = join(dir, '.agents', 'hooks', 'context-mcp-post-tool-use.js');
|
|
841
|
-
_mergeJsonFile(join(dir, '.agents', 'hooks.json'), '.agents/hooks.json', obj => {
|
|
842
|
-
const strip = arr => (arr || []).filter(h => !String(h.command || '').includes('context-mcp-'));
|
|
843
|
-
obj.hooks = strip(obj.hooks).concat([{
|
|
844
|
-
event: 'PostToolUse',
|
|
845
|
-
command: `node "${hookPath}"`,
|
|
846
|
-
}]);
|
|
847
|
-
});
|
|
848
|
-
// Workflows (slash commands) — .agents/workflows/
|
|
849
|
-
const wfSrc = join(TPLS, 'antigravity', 'workflows');
|
|
850
|
-
for (const file of ['context-resume.md', 'graph-build.md', 'save-context.md']) {
|
|
851
|
-
const src = join(wfSrc, file);
|
|
852
|
-
if (existsSync(src)) _writeFile(join(dir, '.agents', 'workflows', file), readFileSync(src, 'utf8'), `.agents/workflows/${file}`);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
// Rules file — project root for project scope, ~/.config/antigravity/GEMINI.md for global
|
|
856
|
-
const agMd = _tpl('antigravity/GEMINI.md');
|
|
857
|
-
if (agMd) {
|
|
858
|
-
const agMdPath = scope === 'project'
|
|
859
|
-
? join(dir, 'GEMINI.md')
|
|
860
|
-
: join(homedir(), '.config', 'antigravity', 'GEMINI.md');
|
|
861
|
-
_writeFile(agMdPath, agMd, scope === 'project' ? 'GEMINI.md' : '~/.config/antigravity/GEMINI.md');
|
|
862
|
-
}
|
|
863
|
-
},
|
|
864
|
-
},
|
|
865
835
|
};
|
|
866
836
|
|
|
867
837
|
async function cmdInstall(args) {
|
|
@@ -923,7 +893,7 @@ async function cmdInstall(args) {
|
|
|
923
893
|
|
|
924
894
|
if (!keys.length) {
|
|
925
895
|
printSection('Install');
|
|
926
|
-
console.log(` ${muted('Usage:')} ctx install ${faint('[--initial] [--claude] [--cursor] [--vscode] [--gemini] [--codex] [--windsurf] [--
|
|
896
|
+
console.log(` ${muted('Usage:')} ctx install ${faint('[--initial] [--claude] [--cursor] [--vscode] [--gemini] [--codex] [--windsurf] [--all]')}`);
|
|
927
897
|
console.log('');
|
|
928
898
|
console.log(` ${accent('--initial ')} ${faint('Install / update Node.js + Python (codegraph) deps')}`);
|
|
929
899
|
console.log('');
|
package/src/db.js
CHANGED
|
@@ -178,6 +178,11 @@ function findEntryById(id, projectHint) {
|
|
|
178
178
|
const e = search(data);
|
|
179
179
|
if (e) return { entry: e, projectName: name };
|
|
180
180
|
}
|
|
181
|
+
// Always check 'global' — it is never in the projects index
|
|
182
|
+
if (!_projectData.has('global') && 'global' !== projectHint) {
|
|
183
|
+
const e = search(loadProjectData('global'));
|
|
184
|
+
if (e) return { entry: e, projectName: 'global' };
|
|
185
|
+
}
|
|
181
186
|
const idx = loadProjectsIndex();
|
|
182
187
|
for (const proj of idx) {
|
|
183
188
|
if (_projectData.has(proj.name) || proj.name === projectHint) continue;
|
|
@@ -384,10 +389,12 @@ export function getContext({ project, tags, limit = 20, compact = false, ids } =
|
|
|
384
389
|
|
|
385
390
|
if (ids && ids.length) {
|
|
386
391
|
const idSet = new Set(ids);
|
|
387
|
-
// Load all projects to find entries
|
|
392
|
+
// Load all projects to find entries — always include 'global' since it is
|
|
393
|
+
// never registered in the projects index (ensureProject skips it)
|
|
388
394
|
const idx = loadProjectsIndex();
|
|
389
395
|
const all = [];
|
|
390
396
|
const loaded = new Set(_projectData.keys());
|
|
397
|
+
loaded.add('global'); // ensure global is always searched
|
|
391
398
|
for (const proj of idx) loaded.add(proj.name);
|
|
392
399
|
for (const name of loaded) {
|
|
393
400
|
for (const e of getAllEntries(name)) {
|
|
@@ -403,10 +410,11 @@ export function getContext({ project, tags, limit = 20, compact = false, ids } =
|
|
|
403
410
|
const globalEntries = project !== 'global' ? getAllEntries('global') : [];
|
|
404
411
|
results = [...entries, ...globalEntries];
|
|
405
412
|
} else {
|
|
406
|
-
// No project filter: load all
|
|
413
|
+
// No project filter: load all — always include 'global' since it is never in the index
|
|
407
414
|
const idx = loadProjectsIndex();
|
|
408
415
|
const all = [];
|
|
409
416
|
const seen = new Set(_projectData.keys());
|
|
417
|
+
seen.add('global');
|
|
410
418
|
for (const proj of idx) seen.add(proj.name);
|
|
411
419
|
for (const name of seen) {
|
|
412
420
|
all.push(...getAllEntries(name));
|
|
@@ -434,7 +442,7 @@ export function getContextSince(since, project) {
|
|
|
434
442
|
} else {
|
|
435
443
|
const idx = loadProjectsIndex();
|
|
436
444
|
results = [];
|
|
437
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
445
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
438
446
|
for (const name of seen) results.push(...getAllEntries(name));
|
|
439
447
|
}
|
|
440
448
|
return results.filter(c => c.createdAt >= since);
|
|
@@ -450,12 +458,15 @@ export function searchContext({ query, project, limit = 10, compact = false }) {
|
|
|
450
458
|
} else {
|
|
451
459
|
const idx = loadProjectsIndex();
|
|
452
460
|
results = [];
|
|
453
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
461
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
454
462
|
for (const name of seen) results.push(...getAllEntries(name));
|
|
455
463
|
}
|
|
456
464
|
const scored = results.map(c => {
|
|
457
465
|
const haystack = `${c.title || ''} ${c.content || ''} ${(Array.isArray(c.tags) ? c.tags : []).join(' ')}`.toLowerCase();
|
|
458
|
-
const score = terms.reduce((s, t) =>
|
|
466
|
+
const score = terms.reduce((s, t) => {
|
|
467
|
+
try { return s + (haystack.match(new RegExp(`\\b${t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g'))?.length ?? 0); }
|
|
468
|
+
catch { return s; }
|
|
469
|
+
}, 0);
|
|
459
470
|
return { ...c, score };
|
|
460
471
|
}).filter(c => c.score > 0).sort((a, b) => b.score - a.score);
|
|
461
472
|
const sliced = scored.slice(0, limit).map(({ score, ...c }) => c);
|
|
@@ -467,8 +478,9 @@ export function deleteContext({ id, ids }) {
|
|
|
467
478
|
const idSet = new Set(ids && ids.length ? ids : (id ? [id] : []));
|
|
468
479
|
if (!idSet.size) return { deleted: 0 };
|
|
469
480
|
let deleted = 0;
|
|
470
|
-
// Scan all loaded projects
|
|
481
|
+
// Scan all loaded projects — always include 'global' since it is never in the index
|
|
471
482
|
const seen = new Set(_projectData.keys());
|
|
483
|
+
seen.add('global');
|
|
472
484
|
loadProjectsIndex().forEach(p => seen.add(p.name));
|
|
473
485
|
for (const name of seen) {
|
|
474
486
|
const data = loadProjectData(name);
|
|
@@ -524,7 +536,7 @@ export function countContext(project) {
|
|
|
524
536
|
if (!project) {
|
|
525
537
|
const idx = loadProjectsIndex();
|
|
526
538
|
let total = 0;
|
|
527
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
539
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
528
540
|
for (const name of seen) total += getAllEntries(name).length;
|
|
529
541
|
return total;
|
|
530
542
|
}
|
|
@@ -658,7 +670,7 @@ export function updateDiscussion({ id, name, title, description, content, status
|
|
|
658
670
|
let disc = null;
|
|
659
671
|
let projName = null;
|
|
660
672
|
const idx = loadProjectsIndex();
|
|
661
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
673
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
662
674
|
for (const pName of seen) {
|
|
663
675
|
const d = loadProjectData(pName);
|
|
664
676
|
const found = id ? d.discussions.find(x => x.id === id) : d.discussions.find(x => x.name === name);
|
|
@@ -690,9 +702,9 @@ export function getDiscussion({ project, name, id } = {}) {
|
|
|
690
702
|
if (name) return list.find(d => d.name === name) || null;
|
|
691
703
|
return null;
|
|
692
704
|
}
|
|
693
|
-
// Search all
|
|
705
|
+
// Search all — always include 'global' since it is never in the projects index
|
|
694
706
|
const idx = loadProjectsIndex();
|
|
695
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
707
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
696
708
|
for (const pName of seen) {
|
|
697
709
|
const d = loadProjectData(pName);
|
|
698
710
|
const found = id ? d.discussions.find(x => x.id === id) : d.discussions.find(x => x.name === name);
|
|
@@ -709,7 +721,7 @@ export function listDiscussions({ project, status, type } = {}) {
|
|
|
709
721
|
if (project !== 'global') list = [...list, ...loadProjectData('global').discussions];
|
|
710
722
|
} else {
|
|
711
723
|
const idx = loadProjectsIndex();
|
|
712
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
724
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
713
725
|
for (const pName of seen) list.push(...loadProjectData(pName).discussions);
|
|
714
726
|
}
|
|
715
727
|
if (status) list = list.filter(d => d.status === status);
|
|
@@ -730,7 +742,7 @@ export function linkContextToDiscussion({ discussionId, discussionName, contextI
|
|
|
730
742
|
let disc = null;
|
|
731
743
|
let discProject = null;
|
|
732
744
|
const idx = loadProjectsIndex();
|
|
733
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
745
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
734
746
|
for (const pName of seen) {
|
|
735
747
|
const d = loadProjectData(pName);
|
|
736
748
|
const found = discussionId
|
|
@@ -764,7 +776,7 @@ export function linkContextToDiscussion({ discussionId, discussionName, contextI
|
|
|
764
776
|
export function deleteDiscussion({ name, id }) {
|
|
765
777
|
init();
|
|
766
778
|
const idx = loadProjectsIndex();
|
|
767
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
779
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
768
780
|
for (const pName of seen) {
|
|
769
781
|
const data = loadProjectData(pName);
|
|
770
782
|
const before = data.discussions.length;
|
|
@@ -803,7 +815,7 @@ export function archiveExpired(project) {
|
|
|
803
815
|
processEntries(getAllEntries(project), project);
|
|
804
816
|
} else {
|
|
805
817
|
const idx = loadProjectsIndex();
|
|
806
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
818
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
807
819
|
for (const name of seen) processEntries(getAllEntries(name).slice(), name);
|
|
808
820
|
}
|
|
809
821
|
if (count > 0) markDirty();
|
package/src/http.js
CHANGED
|
@@ -622,7 +622,7 @@ async function handleRequest(req, res) {
|
|
|
622
622
|
<div class="step">
|
|
623
623
|
<span class="step-num">2</span>
|
|
624
624
|
<span class="step-title">Server URL</span>
|
|
625
|
-
<span class="step-content">Enter: <code>${req.headers['x-forwarded-proto'] || req.socket?.encrypted ? 'https' : 'http'}://${req.headers.host}</code></span>
|
|
625
|
+
<span class="step-content">Enter: <code>${req.headers['x-forwarded-proto'] === 'https' || req.socket?.encrypted ? 'https' : 'http'}://${req.headers.host}</code></span>
|
|
626
626
|
</div>
|
|
627
627
|
|
|
628
628
|
<div class="step">
|
package/src/search.js
CHANGED
|
@@ -98,15 +98,10 @@ export function search({ query, mode = 'semantic', project, limit = 10, id, comp
|
|
|
98
98
|
const all = getContext({ limit: 1000 });
|
|
99
99
|
const target = all.find(e => e.id === id || e.id.startsWith(id));
|
|
100
100
|
if (!target) throw new Error(`No entry found with id starting "${id}"`);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const explicit = all.filter(e => explicitIds.has(e.id));
|
|
106
|
-
const semantic = explicitIds.size < limit
|
|
107
|
-
? findRelated(target, all.filter(e => !explicitIds.has(e.id) && e.id !== target.id), limit - explicitIds.size)
|
|
108
|
-
: [];
|
|
109
|
-
return { target, results: [...explicit, ...semantic].slice(0, limit) };
|
|
101
|
+
// ponytail: relations/relatedBy never populated — pure semantic fallback
|
|
102
|
+
const others = all.filter(e => e.id !== target.id);
|
|
103
|
+
const results = findRelated(target, others, limit);
|
|
104
|
+
return { target, results };
|
|
110
105
|
}
|
|
111
106
|
default:
|
|
112
107
|
throw new Error(`Unknown search mode: ${mode}. Use: keyword, semantic, related`);
|
package/src/server.js
CHANGED
|
@@ -9,17 +9,20 @@ import { getConfig } from './config.js';
|
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const { version } = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
11
11
|
|
|
12
|
-
import * as contextTool
|
|
13
|
-
import * as searchTool
|
|
14
|
-
import * as planTool
|
|
15
|
-
import * as errorCheckTool
|
|
16
|
-
import * as fileTool
|
|
17
|
-
import * as gitTool
|
|
18
|
-
import * as codegraphTool
|
|
12
|
+
import * as contextTool from './tools/context.js';
|
|
13
|
+
import * as searchTool from './tools/search.js';
|
|
14
|
+
import * as planTool from './tools/plan.js';
|
|
15
|
+
import * as errorCheckTool from './tools/errorCheck.js';
|
|
16
|
+
import * as fileTool from './tools/fileTools.js';
|
|
17
|
+
import * as gitTool from './tools/gitTools.js';
|
|
18
|
+
import * as codegraphTool from './tools/codegraph.js';
|
|
19
|
+
import * as symbolDetailTool from './tools/symbolDetail.js';
|
|
20
|
+
import * as toolRegistryTool from './tools/toolRegistry.js';
|
|
19
21
|
|
|
20
22
|
const FILE_TOOL_NAMES = new Set(fileTool.definitions.map(d => d.name));
|
|
21
23
|
const GIT_TOOL_NAMES = new Set(gitTool.definitions.map(d => d.name));
|
|
22
24
|
const CODEGRAPH_TOOL_NAMES = codegraphTool.TOOL_NAMES;
|
|
25
|
+
const REGISTRY_TOOL_NAMES = toolRegistryTool.TOOL_NAMES;
|
|
23
26
|
|
|
24
27
|
export function createServer({ enableFileTools = false, enableGitTools = getConfig().access_git === true } = {}) {
|
|
25
28
|
const state = {
|
|
@@ -43,6 +46,8 @@ export function createServer({ enableFileTools = false, enableGitTools = getConf
|
|
|
43
46
|
if (enableFileTools) tools.push(...fileTool.definitions);
|
|
44
47
|
if (enableGitTools) tools.push(...gitTool.definitions);
|
|
45
48
|
tools.push(...codegraphTool.definitions);
|
|
49
|
+
tools.push(symbolDetailTool.definition);
|
|
50
|
+
tools.push(...toolRegistryTool.definitions);
|
|
46
51
|
return { tools };
|
|
47
52
|
});
|
|
48
53
|
|
|
@@ -73,6 +78,10 @@ export function createServer({ enableFileTools = false, enableGitTools = getConf
|
|
|
73
78
|
result = await gitTool.handle(name, args, state);
|
|
74
79
|
} else if (CODEGRAPH_TOOL_NAMES.has(name)) {
|
|
75
80
|
result = codegraphTool.handle(name, args, state);
|
|
81
|
+
} else if (name === symbolDetailTool.definition.name) {
|
|
82
|
+
result = await symbolDetailTool.handle(args, state);
|
|
83
|
+
} else if (REGISTRY_TOOL_NAMES.has(name)) {
|
|
84
|
+
result = toolRegistryTool.handle(name);
|
|
76
85
|
} else {
|
|
77
86
|
throw new Error(`Unknown tool: ${name}`);
|
|
78
87
|
}
|
|
@@ -74,14 +74,26 @@ Run `ctx search "<query>" --project <project>` before asking the user to re-expl
|
|
|
74
74
|
## 6. ContextGraph CLI
|
|
75
75
|
|
|
76
76
|
```
|
|
77
|
-
ctx graph build <path>
|
|
78
|
-
ctx graph arch <path>
|
|
79
|
-
ctx graph query <path> "<question>"
|
|
80
|
-
ctx graph nodes <path> <type>
|
|
81
|
-
ctx graph report <path>
|
|
77
|
+
ctx graph build <path> → build AST graph (run once, incremental)
|
|
78
|
+
ctx graph arch <path> → module map: files, exports, imports
|
|
79
|
+
ctx graph query <path> "<question>" → structural question about the codebase
|
|
80
|
+
ctx graph nodes <path> <type> → list all nodes of a type (class|function|module|file|struct|table)
|
|
81
|
+
ctx graph report <path> → god nodes, clusters, surprising connections
|
|
82
|
+
ctx graph affected <path> <node> → blast radius — what breaks if X changes?
|
|
82
83
|
```
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
MCP tools (when available):
|
|
86
|
+
- `get_symbol_detail(name, path)` — source code for one function, no full file read
|
|
87
|
+
- `tool_registry()` — which tools have side effects
|
|
88
|
+
- `safety_policy()` — which actions need confirmation
|
|
89
|
+
|
|
90
|
+
Decision rules:
|
|
91
|
+
- **Unknown codebase**: `ctx graph report` first
|
|
92
|
+
- **Before any refactor**: `ctx graph affected <path> <node>` — FIRST
|
|
93
|
+
- **"Show me function X"**: `get_symbol_detail` or `ctx graph query`
|
|
94
|
+
- **`ctx search`** finds past decisions. **`ctx graph query`** finds code. Different tools.
|
|
95
|
+
|
|
96
|
+
Never read files for structure questions.
|
|
85
97
|
|
|
86
98
|
---
|
|
87
99
|
|
|
@@ -122,10 +122,23 @@ codegraph_html(path, formats?) → regenerate visualizations (auto-run
|
|
|
122
122
|
| Where is function X defined? | `codegraph_query node:"X"` |
|
|
123
123
|
| What does module Y depend on? | `codegraph_query question:"what does Y import?"` |
|
|
124
124
|
| What are all the classes? | `codegraph_nodes type:"class"` |
|
|
125
|
-
| Most connected files? | `codegraph_report` |
|
|
125
|
+
| Most connected files / god nodes? | `codegraph_report` |
|
|
126
126
|
| What breaks if I change X? | `codegraph_affected node:"X"` |
|
|
127
|
+
| Show me only the code for function X? | `get_symbol_detail name:"X"` |
|
|
128
|
+
| Which tools have side effects? | `tool_registry` |
|
|
129
|
+
|
|
130
|
+
### When to reach for each graph tool
|
|
131
|
+
|
|
132
|
+
- **Unknown territory** (first look at any codebase): `codegraph_report` — shows bottlenecks + surprises first
|
|
133
|
+
- **"Where is X defined?"**: `codegraph_query node:"X"` — faster than grep
|
|
134
|
+
- **"What does this module do?"**: `codegraph_arch` — static module map, no reads needed
|
|
135
|
+
- **Before any refactor or rename**: `codegraph_affected node:"X"` — see blast radius FIRST
|
|
136
|
+
- **"List all classes/functions"**: `codegraph_nodes type:"class"` or `type:"function"`
|
|
137
|
+
- **Files changed since last session**: `codegraph_build` is incremental — re-run after adding files
|
|
138
|
+
- **"Show me just that function"**: `get_symbol_detail` — avoids reading the whole file
|
|
127
139
|
|
|
128
140
|
**Never read files for structure questions — use graph tools first.**
|
|
141
|
+
**`search` finds past decisions. `codegraph_query` finds code symbols. Different tools.**
|
|
129
142
|
|
|
130
143
|
---
|
|
131
144
|
|
|
@@ -34,7 +34,7 @@ Call `context` tool **before any tool or response** with:
|
|
|
34
34
|
- `rootPath: "<absolute path to git repo root>"` — required for sandbox + graph lookup
|
|
35
35
|
|
|
36
36
|
Returns:
|
|
37
|
-
- `recentEntries` — last 15 entries; newest
|
|
37
|
+
- `recentEntries` — last 15 entries; newest 2 + high-signal entries have full content, rest have 200-char preview
|
|
38
38
|
- `activePlans` — in-progress plans; read them before starting any new work
|
|
39
39
|
- `codegraph` — `{ built: true/false, nodes, edges, communities }`
|
|
40
40
|
- `stats.totalEntries` — if ≥ 20, write a compaction summary before proceeding (see Rule 4)
|
|
@@ -116,10 +116,13 @@ codegraph_build(path) → AST graph: functions, classes, imports, edges
|
|
|
116
116
|
```
|
|
117
117
|
codegraph_arch(path) → module map: every file, exports, imports
|
|
118
118
|
codegraph_query(path, question?, node?) → find symbol or answer structural question
|
|
119
|
-
codegraph_nodes(path, type) → list all nodes of a type
|
|
119
|
+
codegraph_nodes(path, type) → list all nodes of a type (class|function|module|file|struct|table)
|
|
120
120
|
codegraph_report(path) → god nodes, clusters, structural analysis
|
|
121
121
|
codegraph_affected(path, node, depth?) → blast radius BFS — what breaks if X changes?
|
|
122
122
|
codegraph_html(path, formats?) → regenerate visualizations (auto-runs on every build)
|
|
123
|
+
get_symbol_detail(name, path) → source code for one function/class — no full file read
|
|
124
|
+
tool_registry() → which tools have side effects + approval requirements
|
|
125
|
+
safety_policy() → which actions need user confirmation
|
|
123
126
|
```
|
|
124
127
|
|
|
125
128
|
| Question | Tool |
|
|
@@ -130,6 +133,15 @@ codegraph_html(path, formats?) → regenerate visualizations (auto-run
|
|
|
130
133
|
| List all classes/functions | `codegraph_nodes type:"class"` |
|
|
131
134
|
| Most connected / central files | `codegraph_report` |
|
|
132
135
|
| What breaks if I change X? | `codegraph_affected node:"X"` |
|
|
136
|
+
| Show me just the code for function X | `get_symbol_detail name:"X"` |
|
|
137
|
+
| Which tools are dangerous? | `tool_registry` or `safety_policy` |
|
|
138
|
+
|
|
139
|
+
### When to reach for each graph tool
|
|
140
|
+
|
|
141
|
+
- **Unknown territory**: `codegraph_report` first — god nodes + surprises
|
|
142
|
+
- **Before any refactor or rename**: `codegraph_affected` — blast radius FIRST
|
|
143
|
+
- **"Show me just that function"**: `get_symbol_detail` — avoids reading the whole file
|
|
144
|
+
- **`search`** finds past decisions. **`codegraph_query`** finds code symbols. Different tools.
|
|
133
145
|
|
|
134
146
|
---
|
|
135
147
|
|
|
@@ -141,4 +153,4 @@ codegraph_html(path, formats?) → regenerate visualizations (auto-run
|
|
|
141
153
|
4. Compaction at ≥ 20 entries — before starting task
|
|
142
154
|
5. Plan for multi-file work — `status:"done"` deletes it
|
|
143
155
|
6. Search before asking about past work
|
|
144
|
-
7. Graph tools before files
|
|
156
|
+
7. Graph tools before files — `codegraph_affected` before any refactor
|
|
@@ -90,8 +90,15 @@ codegraph_affected(path, node, depth?) -> blast radius BFS — what breaks if
|
|
|
90
90
|
codegraph_html(path, formats?) -> regenerate visualizations on demand
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
Decision rules:
|
|
94
|
+
- Unknown codebase: `codegraph_report` first (god nodes + surprises)
|
|
95
|
+
- "Where is X?": `codegraph_query node:"X"`
|
|
96
|
+
- Before any refactor: `codegraph_affected node:"X"` (blast radius)
|
|
97
|
+
- List all classes: `codegraph_nodes type:"class"`
|
|
98
|
+
- Just the function body: `get_symbol_detail name:"X"` (no full file read)
|
|
99
|
+
- `search` finds past decisions. `codegraph_query` finds code symbols. Different tools.
|
|
100
|
+
|
|
101
|
+
Read files only when you need exact bug or implementation details not in the graph.
|
|
95
102
|
|
|
96
103
|
---
|
|
97
104
|
|
|
@@ -34,18 +34,27 @@ Build once: `codegraph_build(path)` — auto-generates visualizations, then quer
|
|
|
34
34
|
```
|
|
35
35
|
codegraph_arch(path) → module map (files, exports, imports)
|
|
36
36
|
codegraph_query(path, question?, node?) → find symbol or answer structural question
|
|
37
|
-
codegraph_nodes(path, type) → list all nodes of a type
|
|
37
|
+
codegraph_nodes(path, type) → list all nodes of a type (class|function|module|file|struct|table)
|
|
38
38
|
codegraph_report(path) → god nodes, clusters, structural analysis
|
|
39
39
|
codegraph_affected(path, node, depth?) → blast radius — what breaks if X changes?
|
|
40
40
|
codegraph_html(path, formats?) → regenerate visualizations on demand
|
|
41
|
+
get_symbol_detail(name, path) → source code for one function/class — no full file read
|
|
42
|
+
tool_registry() → which tools have side effects + approval requirements
|
|
43
|
+
safety_policy() → which actions need user confirmation before running
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
Decision rules:
|
|
47
|
+
- **Unknown codebase**: `codegraph_report` first (god nodes + surprises)
|
|
48
|
+
- **"Where is X?"**: `codegraph_query node:"X"` — faster than grep
|
|
49
|
+
- **Before any refactor**: `codegraph_affected node:"X"` — see blast radius FIRST
|
|
50
|
+
- **"Show me function X"**: `get_symbol_detail` — no full file read needed
|
|
51
|
+
- **List all classes**: `codegraph_nodes type:"class"`
|
|
52
|
+
- **`search`** finds past decisions. **`codegraph_query`** finds code symbols. Different tools.
|
|
44
53
|
|
|
45
54
|
## Rules
|
|
46
55
|
|
|
47
56
|
1. `context.resume` first — every conversation
|
|
48
57
|
2. Always pass `project`
|
|
49
58
|
3. `search` before asking the user about past work
|
|
50
|
-
4. `
|
|
51
|
-
5. Files only for bugs and logic
|
|
59
|
+
4. Graph tools before reading files — `codegraph_affected` before any refactor
|
|
60
|
+
5. Files only for bugs and exact logic
|