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/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