context-mcp-server 1.1.2 → 1.1.3
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/callflow_html.py +2 -2
- package/codegraph/export.py +1 -1
- package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/ast_extractor.py +3 -1
- 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 +4 -1
- package/codegraph/report.py +1 -1
- package/codegraph/scanner.py +1 -1
- package/codegraph/server.py +20 -3
- package/package.json +2 -2
- package/pyproject.toml +1 -1
- package/src/cli.js +12 -42
- package/src/db.js +22 -13
- package/src/http.js +1 -1
- package/src/tools/errorCheck.js +3 -3
- package/src/tools/fileTools.js +2 -2
- package/src/tools/gitTools.js +1 -1
- package/uv.lock +3 -3
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
Persistent memory and codebase knowledge graph for AI coding assistants — delivered as a single MCP server.
|
|
13
13
|
|
|
14
|
-
One shared context store across Claude Code, Cursor, Gemini CLI, Codex, Windsurf, VS Code Copilot,
|
|
14
|
+
One shared context store across Claude Code, Cursor, Gemini CLI, Codex, Windsurf, VS Code Copilot, Claude.ai, and ChatGPT. Save context from one AI, pick it up in another.
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -77,8 +77,7 @@ ctx install --cursor # Cursor
|
|
|
77
77
|
ctx install --vscode # VS Code Copilot
|
|
78
78
|
ctx install --gemini # Gemini CLI
|
|
79
79
|
ctx install --codex # Codex CLI
|
|
80
|
-
ctx install --windsurf
|
|
81
|
-
ctx install --antigravity # Antigravity IDE
|
|
80
|
+
ctx install --windsurf # Windsurf
|
|
82
81
|
```
|
|
83
82
|
|
|
84
83
|
For Codex project installs, `ctx install --codex` writes:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -210,7 +210,7 @@ def to_html(graph_dict: dict, output_path: str) -> str:
|
|
|
210
210
|
<p>{len(nodes)} nodes · {len(edges)} edges · {len(communities)} communities · generated {escape(generated)}</p>
|
|
211
211
|
{god_html}
|
|
212
212
|
<div class="mermaid">
|
|
213
|
-
{
|
|
213
|
+
{overview_mermaid}
|
|
214
214
|
</div>
|
|
215
215
|
</section>"""
|
|
216
216
|
|
|
@@ -239,7 +239,7 @@ def to_html(graph_dict: dict, output_path: str) -> str:
|
|
|
239
239
|
<div class="card"><h3>Key Files</h3><ul>{files_html}</ul></div>
|
|
240
240
|
</div>
|
|
241
241
|
<div class="mermaid">
|
|
242
|
-
{
|
|
242
|
+
{diagram}
|
|
243
243
|
</div>
|
|
244
244
|
{"<h3>Incoming Cross-Community Calls</h3>" + table if table else ""}
|
|
245
245
|
<hr>
|
package/codegraph/export.py
CHANGED
|
@@ -416,7 +416,7 @@ def to_obsidian(graph_dict: dict, output_dir: str) -> str:
|
|
|
416
416
|
fpath = n.get("file", "")
|
|
417
417
|
ntype = n.get("type", "")
|
|
418
418
|
comm = node_community.get(nid)
|
|
419
|
-
comm_tag = _obsidian_tag(comm
|
|
419
|
+
comm_tag = _obsidian_tag(comm.get("label", f"community_{comm['id']}")) if comm else "misc"
|
|
420
420
|
|
|
421
421
|
lines = [
|
|
422
422
|
"---",
|
|
Binary file
|
|
@@ -530,8 +530,10 @@ def _extract_with_regex(source: str, rel_path: str, ext: str) -> list[dict]:
|
|
|
530
530
|
"imports": import_names[:],
|
|
531
531
|
})
|
|
532
532
|
|
|
533
|
+
# brace-scope tracking only works for brace-delimited languages
|
|
534
|
+
_BRACE_LANGS = {"javascript", "typescript", "go", "rust", "java", "c", "cpp", "csharp", "php", "swift"}
|
|
533
535
|
func_nodes = [n for n in nodes if n["type"] == "function"]
|
|
534
|
-
if func_nodes:
|
|
536
|
+
if func_nodes and lang in _BRACE_LANGS:
|
|
535
537
|
_attach_calls_brace(lines, func_nodes)
|
|
536
538
|
|
|
537
539
|
return nodes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -25,44 +25,48 @@ def build(all_nodes: list[dict]) -> "nx.DiGraph | dict":
|
|
|
25
25
|
|
|
26
26
|
G = nx.DiGraph()
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
_name_to_ids: dict[str, list[str]] = {} # name -> [ids] (may be multiple)
|
|
29
|
+
file_rep: dict[str, str] = {} # rel_path -> representative node id (module > file > first)
|
|
30
|
+
file_imports: dict[str, list[str]] = {} # rel_path -> aggregated import names
|
|
30
31
|
|
|
31
32
|
for node in all_nodes:
|
|
32
33
|
nid = node.get("id", "")
|
|
33
34
|
if not nid:
|
|
34
35
|
continue
|
|
35
36
|
G.add_node(nid, **{k: v for k, v in node.items() if k not in ("imports", "calls", "relations")})
|
|
36
|
-
|
|
37
|
-
if
|
|
38
|
-
|
|
37
|
+
name = node.get("name", "")
|
|
38
|
+
if name:
|
|
39
|
+
_name_to_ids.setdefault(name, []).append(nid)
|
|
40
|
+
# Track a representative node per file (prefer module, then file, then first seen)
|
|
41
|
+
frel = node.get("file", "")
|
|
42
|
+
ntype = node.get("type", "")
|
|
43
|
+
if frel and (frel not in file_rep or ntype in ("module", "file")):
|
|
44
|
+
file_rep[frel] = nid
|
|
45
|
+
# Aggregate imports per file from any node type
|
|
46
|
+
for imp in node.get("imports", []):
|
|
47
|
+
lst = file_imports.setdefault(frel, [])
|
|
48
|
+
if imp not in lst:
|
|
49
|
+
lst.append(imp)
|
|
50
|
+
|
|
51
|
+
# Unambiguous name→id map: only include names that resolve to exactly one node
|
|
52
|
+
node_by_name: dict[str, str] = {n: ids[0] for n, ids in _name_to_ids.items() if len(ids) == 1}
|
|
39
53
|
|
|
40
|
-
# Build file-path lookup from module nodes
|
|
54
|
+
# Build file-path lookup from all file/module representative nodes
|
|
41
55
|
file_node: dict[str, str] = {}
|
|
42
|
-
for rel_path,
|
|
56
|
+
for rel_path, rep_id in file_rep.items():
|
|
43
57
|
p = rel_path.replace("\\", "/")
|
|
44
58
|
stem = p.split("/")[-1].split(".")[0]
|
|
45
59
|
base = p.split("/")[-1]
|
|
46
60
|
for key in (stem, base, p):
|
|
47
|
-
file_node.setdefault(key,
|
|
48
|
-
|
|
49
|
-
# defined-in edges: child nodes → their module
|
|
50
|
-
for node in all_nodes:
|
|
51
|
-
nid = node.get("id", "")
|
|
52
|
-
for rel in node.get("relations", []):
|
|
53
|
-
target_id = rel.get("id") or node_by_name.get(rel.get("name", ""))
|
|
54
|
-
if target_id and target_id != nid:
|
|
55
|
-
G.add_edge(nid, target_id,
|
|
56
|
-
relation=rel.get("relation", "relates-to"),
|
|
57
|
-
confidence=rel.get("confidence", "EXTRACTED"))
|
|
61
|
+
file_node.setdefault(key, rep_id)
|
|
58
62
|
|
|
59
|
-
# Import edges:
|
|
63
|
+
# Import edges: file → file (aggregated from all node types per file)
|
|
60
64
|
seen_edges: set[tuple] = set()
|
|
61
|
-
for
|
|
62
|
-
|
|
65
|
+
for frel, imports in file_imports.items():
|
|
66
|
+
src_id = file_rep.get(frel)
|
|
67
|
+
if not src_id:
|
|
63
68
|
continue
|
|
64
|
-
|
|
65
|
-
for imp in node.get("imports", []):
|
|
69
|
+
for imp in imports:
|
|
66
70
|
clean = imp.lstrip(".")
|
|
67
71
|
parts = clean.replace("\\", "/").split("/")
|
|
68
72
|
last = parts[-1]
|
|
@@ -64,8 +64,9 @@ def _partition(G: nx.Graph, resolution: float = 1.0) -> dict[str, int]:
|
|
|
64
64
|
except ImportError:
|
|
65
65
|
pass
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
louvain_sig = inspect.signature(nx.community.louvain_communities).parameters
|
|
68
|
+
kwargs = {"seed": 42, "resolution": resolution}
|
|
69
|
+
if "max_level" in louvain_sig:
|
|
69
70
|
kwargs["max_level"] = 10
|
|
70
71
|
communities = nx.community.louvain_communities(stable, **kwargs)
|
|
71
72
|
return {node: cid for cid, nodes in enumerate(communities) for node in nodes}
|
|
@@ -102,7 +103,8 @@ def cluster(
|
|
|
102
103
|
if exclude_hubs_percentile is not None:
|
|
103
104
|
degrees = sorted(d for _, d in G.degree())
|
|
104
105
|
if degrees:
|
|
105
|
-
idx
|
|
106
|
+
# idx is the last position we keep; nodes beyond this are hubs
|
|
107
|
+
idx = min(len(degrees) - 1, int(len(degrees) * exclude_hubs_percentile / 100))
|
|
106
108
|
threshold = degrees[idx]
|
|
107
109
|
hub_nodes = {n for n, d in G.degree() if d > threshold}
|
|
108
110
|
|
package/codegraph/graph/query.py
CHANGED
|
@@ -5,6 +5,7 @@ No LLM call on query — pure graph + keyword matching.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
|
+
from collections import deque
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
10
11
|
|
|
@@ -114,10 +115,10 @@ def _find_by_name(nodes: list, name: str) -> dict | None:
|
|
|
114
115
|
|
|
115
116
|
|
|
116
117
|
def _detect_intent(q: str) -> str:
|
|
117
|
-
if any(w in q for w in ("depend", "import", "use", "require")):
|
|
118
|
-
return "depends_on"
|
|
119
118
|
if any(w in q for w in ("used by", "who calls", "caller")):
|
|
120
119
|
return "used_by"
|
|
120
|
+
if any(w in q for w in ("depend", "import", "use", "require")):
|
|
121
|
+
return "depends_on"
|
|
121
122
|
if any(w in q for w in ("path", "connect", "relate", "between")):
|
|
122
123
|
return "path"
|
|
123
124
|
if any(w in q for w in ("list", "all", "show all", "every")):
|
|
@@ -164,9 +165,9 @@ def _shortest_path(from_node: dict, to_node: dict, edges: list, nodes: list) ->
|
|
|
164
165
|
|
|
165
166
|
start, end = from_node["id"], to_node["id"]
|
|
166
167
|
visited = {start: None}
|
|
167
|
-
queue = [start]
|
|
168
|
+
queue: deque[str] = deque([start])
|
|
168
169
|
while queue:
|
|
169
|
-
cur = queue.
|
|
170
|
+
cur = queue.popleft()
|
|
170
171
|
if cur == end:
|
|
171
172
|
break
|
|
172
173
|
for nb in adj.get(cur, []):
|
|
@@ -79,11 +79,14 @@ def resolve_calls(
|
|
|
79
79
|
continue
|
|
80
80
|
|
|
81
81
|
target_ids: list[str] = []
|
|
82
|
+
via_import = False
|
|
82
83
|
|
|
83
84
|
# 1. Try (imported_module_stem, callee_name) for import-guided resolution
|
|
84
85
|
for stem in imported_stems:
|
|
85
86
|
candidates = module_index.get((stem, callee_name), [])
|
|
86
87
|
target_ids.extend(candidates)
|
|
88
|
+
if target_ids:
|
|
89
|
+
via_import = True
|
|
87
90
|
|
|
88
91
|
# 2. Fall back to global unique name match
|
|
89
92
|
if not target_ids:
|
|
@@ -101,7 +104,7 @@ def resolve_calls(
|
|
|
101
104
|
continue
|
|
102
105
|
|
|
103
106
|
existing_edge_keys.add(key)
|
|
104
|
-
confidence = "EXTRACTED" if
|
|
107
|
+
confidence = "EXTRACTED" if via_import else "INFERRED"
|
|
105
108
|
new_edges.append({
|
|
106
109
|
"from": caller_id,
|
|
107
110
|
"to": target_id,
|
package/codegraph/report.py
CHANGED
package/codegraph/scanner.py
CHANGED
|
@@ -19,7 +19,7 @@ def walk_files(root: str, extra_ignore: set | None = None) -> Iterator[str]:
|
|
|
19
19
|
ignore = DEFAULT_IGNORE | (extra_ignore or set())
|
|
20
20
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
21
21
|
# Prune ignored dirs in-place so os.walk doesn't descend
|
|
22
|
-
dirnames[:] = [d for d in dirnames if
|
|
22
|
+
dirnames[:] = [d for d in dirnames if not _should_ignore(d, ignore)]
|
|
23
23
|
for fname in filenames:
|
|
24
24
|
ext = Path(fname).suffix.lower()
|
|
25
25
|
if fname in SKIP_FILENAMES or ext in SKIP_EXTENSIONS:
|
package/codegraph/server.py
CHANGED
|
@@ -192,13 +192,30 @@ async def _build(args: dict) -> dict:
|
|
|
192
192
|
|
|
193
193
|
all_nodes: list[dict] = []
|
|
194
194
|
|
|
195
|
-
for nodes in cached.
|
|
196
|
-
|
|
195
|
+
for rel_path, nodes in cached.items():
|
|
196
|
+
if nodes:
|
|
197
|
+
all_nodes.extend(nodes)
|
|
198
|
+
else:
|
|
199
|
+
# Previously cached as empty — still add a file-level node so it's visible
|
|
200
|
+
all_nodes.append({
|
|
201
|
+
"id": f"{rel_path}::file::{Path(rel_path).name}",
|
|
202
|
+
"name": Path(rel_path).name,
|
|
203
|
+
"type": "file",
|
|
204
|
+
"file": rel_path,
|
|
205
|
+
})
|
|
197
206
|
|
|
198
207
|
for rel_path, abs_path in changed.items():
|
|
199
208
|
cat = classify_file(abs_path)
|
|
200
209
|
if cat in ("code", "sql"):
|
|
201
210
|
nodes = ast_extract(abs_path, rel_path)
|
|
211
|
+
if not nodes:
|
|
212
|
+
# Extractor found no symbols — still represent the file so it appears in the graph
|
|
213
|
+
nodes = [{
|
|
214
|
+
"id": f"{rel_path}::file::{Path(rel_path).name}",
|
|
215
|
+
"name": Path(rel_path).name,
|
|
216
|
+
"type": "file",
|
|
217
|
+
"file": rel_path,
|
|
218
|
+
}]
|
|
202
219
|
set_cached_nodes(cache, rel_path, file_hash(abs_path), nodes)
|
|
203
220
|
all_nodes.extend(nodes)
|
|
204
221
|
elif cat == "config":
|
|
@@ -232,7 +249,7 @@ async def _build(args: dict) -> dict:
|
|
|
232
249
|
generate_report(graph_dict, root)
|
|
233
250
|
save_cache(root, cache)
|
|
234
251
|
try:
|
|
235
|
-
export_all(graph_dict, root)
|
|
252
|
+
export_all(graph_dict, str(Path(root) / "codegraph-cache"))
|
|
236
253
|
except Exception:
|
|
237
254
|
pass
|
|
238
255
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "Persistent AI memory + codebase knowledge graph MCP server. Works across Claude Code, Cursor, Gemini CLI, Codex, Windsurf, VS Code Copilot,
|
|
3
|
+
"version": "1.1.3",
|
|
4
|
+
"description": "Persistent AI memory + codebase knowledge graph MCP server. Works across Claude Code, Cursor, Gemini CLI, Codex, Windsurf, VS Code Copilot, Claude.ai, and ChatGPT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"context-mcp": "./src/index.js",
|
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codegraph-mcp"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.3"
|
|
8
8
|
description = "Codebase knowledge graph MCP server — AST extraction, graph queries, community detection"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
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,7 +458,7 @@ 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 => {
|
|
@@ -467,8 +475,9 @@ export function deleteContext({ id, ids }) {
|
|
|
467
475
|
const idSet = new Set(ids && ids.length ? ids : (id ? [id] : []));
|
|
468
476
|
if (!idSet.size) return { deleted: 0 };
|
|
469
477
|
let deleted = 0;
|
|
470
|
-
// Scan all loaded projects
|
|
478
|
+
// Scan all loaded projects — always include 'global' since it is never in the index
|
|
471
479
|
const seen = new Set(_projectData.keys());
|
|
480
|
+
seen.add('global');
|
|
472
481
|
loadProjectsIndex().forEach(p => seen.add(p.name));
|
|
473
482
|
for (const name of seen) {
|
|
474
483
|
const data = loadProjectData(name);
|
|
@@ -524,7 +533,7 @@ export function countContext(project) {
|
|
|
524
533
|
if (!project) {
|
|
525
534
|
const idx = loadProjectsIndex();
|
|
526
535
|
let total = 0;
|
|
527
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
536
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
528
537
|
for (const name of seen) total += getAllEntries(name).length;
|
|
529
538
|
return total;
|
|
530
539
|
}
|
|
@@ -658,7 +667,7 @@ export function updateDiscussion({ id, name, title, description, content, status
|
|
|
658
667
|
let disc = null;
|
|
659
668
|
let projName = null;
|
|
660
669
|
const idx = loadProjectsIndex();
|
|
661
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
670
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
662
671
|
for (const pName of seen) {
|
|
663
672
|
const d = loadProjectData(pName);
|
|
664
673
|
const found = id ? d.discussions.find(x => x.id === id) : d.discussions.find(x => x.name === name);
|
|
@@ -690,9 +699,9 @@ export function getDiscussion({ project, name, id } = {}) {
|
|
|
690
699
|
if (name) return list.find(d => d.name === name) || null;
|
|
691
700
|
return null;
|
|
692
701
|
}
|
|
693
|
-
// Search all
|
|
702
|
+
// Search all — always include 'global' since it is never in the projects index
|
|
694
703
|
const idx = loadProjectsIndex();
|
|
695
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
704
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
696
705
|
for (const pName of seen) {
|
|
697
706
|
const d = loadProjectData(pName);
|
|
698
707
|
const found = id ? d.discussions.find(x => x.id === id) : d.discussions.find(x => x.name === name);
|
|
@@ -709,7 +718,7 @@ export function listDiscussions({ project, status, type } = {}) {
|
|
|
709
718
|
if (project !== 'global') list = [...list, ...loadProjectData('global').discussions];
|
|
710
719
|
} else {
|
|
711
720
|
const idx = loadProjectsIndex();
|
|
712
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
721
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
713
722
|
for (const pName of seen) list.push(...loadProjectData(pName).discussions);
|
|
714
723
|
}
|
|
715
724
|
if (status) list = list.filter(d => d.status === status);
|
|
@@ -730,7 +739,7 @@ export function linkContextToDiscussion({ discussionId, discussionName, contextI
|
|
|
730
739
|
let disc = null;
|
|
731
740
|
let discProject = null;
|
|
732
741
|
const idx = loadProjectsIndex();
|
|
733
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
742
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
734
743
|
for (const pName of seen) {
|
|
735
744
|
const d = loadProjectData(pName);
|
|
736
745
|
const found = discussionId
|
|
@@ -764,7 +773,7 @@ export function linkContextToDiscussion({ discussionId, discussionName, contextI
|
|
|
764
773
|
export function deleteDiscussion({ name, id }) {
|
|
765
774
|
init();
|
|
766
775
|
const idx = loadProjectsIndex();
|
|
767
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
776
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
768
777
|
for (const pName of seen) {
|
|
769
778
|
const data = loadProjectData(pName);
|
|
770
779
|
const before = data.discussions.length;
|
|
@@ -803,7 +812,7 @@ export function archiveExpired(project) {
|
|
|
803
812
|
processEntries(getAllEntries(project), project);
|
|
804
813
|
} else {
|
|
805
814
|
const idx = loadProjectsIndex();
|
|
806
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
815
|
+
const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
|
|
807
816
|
for (const name of seen) processEntries(getAllEntries(name).slice(), name);
|
|
808
817
|
}
|
|
809
818
|
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/tools/errorCheck.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function handle(args, state) {
|
|
|
35
35
|
|
|
36
36
|
if (action === 'check') {
|
|
37
37
|
const results = unifiedSearch({ mode: 'semantic', query: errorMessage, project, limit: 5 })
|
|
38
|
-
.filter(r => r.
|
|
38
|
+
.filter(r => Array.isArray(r.tags) && r.tags.includes('error-log'));
|
|
39
39
|
if (results.length > 0 && results[0].similarity > 0.4) {
|
|
40
40
|
return {
|
|
41
41
|
success: true, found: true,
|
|
@@ -53,8 +53,8 @@ export async function handle(args, state) {
|
|
|
53
53
|
sessionId: state.sessionId || null,
|
|
54
54
|
title: `Error: ${errorMessage.split('\n')[0].slice(0, 60)}`,
|
|
55
55
|
content: `Command: ${command || 'unknown'}\n\nError:\n${errorMessage}\n\nSolution:\n${solution}`,
|
|
56
|
-
type: '
|
|
57
|
-
status: '
|
|
56
|
+
type: 'note',
|
|
57
|
+
status: 'active',
|
|
58
58
|
tags: ['error-log', command].filter(Boolean),
|
|
59
59
|
});
|
|
60
60
|
fireAutoLink(entry.id, state);
|
package/src/tools/fileTools.js
CHANGED
|
@@ -107,7 +107,7 @@ export async function handle(name, args, state) {
|
|
|
107
107
|
saveAutoContext({
|
|
108
108
|
title: `wrote ${filePath.split(/[\\/]/).pop()}`,
|
|
109
109
|
content: `write_file: created/overwrote ${filePath}`,
|
|
110
|
-
type: '
|
|
110
|
+
type: 'note',
|
|
111
111
|
files: [{ path: filePath, action: 'modified' }],
|
|
112
112
|
tags: ['file-write'],
|
|
113
113
|
state,
|
|
@@ -159,7 +159,7 @@ export async function handle(name, args, state) {
|
|
|
159
159
|
title: `patched ${filePath.split(/[\\/]/).pop()}${sorted.length > 1 ? ` (${sorted.length} edits)` : ''}`,
|
|
160
160
|
content: `patch_file: ${sorted.length} edit(s) in ${filePath}\n` +
|
|
161
161
|
sorted.map(e => ` ${e.description}: -${e.old_str.split('\n').length} +${e.new_str.split('\n').length} lines`).join('\n'),
|
|
162
|
-
type: '
|
|
162
|
+
type: 'note',
|
|
163
163
|
files: [{ path: filePath, action: 'modified' }],
|
|
164
164
|
tags: ['file-patch'],
|
|
165
165
|
state,
|
package/src/tools/gitTools.js
CHANGED
|
@@ -175,7 +175,7 @@ export async function handle(name, args, state) {
|
|
|
175
175
|
saveAutoContext({
|
|
176
176
|
title: `git commit: ${args.message.slice(0, 57)}${args.message.length > 57 ? '...' : ''}`,
|
|
177
177
|
content: `hash: ${hash} | branch: ${branch}\nmessage: ${args.message}\nfiles: ${stagedFiles.map(f => f.path).join(', ')}`,
|
|
178
|
-
type: '
|
|
178
|
+
type: 'note',
|
|
179
179
|
files: stagedFiles,
|
|
180
180
|
tags: ['git', 'commit', branch],
|
|
181
181
|
state,
|
package/uv.lock
CHANGED
|
@@ -201,7 +201,7 @@ wheels = [
|
|
|
201
201
|
|
|
202
202
|
[[package]]
|
|
203
203
|
name = "codegraph-mcp"
|
|
204
|
-
version = "1.1.
|
|
204
|
+
version = "1.1.2"
|
|
205
205
|
source = { editable = "." }
|
|
206
206
|
dependencies = [
|
|
207
207
|
{ name = "mcp" },
|
|
@@ -2308,8 +2308,8 @@ name = "uvicorn"
|
|
|
2308
2308
|
version = "0.46.0"
|
|
2309
2309
|
source = { registry = "https://pypi.org/simple" }
|
|
2310
2310
|
dependencies = [
|
|
2311
|
-
{ name = "click" },
|
|
2312
|
-
{ name = "h11" },
|
|
2311
|
+
{ name = "click", marker = "python_full_version < '3.15' or sys_platform != 'emscripten'" },
|
|
2312
|
+
{ name = "h11", marker = "python_full_version < '3.15' or sys_platform != 'emscripten'" },
|
|
2313
2313
|
]
|
|
2314
2314
|
sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" }
|
|
2315
2315
|
wheels = [
|