cortex-local 2.4.3__tar.gz → 2.4.5__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.
Files changed (24) hide show
  1. {cortex_local-2.4.3 → cortex_local-2.4.5}/PKG-INFO +12 -3
  2. {cortex_local-2.4.3 → cortex_local-2.4.5}/README.md +10 -1
  3. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/cli.py +85 -5
  4. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/config.py +1 -0
  5. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/embedder.py +3 -3
  6. {cortex_local-2.4.3 → cortex_local-2.4.5}/.gitignore +0 -0
  7. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/__init__.py +0 -0
  8. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/core/__init__.py +0 -0
  9. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/core/cache.py +0 -0
  10. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/core/chunker.py +0 -0
  11. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/core/code_graph.py +0 -0
  12. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/core/formatters.py +0 -0
  13. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/core/global_graph.py +0 -0
  14. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/core/graph.py +0 -0
  15. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/core/notes_graph.py +0 -0
  16. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/core/roslyn_analyzer.py +0 -0
  17. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/core/roslyn_tool/CortexAnalyzer.csproj +0 -0
  18. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/core/roslyn_tool/Program.cs +0 -0
  19. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/indexer.py +0 -0
  20. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/mcp_server.py +0 -0
  21. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/state.py +0 -0
  22. {cortex_local-2.4.3 → cortex_local-2.4.5}/cortex/watcher.py +0 -0
  23. {cortex_local-2.4.3 → cortex_local-2.4.5}/pyproject.toml +0 -0
  24. {cortex_local-2.4.3 → cortex_local-2.4.5}/tests/test_imports.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: cortex-local
3
- Version: 2.4.3
3
+ Version: 2.4.5
4
4
  Summary: Local semantic search MCP server — notes + code, embedded Qdrant, fastembed
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.0.0
@@ -23,7 +23,14 @@ pipx install cortex-local
23
23
  cortex install
24
24
  ```
25
25
 
26
- `cortex install` downloads embedding models and registers the MCP server in Claude Code. Restart Claude Code after.
26
+ `cortex install` downloads embedding models and registers the MCP server in Claude Code with `--scope user` (available in all projects). Restart Claude Code after.
27
+
28
+ ## Update
29
+
30
+ ```bash
31
+ pipx upgrade cortex-local
32
+ cortex install # re-registers MCP (idempotent)
33
+ ```
27
34
 
28
35
  ### Legacy CPUs (no SSE4.2 / X86_V2)
29
36
 
@@ -41,6 +48,8 @@ cortex install
41
48
  ```bash
42
49
  cortex index ~/notes # index Obsidian vault
43
50
  cortex index ~/projects/myapp # index a code repo
51
+ cortex doctor # check version, MCP registration, workspace, index status
52
+ cortex uninstall # remove MCP registration
44
53
  ```
45
54
 
46
55
  Then in Claude Code, `search_notes` and `search_code` are available as MCP tools.
@@ -9,7 +9,14 @@ pipx install cortex-local
9
9
  cortex install
10
10
  ```
11
11
 
12
- `cortex install` downloads embedding models and registers the MCP server in Claude Code. Restart Claude Code after.
12
+ `cortex install` downloads embedding models and registers the MCP server in Claude Code with `--scope user` (available in all projects). Restart Claude Code after.
13
+
14
+ ## Update
15
+
16
+ ```bash
17
+ pipx upgrade cortex-local
18
+ cortex install # re-registers MCP (idempotent)
19
+ ```
13
20
 
14
21
  ### Legacy CPUs (no SSE4.2 / X86_V2)
15
22
 
@@ -27,6 +34,8 @@ cortex install
27
34
  ```bash
28
35
  cortex index ~/notes # index Obsidian vault
29
36
  cortex index ~/projects/myapp # index a code repo
37
+ cortex doctor # check version, MCP registration, workspace, index status
38
+ cortex uninstall # remove MCP registration
30
39
  ```
31
40
 
32
41
  Then in Claude Code, `search_notes` and `search_code` are available as MCP tools.
@@ -11,11 +11,12 @@ import click
11
11
 
