cortex-local 2.0.9__tar.gz → 2.1.0__tar.gz
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.
- {cortex_local-2.0.9 → cortex_local-2.1.0}/PKG-INFO +1 -1
- cortex_local-2.1.0/cortex/cli.py +141 -0
- cortex_local-2.1.0/cortex/config.py +64 -0
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/indexer.py +12 -9
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/mcp_server.py +47 -4
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/state.py +10 -9
- {cortex_local-2.0.9 → cortex_local-2.1.0}/pyproject.toml +1 -1
- cortex_local-2.0.9/cortex/cli.py +0 -68
- cortex_local-2.0.9/cortex/config.py +0 -18
- {cortex_local-2.0.9 → cortex_local-2.1.0}/README.md +0 -0
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/__init__.py +0 -0
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/core/__init__.py +0 -0
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/core/cache.py +0 -0
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/core/chunker.py +0 -0
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/core/code_graph.py +0 -0
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/core/global_graph.py +0 -0
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/core/graph.py +0 -0
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/core/notes_graph.py +0 -0
- {cortex_local-2.0.9 → cortex_local-2.1.0}/cortex/embedder.py +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""cortex CLI — index <path> and serve (stdio MCP)."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from .config import (
|
|
11
|
+
WORKSPACES_DIR, get_active_workspace, set_active_workspace, get_workspace_dir,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
def cli() -> None:
|
|
17
|
+
"""cortex: local semantic search MCP server."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@cli.command()
|
|
21
|
+
@click.argument("path", type=click.Path(exists=True, file_okay=False, resolve_path=True))
|
|
22
|
+
def index(path: str) -> None:
|
|
23
|
+
"""Index PATH (notes and/or code) into ~/.cortex."""
|
|
24
|
+
from .indexer import index_path
|
|
25
|
+
index_path(Path(path))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@cli.command()
|
|
29
|
+
def serve() -> None:
|
|
30
|
+
"""Run cortex MCP server on stdio."""
|
|
31
|
+
from .mcp_server import run
|
|
32
|
+
run()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@cli.command()
|
|
36
|
+
def pull_models() -> None:
|
|
37
|
+
"""Pre-download fastembed models."""
|
|
38
|
+
from .embedder import pull_models as _pull
|
|
39
|
+
_pull()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@cli.command()
|
|
43
|
+
def stats() -> None:
|
|
44
|
+
"""Show cortex efficiency stats."""
|
|
45
|
+
from .state import _stats
|
|
46
|
+
from .mcp_server import _fmt_stats
|
|
47
|
+
from .config import VERSION
|
|
48
|
+
click.echo(_fmt_stats(VERSION, _stats, current=True))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@cli.group()
|
|
52
|
+
def workspace() -> None:
|
|
53
|
+
"""Manage cortex workspaces."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@workspace.command("list")
|
|
57
|
+
def workspace_list() -> None:
|
|
58
|
+
"""List all workspaces and their status."""
|
|
59
|
+
active = get_active_workspace()
|
|
60
|
+
if not WORKSPACES_DIR.exists():
|
|
61
|
+
click.echo(f"No workspaces found. Active: '{active}' (not yet indexed).")
|
|
62
|
+
return
|
|
63
|
+
workspaces = sorted([d.name for d in WORKSPACES_DIR.iterdir() if d.is_dir()])
|
|
64
|
+
if not workspaces:
|
|
65
|
+
click.echo(f"No workspaces found. Active: '{active}'.")
|
|
66
|
+
return
|
|
67
|
+
click.echo(f"Active workspace: {active}\n")
|
|
68
|
+
for ws in workspaces:
|
|
69
|
+
marker = "* " if ws == active else " "
|
|
70
|
+
has_index = (WORKSPACES_DIR / ws / "qdrant").exists()
|
|
71
|
+
click.echo(f"{marker}{ws} ({'indexed' if has_index else 'empty'})")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@workspace.command("create")
|
|
75
|
+
@click.argument("name")
|
|
76
|
+
def workspace_create(name: str) -> None:
|
|
77
|
+
"""Create a new workspace."""
|
|
78
|
+
ws_dir = get_workspace_dir(name)
|
|
79
|
+
if ws_dir.exists():
|
|
80
|
+
click.echo(f"Workspace '{name}' already exists.")
|
|
81
|
+
return
|
|
82
|
+
ws_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
click.echo(f"Created workspace '{name}'. Switch with: cortex workspace switch {name}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@workspace.command("switch")
|
|
87
|
+
@click.argument("name")
|
|
88
|
+
def workspace_switch(name: str) -> None:
|
|
89
|
+
"""Switch the active workspace."""
|
|
90
|
+
ws_dir = get_workspace_dir(name)
|
|
91
|
+
if not ws_dir.exists():
|
|
92
|
+
click.echo(f"Workspace '{name}' does not exist. Create it with: cortex workspace create {name}")
|
|
93
|
+
return
|
|
94
|
+
set_active_workspace(name)
|
|
95
|
+
click.echo(f"Switched to workspace '{name}'.")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@workspace.command("delete")
|
|
99
|
+
@click.argument("name")
|
|
100
|
+
@click.confirmation_option(prompt="Delete workspace and all its data?")
|
|
101
|
+
def workspace_delete(name: str) -> None:
|
|
102
|
+
"""Delete a workspace and all its indexed data."""
|
|
103
|
+
if name == get_active_workspace():
|
|
104
|
+
click.echo("Cannot delete active workspace. Switch first.")
|
|
105
|
+
return
|
|
106
|
+
ws_dir = get_workspace_dir(name)
|
|
107
|
+
if not ws_dir.exists():
|
|
108
|
+
click.echo(f"Workspace '{name}' does not exist.")
|
|
109
|
+
return
|
|
110
|
+
shutil.rmtree(ws_dir)
|
|
111
|
+
click.echo(f"Deleted workspace '{name}'.")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@workspace.command("current")
|
|
115
|
+
def workspace_current() -> None:
|
|
116
|
+
"""Print the active workspace name."""
|
|
117
|
+
click.echo(get_active_workspace())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@cli.command("install")
|
|
121
|
+
def install_cmd() -> None:
|
|
122
|
+
"""Download embedding models and register cortex MCP in Claude Code."""
|
|
123
|
+
from .embedder import pull_models as _pull
|
|
124
|
+
click.echo("Downloading embedding models...")
|
|
125
|
+
_pull()
|
|
126
|
+
mcp_cmd = "claude mcp add cortex --transport stdio cortex serve"
|
|
127
|
+
claude = shutil.which("claude")
|
|
128
|
+
if not claude:
|
|
129
|
+
click.echo("Models ready. Register MCP manually (claude not found in PATH):")
|
|
130
|
+
click.echo(f" {mcp_cmd}")
|
|
131
|
+
return
|
|
132
|
+
click.echo("Registering MCP server in Claude Code...")
|
|
133
|
+
result = subprocess.run(
|
|
134
|
+
[claude, "mcp", "add", "cortex", "--transport", "stdio", "cortex", "serve"],
|
|
135
|
+
capture_output=True, text=True,
|
|
136
|
+
)
|
|
137
|
+
if result.returncode == 0:
|
|
138
|
+
click.echo("Done. cortex is ready — restart Claude Code to activate.")
|
|
139
|
+
else:
|
|
140
|
+
click.echo(f"MCP registration failed. Run manually:\n {mcp_cmd}")
|
|
141
|
+
click.echo(result.stderr.strip())
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Paths and constants for cortex local mode."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
CORTEX_DIR = Path.home() / ".cortex"
|
|
7
|
+
WORKSPACES_DIR = CORTEX_DIR / "workspaces"
|
|
8
|
+
ACTIVE_WORKSPACE_FILE = CORTEX_DIR / "active_workspace"
|
|
9
|
+
|
|
10
|
+
EMBED_MODEL = "nomic-ai/nomic-embed-text-v1.5"
|
|
11
|
+
VECTOR_SIZE = 768
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from importlib.metadata import version
|
|
15
|
+
VERSION = version("cortex-local")
|
|
16
|
+
except Exception:
|
|
17
|
+
VERSION = "dev"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_active_workspace() -> str:
|
|
21
|
+
if ACTIVE_WORKSPACE_FILE.exists():
|
|
22
|
+
return ACTIVE_WORKSPACE_FILE.read_text().strip() or "default"
|
|
23
|
+
return "default"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_active_workspace(name: str) -> None:
|
|
27
|
+
CORTEX_DIR.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
ACTIVE_WORKSPACE_FILE.write_text(name)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_workspace_dir(name: str | None = None) -> Path:
|
|
32
|
+
return WORKSPACES_DIR / (name or get_active_workspace())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def qdrant_path(ws: str | None = None) -> str:
|
|
36
|
+
return str(get_workspace_dir(ws) / "qdrant")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def data_dir(ws: str | None = None) -> Path:
|
|
40
|
+
return get_workspace_dir(ws)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cache_file(ws: str | None = None) -> Path:
|
|
44
|
+
return get_workspace_dir(ws) / "embed_cache.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def stats_file(ws: str | None = None) -> Path:
|
|
48
|
+
return get_workspace_dir(ws) / "stats.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def migrate_legacy() -> None:
|
|
52
|
+
"""Move old flat ~/.cortex/ layout into ~/.cortex/workspaces/default/ on first run."""
|
|
53
|
+
old_qdrant = CORTEX_DIR / "qdrant"
|
|
54
|
+
default_dir = WORKSPACES_DIR / "default"
|
|
55
|
+
if not old_qdrant.exists() or WORKSPACES_DIR.exists():
|
|
56
|
+
return
|
|
57
|
+
default_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
shutil.move(str(old_qdrant), str(default_dir / "qdrant"))
|
|
59
|
+
for fname in ("embed_cache.json", "stats.json"):
|
|
60
|
+
src = CORTEX_DIR / fname
|
|
61
|
+
if src.exists():
|
|
62
|
+
shutil.move(str(src), str(default_dir / fname))
|
|
63
|
+
for f in CORTEX_DIR.glob("graph_*.json"):
|
|
64
|
+
shutil.move(str(f), str(default_dir / f.name))
|
|
@@ -10,7 +10,7 @@ from qdrant_client.models import (
|
|
|
10
10
|
MatchValue, PointStruct, SparseVector, SparseVectorParams, VectorParams,
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
-
from .config import
|
|
13
|
+
from .config import qdrant_path, data_dir, cache_file, VECTOR_SIZE, migrate_legacy
|
|
14
14
|
from .embedder import embed, sparse_embed
|
|
15
15
|
from .core.chunker import chunk_file, CODE_EXTS, SKIP_DIRS
|
|
16
16
|
from .core.cache import load_cache, save_cache
|
|
@@ -18,7 +18,10 @@ from .core import graph as _graph
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _qdrant() -> QdrantClient:
|
|
21
|
-
|
|
21
|
+
migrate_legacy()
|
|
22
|
+
d = data_dir()
|
|
23
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
return QdrantClient(path=qdrant_path())
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
def _ensure_collection(client: QdrantClient, name: str) -> bool:
|
|
@@ -116,7 +119,7 @@ def index_notes(notes_dir: Path) -> int:
|
|
|
116
119
|
md_files = [p for p in notes_dir.rglob("*.md")
|
|
117
120
|
if ".obsidian" not in p.parts and ".git" not in p.parts]
|
|
118
121
|
|
|
119
|
-
cache = load_cache("notes",
|
|
122
|
+
cache = load_cache("notes", cache_file())
|
|
120
123
|
if client.get_collection("notes").points_count == 0:
|
|
121
124
|
cache = {}
|
|
122
125
|
new_cache: dict = {}
|
|
@@ -139,12 +142,12 @@ def index_notes(notes_dir: Path) -> int:
|
|
|
139
142
|
print(f" {rel}: {len(points)} chunk(s)", flush=True)
|
|
140
143
|
new_cache[rel] = mtime
|
|
141
144
|
|
|
142
|
-
save_cache("notes", new_cache,
|
|
145
|
+
save_cache("notes", new_cache, cache_file())
|
|
143
146
|
print(f"Done. {total} chunks indexed, {cached}/{len(md_files)} files cached.", flush=True)
|
|
144
147
|
|
|
145
148
|
try:
|
|
146
149
|
G = _graph.build_notes_graph(notes_dir)
|
|
147
|
-
_graph.persist_notes_graph(G,
|
|
150
|
+
_graph.persist_notes_graph(G, data_dir())
|
|
148
151
|
except Exception as e:
|
|
149
152
|
print(f" notes graph failed: {e}")
|
|
150
153
|
|
|
@@ -170,7 +173,7 @@ def index_code(code_dir: Path) -> int:
|
|
|
170
173
|
and not any(s in p.parts for s in SKIP_DIRS)
|
|
171
174
|
]
|
|
172
175
|
|
|
173
|
-
cache = load_cache("code",
|
|
176
|
+
cache = load_cache("code", cache_file())
|
|
174
177
|
if client.get_collection("code").points_count == 0:
|
|
175
178
|
cache = {}
|
|
176
179
|
new_cache = dict(cache)
|
|
@@ -198,7 +201,7 @@ def index_code(code_dir: Path) -> int:
|
|
|
198
201
|
total += len(points)
|
|
199
202
|
new_cache[cache_key] = mtime
|
|
200
203
|
|
|
201
|
-
save_cache("code", new_cache,
|
|
204
|
+
save_cache("code", new_cache, cache_file())
|
|
202
205
|
print(f" {repo_name}: {total} chunks from {len(code_files) - cached} files ({cached} cached)", flush=True)
|
|
203
206
|
|
|
204
207
|
all_files = {
|
|
@@ -208,7 +211,7 @@ def index_code(code_dir: Path) -> int:
|
|
|
208
211
|
G = _graph.build_code_graph(code_dir, all_files)
|
|
209
212
|
if G is not None:
|
|
210
213
|
metadata = _graph.compute_code_metadata(G)
|
|
211
|
-
_graph.persist_code_graph(G, metadata, repo_name,
|
|
214
|
+
_graph.persist_code_graph(G, metadata, repo_name, data_dir())
|
|
212
215
|
for file_rel, meta in metadata.items():
|
|
213
216
|
pids = file_point_ids.get(file_rel, [])
|
|
214
217
|
if pids:
|
|
@@ -233,7 +236,7 @@ def index_code(code_dir: Path) -> int:
|
|
|
233
236
|
def index_path(path: Path) -> None:
|
|
234
237
|
"""Index a path — .md files as notes, code files as code."""
|
|
235
238
|
path = path.resolve()
|
|
236
|
-
|
|
239
|
+
data_dir().mkdir(parents=True, exist_ok=True)
|
|
237
240
|
|
|
238
241
|
md_files = list(path.rglob("*.md"))
|
|
239
242
|
code_files = [p for p in path.rglob("*")
|
|
@@ -10,9 +10,13 @@ from mcp.server.fastmcp import FastMCP
|
|
|
10
10
|
from qdrant_client import QdrantClient
|
|
11
11
|
from qdrant_client.models import Prefetch, Fusion, SparseVector
|
|
12
12
|
|
|
13
|
-
from .config import
|
|
13
|
+
from .config import (
|
|
14
|
+
qdrant_path, data_dir, VERSION, VECTOR_SIZE,
|
|
15
|
+
WORKSPACES_DIR, get_active_workspace, set_active_workspace,
|
|
16
|
+
get_workspace_dir,
|
|
17
|
+
)
|
|
14
18
|
from .embedder import embed, sparse_embed
|
|
15
|
-
from .state import _stats, get_graph_meta, save_stats
|
|
19
|
+
from .state import _stats, get_graph_meta, save_stats, invalidate_graph_cache
|
|
16
20
|
from .indexer import index_path
|
|
17
21
|
|
|
18
22
|
mcp = FastMCP("cortex")
|
|
@@ -24,7 +28,7 @@ _reindex_path: str = ""
|
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
def _qdrant() -> QdrantClient:
|
|
27
|
-
return QdrantClient(path=
|
|
31
|
+
return QdrantClient(path=qdrant_path())
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
# ── Reindex ─────────────────────────────────────────────────────────────────
|
|
@@ -54,7 +58,7 @@ def _ppr_block(matched_files: list, matched_scores: list) -> str | None:
|
|
|
54
58
|
from .core.notes_graph import ppr_augment
|
|
55
59
|
except Exception:
|
|
56
60
|
return None
|
|
57
|
-
graph_path =
|
|
61
|
+
graph_path = data_dir() / "graph_notes.json"
|
|
58
62
|
extras, reason = ppr_augment(matched_files, matched_scores, graph_path, _return_reason=True)
|
|
59
63
|
if extras:
|
|
60
64
|
_stats["ppr_fires"] += 1
|
|
@@ -376,6 +380,45 @@ cortex is available as an MCP tool. Use it proactively:
|
|
|
376
380
|
{cortex_section}"""
|
|
377
381
|
|
|
378
382
|
|
|
383
|
+
@mcp.tool()
|
|
384
|
+
def switch_workspace(name: str) -> str:
|
|
385
|
+
"""Switch the active cortex workspace.
|
|
386
|
+
|
|
387
|
+
Each workspace has its own isolated index (notes + code).
|
|
388
|
+
Use when switching between work/personal/project contexts.
|
|
389
|
+
Example: switch_workspace("work")
|
|
390
|
+
"""
|
|
391
|
+
ws_dir = get_workspace_dir(name)
|
|
392
|
+
if not ws_dir.exists():
|
|
393
|
+
return (
|
|
394
|
+
f"Workspace '{name}' does not exist.\n"
|
|
395
|
+
f"Create it with: cortex workspace create {name}"
|
|
396
|
+
)
|
|
397
|
+
set_active_workspace(name)
|
|
398
|
+
invalidate_graph_cache()
|
|
399
|
+
return f"Switched to workspace '{name}'. search_code and search_notes now use this workspace's index."
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@mcp.tool()
|
|
403
|
+
def list_workspaces() -> str:
|
|
404
|
+
"""List all cortex workspaces and their status.
|
|
405
|
+
|
|
406
|
+
Shows which workspace is currently active and whether each has an index.
|
|
407
|
+
"""
|
|
408
|
+
active = get_active_workspace()
|
|
409
|
+
if not WORKSPACES_DIR.exists():
|
|
410
|
+
return f"No workspaces found. Active: '{active}' (not yet indexed)."
|
|
411
|
+
workspaces = sorted([d.name for d in WORKSPACES_DIR.iterdir() if d.is_dir()])
|
|
412
|
+
if not workspaces:
|
|
413
|
+
return f"No workspaces found. Active: '{active}'."
|
|
414
|
+
lines = [f"Active workspace: {active}\n"]
|
|
415
|
+
for ws in workspaces:
|
|
416
|
+
marker = "* " if ws == active else " "
|
|
417
|
+
has_index = (WORKSPACES_DIR / ws / "qdrant").exists()
|
|
418
|
+
lines.append(f"{marker}{ws} ({'indexed' if has_index else 'empty'})")
|
|
419
|
+
return "\n".join(lines)
|
|
420
|
+
|
|
421
|
+
|
|
379
422
|
def run() -> None:
|
|
380
423
|
"""Entry point for stdio MCP server."""
|
|
381
424
|
mcp.run(transport="stdio")
|
|
@@ -4,10 +4,9 @@ import json
|
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from .config import
|
|
7
|
+
from .config import data_dir, stats_file, VERSION
|
|
8
8
|
|
|
9
9
|
_graph_cache: dict = {}
|
|
10
|
-
|
|
11
10
|
_stats: dict = {}
|
|
12
11
|
|
|
13
12
|
|
|
@@ -35,8 +34,9 @@ def _default_stats() -> dict:
|
|
|
35
34
|
def _load_stats() -> dict:
|
|
36
35
|
defaults = _default_stats()
|
|
37
36
|
try:
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
sf = stats_file()
|
|
38
|
+
if sf.exists():
|
|
39
|
+
data = json.loads(sf.read_text())
|
|
40
40
|
saved = data.get("versions", {}).get(VERSION, {})
|
|
41
41
|
if saved:
|
|
42
42
|
return {**defaults, **saved}
|
|
@@ -47,12 +47,13 @@ def _load_stats() -> dict:
|
|
|
47
47
|
|
|
48
48
|
def save_stats() -> None:
|
|
49
49
|
try:
|
|
50
|
+
sf = stats_file()
|
|
50
51
|
existing: dict = {}
|
|
51
|
-
if
|
|
52
|
-
existing = json.loads(
|
|
52
|
+
if sf.exists():
|
|
53
|
+
existing = json.loads(sf.read_text()).get("versions", {})
|
|
53
54
|
existing[VERSION] = {**_stats, "last_saved": datetime.now(timezone.utc).isoformat()}
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
sf.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
sf.write_text(json.dumps({"versions": existing}, indent=2))
|
|
56
57
|
except Exception:
|
|
57
58
|
pass
|
|
58
59
|
|
|
@@ -61,7 +62,7 @@ def get_graph_meta(repo_name: str) -> dict:
|
|
|
61
62
|
if repo_name in _graph_cache:
|
|
62
63
|
_stats["graph_cache_hits"] += 1
|
|
63
64
|
return _graph_cache[repo_name]
|
|
64
|
-
path =
|
|
65
|
+
path = data_dir() / f"graph_{repo_name}.json"
|
|
65
66
|
if not path.exists():
|
|
66
67
|
_stats["graph_cache_misses"] += 1
|
|
67
68
|
return {}
|
cortex_local-2.0.9/cortex/cli.py
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
"""cortex CLI — index <path> and serve (stdio MCP)."""
|
|
2
|
-
|
|
3
|
-
import shutil
|
|
4
|
-
import subprocess
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
import click
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@click.group()
|
|
12
|
-
def cli() -> None:
|
|
13
|
-
"""cortex: local semantic search MCP server."""
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@cli.command()
|
|
17
|
-
@click.argument("path", type=click.Path(exists=True, file_okay=False, resolve_path=True))
|
|
18
|
-
def index(path: str) -> None:
|
|
19
|
-
"""Index PATH (notes and/or code) into ~/.cortex."""
|
|
20
|
-
from .indexer import index_path
|
|
21
|
-
index_path(Path(path))
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@cli.command()
|
|
25
|
-
def serve() -> None:
|
|
26
|
-
"""Run cortex MCP server on stdio."""
|
|
27
|
-
from .mcp_server import run
|
|
28
|
-
run()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@cli.command()
|
|
32
|
-
def pull_models() -> None:
|
|
33
|
-
"""Pre-download fastembed models."""
|
|
34
|
-
from .embedder import pull_models as _pull
|
|
35
|
-
_pull()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@cli.command()
|
|
39
|
-
def stats() -> None:
|
|
40
|
-
"""Show cortex efficiency stats."""
|
|
41
|
-
from .state import _stats
|
|
42
|
-
from .mcp_server import _fmt_stats
|
|
43
|
-
from .config import VERSION
|
|
44
|
-
click.echo(_fmt_stats(VERSION, _stats, current=True))
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@cli.command("install")
|
|
48
|
-
def install_cmd() -> None:
|
|
49
|
-
"""Download embedding models and register cortex MCP in Claude Code."""
|
|
50
|
-
from .embedder import pull_models as _pull
|
|
51
|
-
click.echo("Downloading embedding models...")
|
|
52
|
-
_pull()
|
|
53
|
-
mcp_cmd = "claude mcp add cortex --transport stdio cortex serve"
|
|
54
|
-
claude = shutil.which("claude")
|
|
55
|
-
if not claude:
|
|
56
|
-
click.echo("Models ready. Register MCP manually (claude not found in PATH):")
|
|
57
|
-
click.echo(f" {mcp_cmd}")
|
|
58
|
-
return
|
|
59
|
-
click.echo("Registering MCP server in Claude Code...")
|
|
60
|
-
result = subprocess.run(
|
|
61
|
-
[claude, "mcp", "add", "cortex", "--transport", "stdio", "cortex", "serve"],
|
|
62
|
-
capture_output=True, text=True,
|
|
63
|
-
)
|
|
64
|
-
if result.returncode == 0:
|
|
65
|
-
click.echo("Done. cortex is ready — restart Claude Code to activate.")
|
|
66
|
-
else:
|
|
67
|
-
click.echo(f"MCP registration failed. Run manually:\n {mcp_cmd}")
|
|
68
|
-
click.echo(result.stderr.strip())
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"""Paths and constants for cortex local mode."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
CORTEX_DIR = Path.home() / ".cortex"
|
|
6
|
-
QDRANT_PATH = str(CORTEX_DIR / "qdrant")
|
|
7
|
-
DATA_DIR = CORTEX_DIR
|
|
8
|
-
STATS_FILE = CORTEX_DIR / "stats.json"
|
|
9
|
-
CACHE_FILE = CORTEX_DIR / "embed_cache.json"
|
|
10
|
-
|
|
11
|
-
EMBED_MODEL = "nomic-ai/nomic-embed-text-v1.5"
|
|
12
|
-
VECTOR_SIZE = 768
|
|
13
|
-
|
|
14
|
-
try:
|
|
15
|
-
from importlib.metadata import version
|
|
16
|
-
VERSION = version("cortex")
|
|
17
|
-
except Exception:
|
|
18
|
-
VERSION = "dev"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|