athena-code 0.0.14__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.
Potentially problematic release.
This version of athena-code might be problematic. Click here for more details.
- athena/README.md +132 -0
- athena/__init__.py +8 -0
- athena/__main__.py +5 -0
- athena/cli.py +347 -0
- athena/docstring_updater.py +133 -0
- athena/entity_path.py +146 -0
- athena/hashing.py +156 -0
- athena/info.py +84 -0
- athena/locate.py +52 -0
- athena/mcp_config.py +103 -0
- athena/mcp_server.py +215 -0
- athena/models.py +90 -0
- athena/parsers/__init__.py +22 -0
- athena/parsers/base.py +39 -0
- athena/parsers/python_parser.py +633 -0
- athena/repository.py +75 -0
- athena/status.py +88 -0
- athena/sync.py +577 -0
- athena_code-0.0.14.dist-info/METADATA +152 -0
- athena_code-0.0.14.dist-info/RECORD +24 -0
- athena_code-0.0.14.dist-info/WHEEL +5 -0
- athena_code-0.0.14.dist-info/entry_points.txt +3 -0
- athena_code-0.0.14.dist-info/licenses/LICENSE +21 -0
- athena_code-0.0.14.dist-info/top_level.txt +1 -0
athena/mcp_server.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""MCP server that exposes Athena Code Knowledge tools to Claude Code.
|
|
2
|
+
|
|
3
|
+
This server wraps the `ack` CLI tool, providing structured access to code
|
|
4
|
+
navigation capabilities through the Model Context Protocol.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from mcp.server import Server
|
|
12
|
+
from mcp.server.stdio import stdio_server
|
|
13
|
+
from mcp.types import Tool, TextContent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Initialize MCP server
|
|
17
|
+
app = Server("ack")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.list_tools()
|
|
21
|
+
async def list_tools() -> list[Tool]:
|
|
22
|
+
"""Declare available tools for Claude Code."""
|
|
23
|
+
return [
|
|
24
|
+
Tool(
|
|
25
|
+
name="ack_locate",
|
|
26
|
+
description=(
|
|
27
|
+
"Find the location of a Python entity (function, class, or method). "
|
|
28
|
+
"Returns file path and line range. Currently supports Python files only. "
|
|
29
|
+
"Use this to locate code before reading files - knowing the exact line "
|
|
30
|
+
"range allows targeted code extraction with tools like sed."
|
|
31
|
+
),
|
|
32
|
+
inputSchema={
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"entity": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Name of the entity to locate (e.g., 'validateSession', 'UserModel')",
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"required": ["entity"],
|
|
41
|
+
},
|
|
42
|
+
),
|
|
43
|
+
Tool(
|
|
44
|
+
name="ack_info",
|
|
45
|
+
description=(
|
|
46
|
+
"Get detailed information about a code entity including signature, "
|
|
47
|
+
"parameters, return type, docstring, and dependencies. Supports functions, "
|
|
48
|
+
"classes, methods, modules, and packages. Returns structured JSON with all "
|
|
49
|
+
"available metadata about the entity."
|
|
50
|
+
),
|
|
51
|
+
inputSchema={
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"location": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": (
|
|
57
|
+
"Path to entity in format 'file_path:entity_name' for functions/classes/methods, "
|
|
58
|
+
"'file_path' for module-level info, or 'directory_path' for package info. "
|
|
59
|
+
"Examples: 'src/auth.py:validate_token', 'src/auth.py', 'src/models'"
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"required": ["location"],
|
|
64
|
+
},
|
|
65
|
+
),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.call_tool()
|
|
70
|
+
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
|
|
71
|
+
"""Handle tool calls by routing to appropriate CLI commands."""
|
|
72
|
+
if name == "ack_locate":
|
|
73
|
+
return await _handle_locate(arguments["entity"])
|
|
74
|
+
elif name == "ack_info":
|
|
75
|
+
return await _handle_info(arguments["location"])
|
|
76
|
+
|
|
77
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def _handle_locate(entity: str) -> list[TextContent]:
|
|
81
|
+
"""Handle ack_locate tool calls.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
entity: Name of the entity to locate
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List containing a single TextContent with JSON results
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
# Call the CLI tool
|
|
91
|
+
result = subprocess.run(
|
|
92
|
+
["ack", "locate", entity],
|
|
93
|
+
capture_output=True,
|
|
94
|
+
text=True,
|
|
95
|
+
check=True,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Parse JSON output from CLI
|
|
99
|
+
locations = json.loads(result.stdout)
|
|
100
|
+
|
|
101
|
+
if not locations:
|
|
102
|
+
return [
|
|
103
|
+
TextContent(
|
|
104
|
+
type="text",
|
|
105
|
+
text=f"No entities found with name '{entity}'",
|
|
106
|
+
)
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
# Format results for Claude Code
|
|
110
|
+
formatted_results = []
|
|
111
|
+
for loc in locations:
|
|
112
|
+
kind = loc["kind"]
|
|
113
|
+
path = loc["path"]
|
|
114
|
+
start = loc["extent"]["start"]
|
|
115
|
+
end = loc["extent"]["end"]
|
|
116
|
+
formatted_results.append(
|
|
117
|
+
f"{kind} '{entity}' found in {path} (lines {start}-{end})"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
TextContent(
|
|
122
|
+
type="text",
|
|
123
|
+
text="\n".join(formatted_results),
|
|
124
|
+
)
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
except subprocess.CalledProcessError as e:
|
|
128
|
+
error_msg = e.stderr.strip() if e.stderr else str(e)
|
|
129
|
+
return [
|
|
130
|
+
TextContent(
|
|
131
|
+
type="text",
|
|
132
|
+
text=f"Error running ack locate: {error_msg}",
|
|
133
|
+
)
|
|
134
|
+
]
|
|
135
|
+
except json.JSONDecodeError as e:
|
|
136
|
+
return [
|
|
137
|
+
TextContent(
|
|
138
|
+
type="text",
|
|
139
|
+
text=f"Error parsing ack output: {e}",
|
|
140
|
+
)
|
|
141
|
+
]
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return [
|
|
144
|
+
TextContent(
|
|
145
|
+
type="text",
|
|
146
|
+
text=f"Unexpected error: {e}",
|
|
147
|
+
)
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def _handle_info(location: str) -> list[TextContent]:
|
|
152
|
+
"""Handle ack_info tool calls.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
location: Path to entity in format "file_path:entity_name",
|
|
156
|
+
"file_path" for module-level info,
|
|
157
|
+
or "directory_path" for package info
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List containing a single TextContent with JSON results
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
# Call the CLI tool
|
|
164
|
+
result = subprocess.run(
|
|
165
|
+
["ack", "info", location],
|
|
166
|
+
capture_output=True,
|
|
167
|
+
text=True,
|
|
168
|
+
check=True,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Parse JSON output from CLI
|
|
172
|
+
entity_info = json.loads(result.stdout)
|
|
173
|
+
|
|
174
|
+
# Return formatted JSON
|
|
175
|
+
return [
|
|
176
|
+
TextContent(
|
|
177
|
+
type="text",
|
|
178
|
+
text=json.dumps(entity_info, indent=2),
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
except subprocess.CalledProcessError as e:
|
|
183
|
+
error_msg = e.stderr.strip() if e.stderr else str(e)
|
|
184
|
+
return [
|
|
185
|
+
TextContent(
|
|
186
|
+
type="text",
|
|
187
|
+
text=f"Error running ack info: {error_msg}",
|
|
188
|
+
)
|
|
189
|
+
]
|
|
190
|
+
except json.JSONDecodeError as e:
|
|
191
|
+
return [
|
|
192
|
+
TextContent(
|
|
193
|
+
type="text",
|
|
194
|
+
text=f"Error parsing ack output: {e}",
|
|
195
|
+
)
|
|
196
|
+
]
|
|
197
|
+
except Exception as e:
|
|
198
|
+
return [
|
|
199
|
+
TextContent(
|
|
200
|
+
type="text",
|
|
201
|
+
text=f"Unexpected error: {e}",
|
|
202
|
+
)
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def main():
|
|
207
|
+
"""Run the MCP server."""
|
|
208
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
209
|
+
await app.run(read_stream, write_stream, app.create_initialization_options())
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
import asyncio
|
|
214
|
+
|
|
215
|
+
asyncio.run(main())
|
athena/models.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Location:
|
|
6
|
+
"""Represents a line range in a source file (0-indexed, inclusive)."""
|
|
7
|
+
start: int
|
|
8
|
+
end: int
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Entity:
|
|
13
|
+
"""Represents a code entity (function, class, or method) found in a file."""
|
|
14
|
+
kind: str
|
|
15
|
+
path: str
|
|
16
|
+
extent: Location
|
|
17
|
+
name: str = "" # Entity name (for filtering, not included in JSON output)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Parameter:
|
|
22
|
+
"""Represents a function/method parameter."""
|
|
23
|
+
name: str
|
|
24
|
+
type: str | None = None # None if no type hint
|
|
25
|
+
default: str | None = None # None if no default value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Signature:
|
|
30
|
+
"""Represents a function/method signature."""
|
|
31
|
+
name: str
|
|
32
|
+
args: list[Parameter]
|
|
33
|
+
return_type: str | None = None # None if no return annotation
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class FunctionInfo:
|
|
38
|
+
"""Information about a function."""
|
|
39
|
+
path: str
|
|
40
|
+
extent: Location
|
|
41
|
+
sig: Signature
|
|
42
|
+
summary: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ClassInfo:
|
|
47
|
+
"""Information about a class."""
|
|
48
|
+
path: str
|
|
49
|
+
extent: Location
|
|
50
|
+
methods: list[str] # Formatted method signatures
|
|
51
|
+
summary: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class MethodInfo:
|
|
56
|
+
"""Information about a method."""
|
|
57
|
+
name: str # Qualified name: "ClassName.method_name"
|
|
58
|
+
path: str
|
|
59
|
+
extent: Location
|
|
60
|
+
sig: Signature
|
|
61
|
+
summary: str | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ModuleInfo:
|
|
66
|
+
"""Information about a module."""
|
|
67
|
+
path: str
|
|
68
|
+
extent: Location
|
|
69
|
+
summary: str | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class PackageInfo:
|
|
74
|
+
"""Information about a package (directory with __init__.py)."""
|
|
75
|
+
path: str
|
|
76
|
+
summary: str | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class EntityStatus:
|
|
81
|
+
"""Status information for an entity's hash synchronization state."""
|
|
82
|
+
kind: str
|
|
83
|
+
path: str
|
|
84
|
+
extent: str # Format: "start-end" or empty for packages/modules
|
|
85
|
+
recorded_hash: str | None # Hash from docstring, None if no hash
|
|
86
|
+
calculated_hash: str # Hash computed from AST
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Union type for entity info
|
|
90
|
+
EntityInfo = FunctionInfo | ClassInfo | MethodInfo | ModuleInfo | PackageInfo
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Parser module for extracting entities from source code"""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from athena.parsers.base import BaseParser
|
|
5
|
+
from athena.parsers.python_parser import PythonParser
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_parser_for_file(file_path: Path) -> BaseParser | None:
|
|
9
|
+
"""Get the appropriate parser for a file based on its extension.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
file_path: Path to the source file
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Parser instance if the file type is supported, None otherwise
|
|
16
|
+
"""
|
|
17
|
+
extension = file_path.suffix.lower()
|
|
18
|
+
|
|
19
|
+
if extension == ".py":
|
|
20
|
+
return PythonParser()
|
|
21
|
+
|
|
22
|
+
return None
|
athena/parsers/base.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from athena.models import ClassInfo, Entity, FunctionInfo, MethodInfo, ModuleInfo
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseParser(ABC):
|
|
7
|
+
"""Abstract base class for language-specific entity parsers."""
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def extract_entities(self, source_code: str, file_path: str) -> list[Entity]:
|
|
11
|
+
"""Extract all entities (functions, classes, methods) from source code.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
source_code: The source code to parse
|
|
15
|
+
file_path: Relative path to the file (for Entity.path)
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
List of Entity objects found in the source code
|
|
19
|
+
"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def extract_entity_info(
|
|
24
|
+
self,
|
|
25
|
+
source_code: str,
|
|
26
|
+
file_path: str,
|
|
27
|
+
entity_name: str | None = None
|
|
28
|
+
) -> FunctionInfo | ClassInfo | MethodInfo | ModuleInfo | None:
|
|
29
|
+
"""Extract detailed information about a specific entity.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
source_code: The source code to parse
|
|
33
|
+
file_path: Relative path to the file (for EntityInfo.path)
|
|
34
|
+
entity_name: Name of entity to find, or None for module-level
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
EntityInfo object, or None if not found
|
|
38
|
+
"""
|
|
39
|
+
pass
|