12
12
  from .config import (
13
13
  WORKSPACES_DIR, get_active_workspace, set_active_workspace, get_workspace_dir,
14
- data_dir, save_last_indexed_path,
14
+ data_dir, save_last_indexed_path, qdrant_path, get_config, VERSION, MODELS_DIR,
15
15
  )
16
16
 
17
17
 
18
18
  @click.group()
19
+ @click.version_option(VERSION, prog_name="cortex")
19
20
  def cli() -> None:
20
21
  """cortex: local semantic search MCP server."""
21
22
 
@@ -160,25 +161,104 @@ def search_log_cmd(top: int) -> None:
160
161
  click.echo(f" {q}")
161
162
 
162
163
 
164
+ _SYSTEM_PREFIXES = [Path(p) for p in ("/usr", "/bin", "/etc", "/sbin", "/var", r"C:\Windows")]
165
+ _SCOPE_CHOICES = click.Choice(["user", "local", "project"])
166
+
167
+
168
+ def _is_system_dir(p: Path) -> bool:
169
+ r = p.resolve()
170
+ if r == Path("/"):
171
+ return True
172
+ return any(prefix.exists() and r.is_relative_to(prefix) for prefix in _SYSTEM_PREFIXES)
173
+
174
+
163
175
  @cli.command("install")
164
- def install_cmd() -> None:
176
+ @click.option("--scope", default="user", type=_SCOPE_CHOICES, show_default=True,
177
+ help="MCP registration scope.")
178
+ def install_cmd(scope: str) -> None:
165
179
  """Download embedding models and register cortex MCP in Claude Code."""
166
180
  from .embedder import pull_models as _pull
181
+ cwd = Path.cwd()
182
+ if scope != "user" and _is_system_dir(cwd):
183
+ click.echo(f"Warning: cwd looks like a system directory ({cwd}). Use --scope user to register globally.")
167
184
  click.echo("Downloading embedding models...")
168
185
  _pull()
169
- mcp_cmd = "claude mcp add cortex --transport stdio cortex serve"
186
+ mcp_cmd = f"claude mcp add --scope {scope} cortex --transport stdio cortex serve"
170
187
  claude = shutil.which("claude")
171
188
  if not claude:
172
189
  click.echo("Models ready. Register MCP manually (claude not found in PATH):")
173
190
  click.echo(f" {mcp_cmd}")
174
191
  return
175
192
  click.echo("Registering MCP server in Claude Code...")
193
+ subprocess.run(
194
+ [claude, "mcp", "remove", "cortex", "--scope", scope],
195
+ capture_output=True, text=True,
196
+ )
176
197
  result = subprocess.run(
177
- [claude, "mcp", "add", "cortex", "--transport", "stdio", "cortex", "serve"],
198
+ [claude, "mcp", "add", "--scope", scope, "cortex", "--transport", "stdio", "cortex", "serve"],
178
199
  capture_output=True, text=True,
179
200
  )
180
201
  if result.returncode == 0:
181
- click.echo("Done. cortex is ready — restart Claude Code to activate.")
202
+ click.echo(f"Done. cortex is ready (scope: {scope}) — restart Claude Code to activate.")
182
203
  else:
183
204
  click.echo(f"MCP registration failed. Run manually:\n {mcp_cmd}")
184
205
  click.echo(result.stderr.strip())
