navcode 0.1.0__py3-none-any.whl

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.
navcode/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ from navcode._bootstrap import ensure_model
2
+ from loguru import logger
3
+ from pathlib import Path
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ ensure_model()
8
+
9
+
10
+ def _setup_logging() -> None:
11
+ log_dir = Path.cwd() / ".codenav"
12
+ if log_dir.exists():
13
+ logger.add(
14
+ log_dir / "navcode.log",
15
+ rotation="5 MB",
16
+ retention="7 days",
17
+ level="INFO",
18
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
19
+ )
20
+
21
+
22
+ _setup_logging()
navcode/_bootstrap.py ADDED
@@ -0,0 +1,122 @@
1
+ """Bootstrap utilities — ensures the ONNX embedding model is present on disk."""
2
+
3
+ from pathlib import Path
4
+
5
+ import httpx
6
+ from loguru import logger
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.progress import (
10
+ BarColumn,
11
+ DownloadColumn,
12
+ Progress,
13
+ TextColumn,
14
+ TimeRemainingColumn,
15
+ TransferSpeedColumn,
16
+ )
17
+
18
+ _HF_BASE: str = (
19
+ "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main"
20
+ )
21
+ MODEL_URL: str = f"{_HF_BASE}/onnx/model_quantized.onnx"
22
+ MODEL_PATH: Path = Path.home() / ".navcode" / "models" / "model_quantized.onnx"
23
+
24
+ # Tokenizer files required by EmbeddingsEngine
25
+ _TOKENIZER_FILES: list[str] = [
26
+ "tokenizer.json",
27
+ "tokenizer_config.json",
28
+ "vocab.txt",
29
+ "special_tokens_map.json",
30
+ ]
31
+
32
+ _console = Console()
33
+
34
+
35
+ def _download_file(url: str, dest: Path, label: str) -> None:
36
+ """Stream *url* to *dest* with a Rich progress bar.
37
+
38
+ Raises:
39
+ httpx.HTTPError: on any HTTP-level failure.
40
+ """
41
+ with httpx.stream("GET", url, follow_redirects=True, timeout=60) as response:
42
+ response.raise_for_status()
43
+ total = int(response.headers.get("content-length", 0)) or None
44
+
45
+ with Progress(
46
+ TextColumn(f"[bold blue]{label}[/bold blue]"),
47
+ BarColumn(),
48
+ DownloadColumn(),
49
+ TransferSpeedColumn(),
50
+ TimeRemainingColumn(),
51
+ console=_console,
52
+ transient=True,
53
+ ) as progress:
54
+ task = progress.add_task("download", total=total)
55
+ with dest.open("wb") as fh:
56
+ for chunk in response.iter_bytes(chunk_size=8192):
57
+ fh.write(chunk)
58
+ progress.update(task, advance=len(chunk))
59
+
60
+
61
+ def ensure_model() -> None:
62
+ """Ensure the quantised ONNX embedding model exists locally.
63
+
64
+ If the model file is already present at ``~/.navcode/models/model_quantized.onnx``
65
+ this function returns immediately. On first run it streams the file from
66
+ HuggingFace, displaying a Rich progress bar. Network failures are caught,
67
+ shown as a Rich warning panel, and silently swallowed so the rest of navcode
68
+ can still load (the model will be re-attempted on ``navcode init``).
69
+ """
70
+ if MODEL_PATH.exists():
71
+ logger.debug("ONNX model already present at {}", MODEL_PATH)
72
+ else:
73
+ MODEL_PATH.parent.mkdir(parents=True, exist_ok=True)
74
+ logger.info("ONNX model not found — downloading from HuggingFace…")
75
+
76
+ try:
77
+ _download_file(MODEL_URL, MODEL_PATH, "Downloading model")
78
+ logger.info("Model saved to {}", MODEL_PATH)
79
+ except httpx.HTTPError as exc:
80
+ _console.print(
81
+ Panel(
82
+ f"[yellow]navcode[/yellow] could not download the embedding model.\n"
83
+ f"[dim]{exc}[/dim]\n\n"
84
+ "navcode will still load — the model will be downloaded on first "
85
+ "[cyan]navcode init[/cyan].",
86
+ title="[bold yellow]⚠ Model download skipped[/bold yellow]",
87
+ border_style="yellow",
88
+ )
89
+ )
90
+ logger.warning("Model download failed: {}", exc)
91
+
92
+ # Ensure tokenizer files are present alongside the model
93
+ _ensure_tokenizer_files()
94
+
95
+
96
+ def _ensure_tokenizer_files() -> None:
97
+ """Download any missing tokenizer files from HuggingFace."""
98
+ models_dir = MODEL_PATH.parent
99
+ models_dir.mkdir(parents=True, exist_ok=True)
100
+
101
+ for filename in _TOKENIZER_FILES:
102
+ dest = models_dir / filename
103
+ if dest.exists():
104
+ logger.debug("Tokenizer file already present: {}", filename)
105
+ continue
106
+
107
+ url = f"{_HF_BASE}/{filename}"
108
+ logger.info("Tokenizer file not found — downloading: {}", filename)
109
+ try:
110
+ _download_file(url, dest, f"Downloading {filename}")
111
+ logger.info("Saved {}", dest)
112
+ except httpx.HTTPError as exc:
113
+ _console.print(
114
+ Panel(
115
+ f"[yellow]navcode[/yellow] could not download [cyan]{filename}[/cyan].\n"
116
+ f"[dim]{exc}[/dim]\n\n"
117
+ "Semantic search may be unavailable until this file is present.",
118
+ title="[bold yellow]⚠ Tokenizer file download skipped[/bold yellow]",
119
+ border_style="yellow",
120
+ )
121
+ )
122
+ logger.warning("Tokenizer file download failed ({}): {}", filename, exc)
@@ -0,0 +1,36 @@
1
+ """navcode agent adapters — one per supported AI coding agent."""
2
+
3
+ from navcode.adapters.claude import ClaudeAdapter
4
+ from navcode.adapters.cursor import CursorAdapter
5
+ from navcode.adapters.codex import CodexAdapter
6
+ from navcode.adapters.copilot import CopilotAdapter
7
+ from navcode.adapters.bob import BobAdapter
8
+ from navcode.adapters.kimi import KimiAdapter
9
+ from navcode.adapters.antigravity import AntigravityAdapter
10
+ from navcode.adapters.windsurf import WindsurfAdapter
11
+ from navcode.adapters.continue_ import ContinueAdapter
12
+
13
+ ALL_ADAPTERS = {
14
+ "claude": ClaudeAdapter,
15
+ "cursor": CursorAdapter,
16
+ "codex": CodexAdapter,
17
+ "copilot": CopilotAdapter,
18
+ "bob": BobAdapter,
19
+ "kimi": KimiAdapter,
20
+ "antigravity": AntigravityAdapter,
21
+ "windsurf": WindsurfAdapter,
22
+ "continue": ContinueAdapter,
23
+ }
24
+
25
+ __all__ = [
26
+ "ClaudeAdapter",
27
+ "CursorAdapter",
28
+ "CodexAdapter",
29
+ "CopilotAdapter",
30
+ "BobAdapter",
31
+ "KimiAdapter",
32
+ "AntigravityAdapter",
33
+ "WindsurfAdapter",
34
+ "ContinueAdapter",
35
+ "ALL_ADAPTERS",
36
+ ]
@@ -0,0 +1,31 @@
1
+ """Antigravity adapter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from navcode.adapters.base import BaseAdapter
6
+
7
+
8
+ class AntigravityAdapter(BaseAdapter):
9
+
10
+ NAME = "Antigravity"
11
+ BINARY = "antigravity"
12
+ CONFIG_DIR = "~/.antigravity"
13
+ PROJECT_DIR = ".antigravity"
14
+ PROCESS_NAME = "antigravity"
15
+ INSTRUCTION_FILE = ".antigravity/instructions.md"
16
+
17
+ def install(self) -> None:
18
+ self._write_instruction_file(
19
+ self.project_root / ".antigravity" / "instructions.md",
20
+ self._instruction_content(),
21
+ )
22
+ self._inject_mcp_config(
23
+ Path.home() / ".antigravity" / "mcp.json"
24
+ )
25
+
26
+ def is_detected(self) -> bool:
27
+ return (
28
+ self._detect_binary()
29
+ or self._detect_config_dir()
30
+ or self._detect_project_dir()
31
+ )
@@ -0,0 +1,100 @@
1
+ """Abstract base class for all navcode agent adapters."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+
6
+
7
+ class BaseAdapter(ABC):
8
+
9
+ # Override these in every adapter
10
+ NAME: str = ""
11
+ BINARY: str = ""
12
+ CONFIG_DIR: str = ""
13
+ PROJECT_DIR: str = ""
14
+ PROCESS_NAME: str | None = None
15
+ INSTRUCTION_FILE: str = ""
16
+
17
+ def __init__(self, project_root: Path):
18
+ self.project_root = project_root
19
+ self.codenav_dir = project_root / ".codenav"
20
+
21
+ @abstractmethod
22
+ def install(self) -> None:
23
+ """Create all required files for this agent."""
24
+ pass
25
+
26
+ @abstractmethod
27
+ def is_detected(self) -> bool:
28
+ """Return True if this agent is installed on the system."""
29
+ pass
30
+
31
+ def _write_instruction_file(self, path: Path, content: str) -> None:
32
+ """Write instruction file, create parent dirs if needed."""
33
+ path.parent.mkdir(parents=True, exist_ok=True)
34
+ path.write_text(content, encoding="utf-8")
35
+
36
+ def _inject_mcp_config(self, config_path: Path) -> None:
37
+ """Inject navcode MCP entry into agent JSON config."""
38
+ import json
39
+
40
+ config_path.parent.mkdir(parents=True, exist_ok=True)
41
+
42
+ existing = {}
43
+ if config_path.exists():
44
+ try:
45
+ existing = json.loads(config_path.read_text())
46
+ except json.JSONDecodeError:
47
+ pass
48
+
49
+ existing.setdefault("mcpServers", {})
50
+ existing["mcpServers"]["navcode"] = {
51
+ "command": "navcode",
52
+ "args": ["mcp-serve"],
53
+ "cwd": str(self.project_root),
54
+ }
55
+ config_path.write_text(
56
+ json.dumps(existing, indent=2),
57
+ encoding="utf-8",
58
+ )
59
+
60
+ def _detect_binary(self) -> bool:
61
+ import shutil
62
+
63
+ return shutil.which(self.BINARY) is not None
64
+
65
+ def _detect_config_dir(self) -> bool:
66
+ return Path.home().joinpath(
67
+ self.CONFIG_DIR.lstrip("~/")
68
+ ).exists()
69
+
70
+ def _detect_project_dir(self) -> bool:
71
+ return (self.project_root / self.PROJECT_DIR).exists()
72
+
73
+ def _instruction_content(self) -> str:
74
+ return f"""# navcode — Active
75
+
76
+ ## Rules for {self.NAME}
77
+
78
+ NEVER use read_file or list_directory to explore the codebase.
79
+ ALWAYS start every task with navcode_get_context(task=<your task>).
80
+ Work ONLY within files and symbols navcode returns.
81
+ If you need more: call navcode_expand_context(file_path, reason).
82
+
83
+ ## Available MCP Tools
84
+
85
+ | Tool | When to use |
86
+ |---|---|
87
+ | navcode_get_context(task) | FIRST call — always |
88
+ | navcode_expand_context(file, reason) | Need more from one file |
89
+ | navcode_get_callers(function) | Understand blast radius |
90
+ | navcode_get_deps(file) | See what a file imports |
91
+ | navcode_search(query) | Find where something is defined |
92
+ | navcode_get_structure() | Architectural overview |
93
+ | navcode_stats() | Index health check |
94
+
95
+ ## Why
96
+ navcode has pre-indexed this codebase.
97
+ Using it saves 80-90% tokens vs raw file reads.
98
+
99
+ Built by Nythris Studio
100
+ """
@@ -0,0 +1,33 @@
1
+ """IBM Bob adapter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from navcode.adapters.base import BaseAdapter
6
+
7
+
8
+ class BobAdapter(BaseAdapter):
9
+
10
+ NAME = "IBM Bob"
11
+ BINARY = "bob"
12
+ CONFIG_DIR = "~/.bob"
13
+ PROJECT_DIR = ".bob"
14
+ PROCESS_NAME = "bob-agent"
15
+ INSTRUCTION_FILE = ".bob/instructions.md"
16
+
17
+ def install(self) -> None:
18
+ # 1. Write .bob/instructions.md
19
+ self._write_instruction_file(
20
+ self.project_root / ".bob" / "instructions.md",
21
+ self._instruction_content(),
22
+ )
23
+ # 2. Inject MCP into ~/.bob/mcp.json
24
+ self._inject_mcp_config(
25
+ Path.home() / ".bob" / "mcp.json"
26
+ )
27
+
28
+ def is_detected(self) -> bool:
29
+ return (
30
+ self._detect_binary()
31
+ or self._detect_config_dir()
32
+ or self._detect_project_dir()
33
+ )
@@ -0,0 +1,33 @@
1
+ """Claude Code adapter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from navcode.adapters.base import BaseAdapter
6
+
7
+
8
+ class ClaudeAdapter(BaseAdapter):
9
+
10
+ NAME = "Claude Code"
11
+ BINARY = "claude"
12
+ CONFIG_DIR = "~/.claude"
13
+ PROJECT_DIR = "CLAUDE.md"
14
+ PROCESS_NAME = None
15
+ INSTRUCTION_FILE = "CLAUDE.md"
16
+
17
+ def install(self) -> None:
18
+ # 1. Write CLAUDE.md to project root
19
+ self._write_instruction_file(
20
+ self.project_root / "CLAUDE.md",
21
+ self._instruction_content(),
22
+ )
23
+ # 2. Inject MCP into ~/.claude/mcp.json
24
+ self._inject_mcp_config(
25
+ Path.home() / ".claude" / "mcp.json"
26
+ )
27
+
28
+ def is_detected(self) -> bool:
29
+ return (
30
+ self._detect_binary()
31
+ or self._detect_config_dir()
32
+ or self._detect_project_dir()
33
+ )
@@ -0,0 +1,33 @@
1
+ """Codex CLI adapter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from navcode.adapters.base import BaseAdapter
6
+
7
+
8
+ class CodexAdapter(BaseAdapter):
9
+
10
+ NAME = "Codex CLI"
11
+ BINARY = "codex"
12
+ CONFIG_DIR = "~/.codex"
13
+ PROJECT_DIR = "AGENTS.md"
14
+ PROCESS_NAME = None
15
+ INSTRUCTION_FILE = "AGENTS.md"
16
+
17
+ def install(self) -> None:
18
+ # 1. Write AGENTS.md
19
+ self._write_instruction_file(
20
+ self.project_root / "AGENTS.md",
21
+ self._instruction_content(),
22
+ )
23
+ # 2. Inject MCP into ~/.codex/config.json
24
+ self._inject_mcp_config(
25
+ Path.home() / ".codex" / "config.json"
26
+ )
27
+
28
+ def is_detected(self) -> bool:
29
+ return (
30
+ self._detect_binary()
31
+ or self._detect_config_dir()
32
+ or self._detect_project_dir()
33
+ )
@@ -0,0 +1,31 @@
1
+ """Continue adapter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from navcode.adapters.base import BaseAdapter
6
+
7
+
8
+ class ContinueAdapter(BaseAdapter):
9
+
10
+ NAME = "Continue"
11
+ BINARY = "continue"
12
+ CONFIG_DIR = "~/.continue"
13
+ PROJECT_DIR = ".continue"
14
+ PROCESS_NAME = None
15
+ INSTRUCTION_FILE = ".continue/navcode.md"
16
+
17
+ def install(self) -> None:
18
+ self._write_instruction_file(
19
+ self.project_root / ".continue" / "navcode.md",
20
+ self._instruction_content(),
21
+ )
22
+ self._inject_mcp_config(
23
+ Path.home() / ".continue" / "config.json"
24
+ )
25
+
26
+ def is_detected(self) -> bool:
27
+ return (
28
+ self._detect_binary()
29
+ or self._detect_config_dir()
30
+ or self._detect_project_dir()
31
+ )
@@ -0,0 +1,24 @@
1
+ """GitHub Copilot adapter."""
2
+
3
+ from navcode.adapters.base import BaseAdapter
4
+
5
+
6
+ class CopilotAdapter(BaseAdapter):
7
+
8
+ NAME = "GitHub Copilot"
9
+ BINARY = "gh"
10
+ CONFIG_DIR = "~/.config/gh"
11
+ PROJECT_DIR = ".github"
12
+ PROCESS_NAME = None
13
+ INSTRUCTION_FILE = ".github/copilot-instructions.md"
14
+
15
+ def install(self) -> None:
16
+ # Write .github/copilot-instructions.md
17
+ self._write_instruction_file(
18
+ self.project_root / ".github" / "copilot-instructions.md",
19
+ self._instruction_content(),
20
+ )
21
+ # No MCP config for Copilot yet
22
+
23
+ def is_detected(self) -> bool:
24
+ return self._detect_binary() or self._detect_project_dir()
@@ -0,0 +1,33 @@
1
+ """Cursor adapter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from navcode.adapters.base import BaseAdapter
6
+
7
+
8
+ class CursorAdapter(BaseAdapter):
9
+
10
+ NAME = "Cursor"
11
+ BINARY = "cursor"
12
+ CONFIG_DIR = "~/.cursor"
13
+ PROJECT_DIR = ".cursor"
14
+ PROCESS_NAME = "cursor"
15
+ INSTRUCTION_FILE = ".cursor/rules/navcode.md"
16
+
17
+ def install(self) -> None:
18
+ # 1. Write .cursor/rules/navcode.md
19
+ self._write_instruction_file(
20
+ self.project_root / ".cursor" / "rules" / "navcode.md",
21
+ self._instruction_content(),
22
+ )
23
+ # 2. Inject MCP into .cursor/mcp.json
24
+ self._inject_mcp_config(
25
+ self.project_root / ".cursor" / "mcp.json"
26
+ )
27
+
28
+ def is_detected(self) -> bool:
29
+ return (
30
+ self._detect_binary()
31
+ or self._detect_config_dir()
32
+ or self._detect_project_dir()
33
+ )
@@ -0,0 +1,31 @@
1
+ """Kimi Code adapter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from navcode.adapters.base import BaseAdapter
6
+
7
+
8
+ class KimiAdapter(BaseAdapter):
9
+
10
+ NAME = "Kimi Code"
11
+ BINARY = "kimi"
12
+ CONFIG_DIR = "~/.kimi"
13
+ PROJECT_DIR = "KIMI.md"
14
+ PROCESS_NAME = None
15
+ INSTRUCTION_FILE = "KIMI.md"
16
+
17
+ def install(self) -> None:
18
+ self._write_instruction_file(
19
+ self.project_root / "KIMI.md",
20
+ self._instruction_content(),
21
+ )
22
+ self._inject_mcp_config(
23
+ Path.home() / ".kimi" / "mcp.json"
24
+ )
25
+
26
+ def is_detected(self) -> bool:
27
+ return (
28
+ self._detect_binary()
29
+ or self._detect_config_dir()
30
+ or self._detect_project_dir()
31
+ )
@@ -0,0 +1,29 @@
1
+ """Windsurf adapter."""
2
+
3
+ from navcode.adapters.base import BaseAdapter
4
+
5
+
6
+ class WindsurfAdapter(BaseAdapter):
7
+
8
+ NAME = "Windsurf"
9
+ BINARY = "windsurf"
10
+ CONFIG_DIR = "~/.codeium/windsurf"
11
+ PROJECT_DIR = ".windsurf"
12
+ PROCESS_NAME = "windsurf"
13
+ INSTRUCTION_FILE = ".windsurf/rules/navcode.md"
14
+
15
+ def install(self) -> None:
16
+ self._write_instruction_file(
17
+ self.project_root / ".windsurf" / "rules" / "navcode.md",
18
+ self._instruction_content(),
19
+ )
20
+ self._inject_mcp_config(
21
+ self.project_root / ".windsurf" / "mcp.json"
22
+ )
23
+
24
+ def is_detected(self) -> bool:
25
+ return (
26
+ self._detect_binary()
27
+ or self._detect_config_dir()
28
+ or self._detect_project_dir()
29
+ )