symdex 0.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.
- symdex-0.1.0/LICENSE +21 -0
- symdex-0.1.0/PKG-INFO +134 -0
- symdex-0.1.0/README.md +89 -0
- symdex-0.1.0/pyproject.toml +67 -0
- symdex-0.1.0/setup.cfg +4 -0
- symdex-0.1.0/symdex/__init__.py +3 -0
- symdex-0.1.0/symdex/cli.py +314 -0
- symdex-0.1.0/symdex/core/__init__.py +3 -0
- symdex-0.1.0/symdex/core/indexer.py +174 -0
- symdex-0.1.0/symdex/core/parser.py +274 -0
- symdex-0.1.0/symdex/core/storage.py +248 -0
- symdex-0.1.0/symdex/graph/__init__.py +3 -0
- symdex-0.1.0/symdex/graph/call_graph.py +136 -0
- symdex-0.1.0/symdex/graph/registry.py +66 -0
- symdex-0.1.0/symdex/mcp/__init__.py +3 -0
- symdex-0.1.0/symdex/mcp/server.py +82 -0
- symdex-0.1.0/symdex/mcp/tools.py +278 -0
- symdex-0.1.0/symdex/search/__init__.py +3 -0
- symdex-0.1.0/symdex/search/semantic.py +64 -0
- symdex-0.1.0/symdex/search/symbol_search.py +28 -0
- symdex-0.1.0/symdex/search/text_search.py +34 -0
- symdex-0.1.0/symdex.egg-info/PKG-INFO +134 -0
- symdex-0.1.0/symdex.egg-info/SOURCES.txt +25 -0
- symdex-0.1.0/symdex.egg-info/dependency_links.txt +1 -0
- symdex-0.1.0/symdex.egg-info/entry_points.txt +2 -0
- symdex-0.1.0/symdex.egg-info/requires.txt +24 -0
- symdex-0.1.0/symdex.egg-info/top_level.txt +1 -0
symdex-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muhammad Husnain
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
symdex-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: symdex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal code-indexer MCP server for AI coding agents
|
|
5
|
+
Author: Muhammad Husnain
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://symdex.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/husnain/symdex
|
|
9
|
+
Keywords: mcp,code-indexer,ai,llm,tree-sitter,semantic-search
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: tree-sitter<0.26,>=0.25
|
|
22
|
+
Requires-Dist: tree-sitter-python>=0.1
|
|
23
|
+
Requires-Dist: tree-sitter-javascript>=0.1
|
|
24
|
+
Requires-Dist: tree-sitter-typescript>=0.1
|
|
25
|
+
Requires-Dist: tree-sitter-go>=0.1
|
|
26
|
+
Requires-Dist: tree-sitter-rust>=0.1
|
|
27
|
+
Requires-Dist: tree-sitter-java>=0.1
|
|
28
|
+
Requires-Dist: tree-sitter-php>=0.1
|
|
29
|
+
Requires-Dist: tree-sitter-c-sharp>=0.1
|
|
30
|
+
Requires-Dist: tree-sitter-c>=0.1
|
|
31
|
+
Requires-Dist: tree-sitter-cpp>=0.1
|
|
32
|
+
Requires-Dist: tree-sitter-elixir>=0.1
|
|
33
|
+
Requires-Dist: tree-sitter-ruby>=0.1
|
|
34
|
+
Requires-Dist: fastmcp>=2.0
|
|
35
|
+
Requires-Dist: typer>=0.12
|
|
36
|
+
Requires-Dist: rich>=13
|
|
37
|
+
Requires-Dist: sentence-transformers>=3.0
|
|
38
|
+
Requires-Dist: sqlite-vec>=0.1
|
|
39
|
+
Requires-Dist: numpy>=1.26
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-cov>=4; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-bdd>=7; extra == "dev"
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+
# SymDex
|
|
47
|
+
|
|
48
|
+
Universal code-indexer MCP server for AI coding agents — Claude, Codex, Gemini, Cursor, and any agent that speaks MCP.
|
|
49
|
+
|
|
50
|
+
**The problem:** Reading a full file to find one function costs ~7,500 tokens. SymDex pre-indexes your codebase once, then returns precise byte-offset locations. The same lookup costs ~200 tokens — a **97% reduction**.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install symdex
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
### 1. Index a project
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
symdex index ./myproject --name myproject
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Search for a symbol
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
symdex search "validate email"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Start the MCP server
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
symdex serve # stdio mode (for local agents)
|
|
76
|
+
symdex serve --port 8080 # HTTP mode
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## MCP Tool Reference
|
|
80
|
+
|
|
81
|
+
| Tool | Description |
|
|
82
|
+
|------|-------------|
|
|
83
|
+
| `index_folder` | Index a local folder (run once per session) |
|
|
84
|
+
| `search_symbols` | Find function/class by name (~200 tokens) |
|
|
85
|
+
| `get_symbol` | Get one function's full source by byte offset |
|
|
86
|
+
| `get_file_outline` | All symbols in a file, no full content |
|
|
87
|
+
| `get_repo_outline` | Directory structure + symbol stats |
|
|
88
|
+
| `search_text` | Text search, returns matching lines only |
|
|
89
|
+
| `get_file_tree` | Directory tree without content |
|
|
90
|
+
| `list_repos` | List all indexed repos |
|
|
91
|
+
| `get_symbols` | Bulk symbol retrieval |
|
|
92
|
+
| `index_repo` | Index a named repo |
|
|
93
|
+
| `invalidate_cache` | Force re-index on next request |
|
|
94
|
+
| `semantic_search` | Find symbols by meaning (embedding similarity) |
|
|
95
|
+
| `get_callers` | Who calls this function |
|
|
96
|
+
| `get_callees` | What this function calls |
|
|
97
|
+
|
|
98
|
+
## CLI Commands
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
symdex index ./myproject # Index a folder
|
|
102
|
+
symdex search "validate email" # Search symbols
|
|
103
|
+
symdex find MyClass # Exact name lookup
|
|
104
|
+
symdex outline myproject/module.py --repo myproject # File outline
|
|
105
|
+
symdex text "TODO" --repo myproject # Text search
|
|
106
|
+
symdex semantic "parse authentication token" --repo myproject # Semantic search
|
|
107
|
+
symdex callers my_function --repo myproject # Call graph: who calls this
|
|
108
|
+
symdex callees my_function --repo myproject # Call graph: what this calls
|
|
109
|
+
symdex repos # List indexed repos
|
|
110
|
+
symdex serve # Start MCP server
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Supported Languages
|
|
114
|
+
|
|
115
|
+
Python, JavaScript, TypeScript, Go, Rust, Java, PHP, C#, C, C++, Elixir, Ruby (13 languages via tree-sitter)
|
|
116
|
+
|
|
117
|
+
## Agent Configuration
|
|
118
|
+
|
|
119
|
+
Add to your agent's MCP config:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"mcpServers": {
|
|
124
|
+
"symdex": {
|
|
125
|
+
"command": "symdex",
|
|
126
|
+
"args": ["serve"]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT — see [LICENSE](LICENSE)
|
symdex-0.1.0/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# SymDex
|
|
2
|
+
|
|
3
|
+
Universal code-indexer MCP server for AI coding agents — Claude, Codex, Gemini, Cursor, and any agent that speaks MCP.
|
|
4
|
+
|
|
5
|
+
**The problem:** Reading a full file to find one function costs ~7,500 tokens. SymDex pre-indexes your codebase once, then returns precise byte-offset locations. The same lookup costs ~200 tokens — a **97% reduction**.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install symdex
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
### 1. Index a project
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
symdex index ./myproject --name myproject
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Search for a symbol
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
symdex search "validate email"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 3. Start the MCP server
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
symdex serve # stdio mode (for local agents)
|
|
31
|
+
symdex serve --port 8080 # HTTP mode
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## MCP Tool Reference
|
|
35
|
+
|
|
36
|
+
| Tool | Description |
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| `index_folder` | Index a local folder (run once per session) |
|
|
39
|
+
| `search_symbols` | Find function/class by name (~200 tokens) |
|
|
40
|
+
| `get_symbol` | Get one function's full source by byte offset |
|
|
41
|
+
| `get_file_outline` | All symbols in a file, no full content |
|
|
42
|
+
| `get_repo_outline` | Directory structure + symbol stats |
|
|
43
|
+
| `search_text` | Text search, returns matching lines only |
|
|
44
|
+
| `get_file_tree` | Directory tree without content |
|
|
45
|
+
| `list_repos` | List all indexed repos |
|
|
46
|
+
| `get_symbols` | Bulk symbol retrieval |
|
|
47
|
+
| `index_repo` | Index a named repo |
|
|
48
|
+
| `invalidate_cache` | Force re-index on next request |
|
|
49
|
+
| `semantic_search` | Find symbols by meaning (embedding similarity) |
|
|
50
|
+
| `get_callers` | Who calls this function |
|
|
51
|
+
| `get_callees` | What this function calls |
|
|
52
|
+
|
|
53
|
+
## CLI Commands
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
symdex index ./myproject # Index a folder
|
|
57
|
+
symdex search "validate email" # Search symbols
|
|
58
|
+
symdex find MyClass # Exact name lookup
|
|
59
|
+
symdex outline myproject/module.py --repo myproject # File outline
|
|
60
|
+
symdex text "TODO" --repo myproject # Text search
|
|
61
|
+
symdex semantic "parse authentication token" --repo myproject # Semantic search
|
|
62
|
+
symdex callers my_function --repo myproject # Call graph: who calls this
|
|
63
|
+
symdex callees my_function --repo myproject # Call graph: what this calls
|
|
64
|
+
symdex repos # List indexed repos
|
|
65
|
+
symdex serve # Start MCP server
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Supported Languages
|
|
69
|
+
|
|
70
|
+
Python, JavaScript, TypeScript, Go, Rust, Java, PHP, C#, C, C++, Elixir, Ruby (13 languages via tree-sitter)
|
|
71
|
+
|
|
72
|
+
## Agent Configuration
|
|
73
|
+
|
|
74
|
+
Add to your agent's MCP config:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"symdex": {
|
|
80
|
+
"command": "symdex",
|
|
81
|
+
"args": ["serve"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "symdex"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Universal code-indexer MCP server for AI coding agents"
|
|
9
|
+
authors = [{name = "Muhammad Husnain"}]
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
keywords = ["mcp", "code-indexer", "ai", "llm", "tree-sitter", "semantic-search"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"tree-sitter>=0.25,<0.26",
|
|
26
|
+
"tree-sitter-python>=0.1",
|
|
27
|
+
"tree-sitter-javascript>=0.1",
|
|
28
|
+
"tree-sitter-typescript>=0.1",
|
|
29
|
+
"tree-sitter-go>=0.1",
|
|
30
|
+
"tree-sitter-rust>=0.1",
|
|
31
|
+
"tree-sitter-java>=0.1",
|
|
32
|
+
"tree-sitter-php>=0.1",
|
|
33
|
+
# tree-sitter-dart: not available on PyPI, excluded
|
|
34
|
+
"tree-sitter-c-sharp>=0.1",
|
|
35
|
+
"tree-sitter-c>=0.1",
|
|
36
|
+
"tree-sitter-cpp>=0.1",
|
|
37
|
+
"tree-sitter-elixir>=0.1",
|
|
38
|
+
"tree-sitter-ruby>=0.1",
|
|
39
|
+
"fastmcp>=2.0",
|
|
40
|
+
"typer>=0.12",
|
|
41
|
+
"rich>=13",
|
|
42
|
+
"sentence-transformers>=3.0",
|
|
43
|
+
"sqlite-vec>=0.1",
|
|
44
|
+
"numpy>=1.26",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://symdex.dev"
|
|
49
|
+
Repository = "https://github.com/husnain/symdex"
|
|
50
|
+
|
|
51
|
+
[project.optional-dependencies]
|
|
52
|
+
dev = [
|
|
53
|
+
"pytest>=7",
|
|
54
|
+
"pytest-cov>=4",
|
|
55
|
+
"pytest-bdd>=7",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[project.scripts]
|
|
59
|
+
symdex = "symdex.cli:app"
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.packages.find]
|
|
62
|
+
where = ["."]
|
|
63
|
+
include = ["symdex*"]
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
testpaths = ["tests"]
|
|
67
|
+
python_files = ["test_*.py", "*_test.py", "*_steps.py"]
|
symdex-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Copyright (c) 2026 Muhammad Husnain
|
|
2
|
+
# This file is part of SymDex.
|
|
3
|
+
# License: See LICENSE file in the project root.
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from symdex.core.indexer import index_folder as _index_folder, invalidate as _invalidate
|
|
13
|
+
from symdex.core.storage import (
|
|
14
|
+
get_connection,
|
|
15
|
+
get_db_path,
|
|
16
|
+
get_registry_path, # noqa: F401 — imported for monkeypatching
|
|
17
|
+
query_file_symbols,
|
|
18
|
+
query_repos,
|
|
19
|
+
search_text_in_index,
|
|
20
|
+
upsert_repo,
|
|
21
|
+
)
|
|
22
|
+
from symdex.search.symbol_search import search_symbols as _search_symbols
|
|
23
|
+
|
|
24
|
+
app = typer.Typer(name="symdex", help="SymDex — universal code indexer")
|
|
25
|
+
console = Console()
|
|
26
|
+
err_console = Console(stderr=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def index(
|
|
31
|
+
path: str = typer.Argument(..., help="Directory to index"),
|
|
32
|
+
name: str = typer.Option(None, "--name", "-n", help="Repo name (defaults to folder name)"),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Index a folder and register it."""
|
|
35
|
+
if not os.path.isdir(path):
|
|
36
|
+
err_console.print(f"[red]Error:[/red] Path does not exist: {path}")
|
|
37
|
+
raise typer.Exit(code=1)
|
|
38
|
+
result = _index_folder(path, name=name)
|
|
39
|
+
upsert_repo(result.repo, root_path=os.path.abspath(path), db_path=result.db_path)
|
|
40
|
+
table = Table(title="Index Result")
|
|
41
|
+
table.add_column("Repo", style="cyan")
|
|
42
|
+
table.add_column("Indexed", style="green")
|
|
43
|
+
table.add_column("Skipped", style="yellow")
|
|
44
|
+
table.add_column("DB Path")
|
|
45
|
+
table.add_row(result.repo, str(result.indexed_count), str(result.skipped_count), result.db_path)
|
|
46
|
+
console.print(table)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command()
|
|
50
|
+
def search(
|
|
51
|
+
query: str = typer.Argument(..., help="Symbol name to search for"),
|
|
52
|
+
repo: str = typer.Option(None, "--repo", "-r", help="Repo name (omit to search all repos)"),
|
|
53
|
+
kind: str = typer.Option(None, "--kind", "-k", help="Symbol kind filter"),
|
|
54
|
+
limit: int = typer.Option(20, "--limit", "-l", help="Max results"),
|
|
55
|
+
json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Find functions/classes by name (omit --repo to search all indexed repos)."""
|
|
58
|
+
if repo:
|
|
59
|
+
conn = get_connection(get_db_path(repo))
|
|
60
|
+
try:
|
|
61
|
+
symbols = _search_symbols(conn, repo=repo, query=query, kind=kind, limit=limit)
|
|
62
|
+
finally:
|
|
63
|
+
conn.close()
|
|
64
|
+
else:
|
|
65
|
+
from symdex.graph.registry import search_across_repos
|
|
66
|
+
symbols = search_across_repos(query=query, kind=kind, limit=limit)
|
|
67
|
+
if not symbols:
|
|
68
|
+
err_console.print(f"[red]Error:[/red] No symbols found matching: {query}")
|
|
69
|
+
raise typer.Exit(code=1)
|
|
70
|
+
if json_output:
|
|
71
|
+
typer.echo(json.dumps({"symbols": symbols}))
|
|
72
|
+
return
|
|
73
|
+
table = Table(title=f"Symbols matching '{query}'")
|
|
74
|
+
table.add_column("Repo", style="blue")
|
|
75
|
+
table.add_column("Name", style="cyan")
|
|
76
|
+
table.add_column("Kind", style="magenta")
|
|
77
|
+
table.add_column("File")
|
|
78
|
+
table.add_column("Start", style="dim")
|
|
79
|
+
for s in symbols:
|
|
80
|
+
table.add_row(s.get("repo", repo or ""), s["name"], s["kind"], s["file"], str(s["start_byte"]))
|
|
81
|
+
console.print(table)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command()
|
|
85
|
+
def find(
|
|
86
|
+
name: str = typer.Argument(..., help="Exact symbol name"),
|
|
87
|
+
repo: str = typer.Option(None, "--repo", "-r", help="Repo name"),
|
|
88
|
+
json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Exact symbol name lookup by symbol name."""
|
|
91
|
+
if not repo:
|
|
92
|
+
err_console.print("[red]Error:[/red] --repo is required")
|
|
93
|
+
raise typer.Exit(code=1)
|
|
94
|
+
conn = get_connection(get_db_path(repo))
|
|
95
|
+
try:
|
|
96
|
+
symbols = _search_symbols(conn, repo=repo, query=name, limit=1)
|
|
97
|
+
finally:
|
|
98
|
+
conn.close()
|
|
99
|
+
if not symbols:
|
|
100
|
+
err_console.print(f"[red]Error:[/red] Symbol not found: {name}")
|
|
101
|
+
raise typer.Exit(code=1)
|
|
102
|
+
if json_output:
|
|
103
|
+
typer.echo(json.dumps({"symbols": symbols}))
|
|
104
|
+
return
|
|
105
|
+
s = symbols[0]
|
|
106
|
+
table = Table(title=f"Symbol: {name}")
|
|
107
|
+
table.add_column("Field")
|
|
108
|
+
table.add_column("Value")
|
|
109
|
+
for k, v in s.items():
|
|
110
|
+
table.add_row(k, str(v) if v is not None else "")
|
|
111
|
+
console.print(table)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command()
|
|
115
|
+
def outline(
|
|
116
|
+
file: str = typer.Argument(..., help="Relative file path within repo"),
|
|
117
|
+
repo: str = typer.Option(..., "--repo", "-r", help="Repo name"),
|
|
118
|
+
json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
119
|
+
) -> None:
|
|
120
|
+
"""List all symbols in a file."""
|
|
121
|
+
conn = get_connection(get_db_path(repo))
|
|
122
|
+
try:
|
|
123
|
+
symbols = query_file_symbols(conn, repo=repo, file=file)
|
|
124
|
+
finally:
|
|
125
|
+
conn.close()
|
|
126
|
+
if not symbols:
|
|
127
|
+
err_console.print(f"[red]Error:[/red] No symbols found in: {file}")
|
|
128
|
+
raise typer.Exit(code=1)
|
|
129
|
+
if json_output:
|
|
130
|
+
typer.echo(json.dumps({"symbols": symbols}))
|
|
131
|
+
return
|
|
132
|
+
table = Table(title=f"Outline: {file}")
|
|
133
|
+
table.add_column("Name", style="cyan")
|
|
134
|
+
table.add_column("Kind", style="magenta")
|
|
135
|
+
table.add_column("Start", style="dim")
|
|
136
|
+
table.add_column("End", style="dim")
|
|
137
|
+
for s in symbols:
|
|
138
|
+
table.add_row(s["name"], s["kind"], str(s["start_byte"]), str(s["end_byte"]))
|
|
139
|
+
console.print(table)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command()
|
|
143
|
+
def text(
|
|
144
|
+
query: str = typer.Argument(..., help="Text to search for"),
|
|
145
|
+
repo: str = typer.Option(None, "--repo", "-r", help="Repo name"),
|
|
146
|
+
pattern: str = typer.Option(None, "--pattern", "-p", help="File glob pattern"),
|
|
147
|
+
json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Text search across indexed files."""
|
|
150
|
+
if not repo:
|
|
151
|
+
err_console.print("[red]Error:[/red] --repo is required")
|
|
152
|
+
raise typer.Exit(code=1)
|
|
153
|
+
all_repos = query_repos()
|
|
154
|
+
repo_info = next((r for r in all_repos if r["name"] == repo), None)
|
|
155
|
+
if repo_info is None:
|
|
156
|
+
err_console.print(f"[red]Error:[/red] Repo not indexed: {repo}")
|
|
157
|
+
raise typer.Exit(code=1)
|
|
158
|
+
conn = get_connection(get_db_path(repo))
|
|
159
|
+
try:
|
|
160
|
+
matches = search_text_in_index(conn, repo=repo, query=query, repo_root=repo_info["root_path"], file_pattern=pattern)
|
|
161
|
+
finally:
|
|
162
|
+
conn.close()
|
|
163
|
+
if not matches:
|
|
164
|
+
err_console.print(f"[red]Error:[/red] No matches found for: {query}")
|
|
165
|
+
raise typer.Exit(code=1)
|
|
166
|
+
if json_output:
|
|
167
|
+
typer.echo(json.dumps({"matches": matches}))
|
|
168
|
+
return
|
|
169
|
+
table = Table(title=f"Text matches for '{query}'")
|
|
170
|
+
table.add_column("File")
|
|
171
|
+
table.add_column("Line", style="dim")
|
|
172
|
+
table.add_column("Text")
|
|
173
|
+
for m in matches:
|
|
174
|
+
table.add_row(m["file"], str(m["line"]), m["text"])
|
|
175
|
+
console.print(table)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.command()
|
|
179
|
+
def semantic(
|
|
180
|
+
query: str = typer.Argument(..., help="Natural language query"),
|
|
181
|
+
repo: str = typer.Option(None, "--repo", "-r", help="Repo name"),
|
|
182
|
+
limit: int = typer.Option(10, "--limit", "-l", help="Max results"),
|
|
183
|
+
json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Semantic similarity search by meaning."""
|
|
186
|
+
from symdex.search.semantic import search_semantic
|
|
187
|
+
if not repo:
|
|
188
|
+
err_console.print("[red]Error:[/red] --repo is required")
|
|
189
|
+
raise typer.Exit(code=1)
|
|
190
|
+
conn = get_connection(get_db_path(repo))
|
|
191
|
+
try:
|
|
192
|
+
results = search_semantic(conn, query=query, repo=repo, limit=limit)
|
|
193
|
+
finally:
|
|
194
|
+
conn.close()
|
|
195
|
+
if not results:
|
|
196
|
+
err_console.print(f"[red]Error:[/red] No semantic matches found for: {query}")
|
|
197
|
+
raise typer.Exit(code=1)
|
|
198
|
+
if json_output:
|
|
199
|
+
typer.echo(json.dumps({"symbols": results}))
|
|
200
|
+
return
|
|
201
|
+
table = Table(title=f"Semantic matches for '{query}'")
|
|
202
|
+
table.add_column("Name", style="cyan")
|
|
203
|
+
table.add_column("Kind", style="magenta")
|
|
204
|
+
table.add_column("Score", style="green")
|
|
205
|
+
table.add_column("File")
|
|
206
|
+
for s in results:
|
|
207
|
+
table.add_row(s["name"], s["kind"], str(s["score"]), s["file"])
|
|
208
|
+
console.print(table)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.command()
|
|
212
|
+
def callers(
|
|
213
|
+
name: str = typer.Argument(..., help="Function name to find callers of"),
|
|
214
|
+
repo: str = typer.Option(..., "--repo", "-r", help="Repo name"),
|
|
215
|
+
json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Show all functions that call the named function."""
|
|
218
|
+
from symdex.graph.call_graph import get_callers as _get_callers
|
|
219
|
+
conn = get_connection(get_db_path(repo))
|
|
220
|
+
try:
|
|
221
|
+
results = _get_callers(conn, name=name, repo=repo)
|
|
222
|
+
finally:
|
|
223
|
+
conn.close()
|
|
224
|
+
if not results:
|
|
225
|
+
err_console.print(f"[red]Error:[/red] No callers found for: {name}")
|
|
226
|
+
raise typer.Exit(code=1)
|
|
227
|
+
if json_output:
|
|
228
|
+
typer.echo(json.dumps({"callers": results}))
|
|
229
|
+
return
|
|
230
|
+
table = Table(title=f"Callers of '{name}'")
|
|
231
|
+
table.add_column("Name", style="cyan")
|
|
232
|
+
table.add_column("Kind", style="magenta")
|
|
233
|
+
table.add_column("File")
|
|
234
|
+
for s in results:
|
|
235
|
+
table.add_row(s["name"], s["kind"], s["file"])
|
|
236
|
+
console.print(table)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@app.command()
|
|
240
|
+
def callees(
|
|
241
|
+
name: str = typer.Argument(..., help="Function name to find callees of"),
|
|
242
|
+
repo: str = typer.Option(..., "--repo", "-r", help="Repo name"),
|
|
243
|
+
json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Show all functions called by the named function."""
|
|
246
|
+
from symdex.graph.call_graph import get_callees as _get_callees
|
|
247
|
+
conn = get_connection(get_db_path(repo))
|
|
248
|
+
try:
|
|
249
|
+
results = _get_callees(conn, name=name, repo=repo)
|
|
250
|
+
finally:
|
|
251
|
+
conn.close()
|
|
252
|
+
if not results:
|
|
253
|
+
err_console.print(f"[red]Error:[/red] No callees found for: {name}")
|
|
254
|
+
raise typer.Exit(code=1)
|
|
255
|
+
if json_output:
|
|
256
|
+
typer.echo(json.dumps({"callees": results}))
|
|
257
|
+
return
|
|
258
|
+
table = Table(title=f"Callees of '{name}'")
|
|
259
|
+
table.add_column("Name", style="cyan")
|
|
260
|
+
table.add_column("File")
|
|
261
|
+
for s in results:
|
|
262
|
+
table.add_row(s["name"], s.get("file") or "")
|
|
263
|
+
console.print(table)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@app.command()
|
|
267
|
+
def repos(
|
|
268
|
+
json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
269
|
+
) -> None:
|
|
270
|
+
"""List all indexed repositories."""
|
|
271
|
+
all_repos = query_repos()
|
|
272
|
+
if not all_repos:
|
|
273
|
+
err_console.print("[red]Error:[/red] No repos indexed yet.")
|
|
274
|
+
raise typer.Exit(code=1)
|
|
275
|
+
if json_output:
|
|
276
|
+
typer.echo(json.dumps({"repos": all_repos}))
|
|
277
|
+
return
|
|
278
|
+
table = Table(title="Indexed Repositories")
|
|
279
|
+
table.add_column("Name", style="cyan")
|
|
280
|
+
table.add_column("Root Path")
|
|
281
|
+
table.add_column("Last Indexed", style="dim")
|
|
282
|
+
for r in all_repos:
|
|
283
|
+
table.add_row(r["name"], r["root_path"], str(r.get("last_indexed", "")))
|
|
284
|
+
console.print(table)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@app.command()
|
|
288
|
+
def invalidate(
|
|
289
|
+
repo: str = typer.Option(..., "--repo", "-r", help="Repo name"),
|
|
290
|
+
file: str = typer.Option(None, "--file", "-f", help="Specific file to invalidate"),
|
|
291
|
+
json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Force re-index of a repo or specific file."""
|
|
294
|
+
count = _invalidate(repo, file=file)
|
|
295
|
+
if json_output:
|
|
296
|
+
typer.echo(json.dumps({"invalidated": count}))
|
|
297
|
+
return
|
|
298
|
+
console.print(f"Invalidated [green]{count}[/green] record(s) for repo '[cyan]{repo}[/cyan]'")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@app.command()
|
|
302
|
+
def serve(
|
|
303
|
+
port: int = typer.Option(None, "--port", "-p", help="HTTP port (omit for stdio mode)"),
|
|
304
|
+
) -> None:
|
|
305
|
+
"""Start the MCP server."""
|
|
306
|
+
from symdex.mcp.server import mcp
|
|
307
|
+
if port:
|
|
308
|
+
mcp.run(transport="streamable-http", port=port)
|
|
309
|
+
else:
|
|
310
|
+
mcp.run()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if __name__ == "__main__":
|
|
314
|
+
app()
|