206
+
207
+
208
+ @cli.command("uninstall")
209
+ @click.option("--scope", default="user", type=_SCOPE_CHOICES, show_default=True,
210
+ help="MCP registration scope to remove from.")
211
+ def uninstall_cmd(scope: str) -> None:
212
+ """Remove cortex MCP registration from Claude Code."""
213
+ claude = shutil.which("claude")
214
+ if not claude:
215
+ click.echo(f"claude not found in PATH. Remove manually:\n claude mcp remove cortex --scope {scope}")
216
+ return
217
+ result = subprocess.run(
218
+ [claude, "mcp", "remove", "cortex", "--scope", scope],
219
+ capture_output=True, text=True,
220
+ )
221
+ if result.returncode == 0:
222
+ click.echo(f"cortex MCP removed (scope: {scope}).")
223
+ else:
224
+ click.echo(f"Removal failed (scope: {scope}).")
225
+ click.echo(result.stderr.strip())
226
+
227
+
228
+ @cli.command("doctor")
229
+ def doctor_cmd() -> None:
230
+ """Show cortex status: version, model cache, MCP registration, workspace, index."""
231
+ click.echo(f"version {VERSION}")
232
+
233
+ cache_ok = MODELS_DIR.exists() and any(MODELS_DIR.iterdir())
234
+ click.echo(f"model cache {'ok' if cache_ok else 'missing — run: cortex pull-models'}")
235
+
236
+ claude = shutil.which("claude")
237
+ mcp_status = "claude not found in PATH"
238
+ if claude:
239
+ r = subprocess.run([claude, "mcp", "get", "cortex"], capture_output=True, text=True)
240
+ if r.returncode == 0:
241
+ detail = r.stdout.strip().splitlines()[0] if r.stdout.strip() else "registered"
242
+ mcp_status = detail
243
+ else:
244
+ mcp_status = "not registered — run: cortex install"
245
+ click.echo(f"MCP {mcp_status}")
246
+
247
+ ws = get_active_workspace()
248
+ indexed = Path(qdrant_path(ws)).exists()
249
+ click.echo(f"workspace {ws} ({'indexed' if indexed else 'not indexed'})")
250
+
251
+ last = get_config()["index"].get("last_path") or "(none)"
252
+ click.echo(f"last indexed {last}")
253
+
254
+ claude_json = Path.home() / ".claude.json"
255
+ if claude_json.exists():
256
+ try:
257
+ projects = json.loads(claude_json.read_text()).get("projects", {})
258
+ stale = [p for p, v in projects.items() if "cortex" in v.get("mcpServers", {})]
259
+ if stale:
260
+ click.echo("\nStale local registrations (run from each dir to remove):")
261
+ for p in stale:
262
+ click.echo(f" cd {p} && claude mcp remove cortex --scope local")
263
+ except Exception:
264
+ pass
@@ -6,6 +6,7 @@ import tomllib
6
6
  from pathlib import Path
7
7
 
8
8
  CORTEX_DIR = Path.home() / ".cortex"
9
+ MODELS_DIR = CORTEX_DIR / "models"
9
10
  WORKSPACES_DIR = CORTEX_DIR / "workspaces"
10
11
  ACTIVE_WORKSPACE_FILE = CORTEX_DIR / "active_workspace"
11
12
  CONFIG_FILE = CORTEX_DIR / "config.toml"
@@ -5,7 +5,7 @@ import os
5
5
  os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1")
6
6
  os.environ.setdefault("HF_HUB_DISABLE_IMPLICIT_TOKEN", "1")
7
7
 
8
- from .config import EMBED_MODEL
8
+ from .config import EMBED_MODEL, MODELS_DIR
9
9
 
10
10
  _dense = None
11
11
  _sparse = None
@@ -30,7 +30,7 @@ def embed(text: str) -> list:
30
30
  global _dense
31
31
  if _dense is None:
32
32
  from fastembed import TextEmbedding
33
- _dense = TextEmbedding(EMBED_MODEL)
33
+ _dense = TextEmbedding(EMBED_MODEL, cache_dir=str(MODELS_DIR))
34
34
  return list(_dense.embed([text[:4000]]))[0].tolist()
35
35
 
36
36
 
@@ -39,7 +39,7 @@ def sparse_embed(text: str) -> tuple:
39
39
  global _sparse
40
40
  if _sparse is None:
41
41
  from fastembed import SparseTextEmbedding
42
- _sparse = SparseTextEmbedding("Qdrant/bm25")
42
+ _sparse = SparseTextEmbedding("Qdrant/bm25", cache_dir=str(MODELS_DIR))
43
43
  e = list(_sparse.embed([text[:4000]]))[0]
44
44
  return e.indices.tolist(), e.values.tolist()
45
45
 
File without changes