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/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Athena Code Knowledge
|
|
2
|
+
|
|
3
|
+
A semantic code analysis tool designed to help Claude Code navigate repositories efficiently while dramatically reducing token consumption.
|
|
4
|
+
|
|
5
|
+
## Motivation
|
|
6
|
+
|
|
7
|
+
Claude Code currently incurs significant token costs during repository navigation. A typical planning phase involves reading 10-20 files to understand codebase architecture, consuming substantially more tokens than the targeted modifications themselves. This linear scaling with codebase size makes work on large repositories inefficient.
|
|
8
|
+
|
|
9
|
+
Most discovery queries ("What does this file contain?", "Where is function X?") don't require reading entire source files. By building a queryable semantic index, we can answer these questions using structured metadata instead, potentially reducing planning token costs by 15-30x.
|
|
10
|
+
|
|
11
|
+
## What's the deal with the name?
|
|
12
|
+
Athena was an Ancient Greek goddess associated with strategic wisdom, logic, crafts, architecture and discipline. She is a patron of engineers and planners, not dreamers. Seemed appropriate.
|
|
13
|
+
|
|
14
|
+
One of her symbolic animals was the owl.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
NOTE: Athena currently only works in a Python codebase. More supported languages coming soon!
|
|
19
|
+
|
|
20
|
+
Install with pipx:
|
|
21
|
+
```bash
|
|
22
|
+
pipx install athena-code
|
|
23
|
+
```
|
|
24
|
+
Requires at least Python 3.12, so if that's not installed you should do that with your system package manager. It doesn't need to be the default Python, you can leave that at whatever you want and point Pipx at Python 3.12 explicitly:
|
|
25
|
+
```bash
|
|
26
|
+
pipx install --python python3.12 athena-code
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
use `athena --help` to see up-to-date information about the available features:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
╭─ Commands ─────────────────────────────────────────────────────────────────────╮
|
|
34
|
+
│ locate Locate entities (functions, classes, methods) by name. │
|
|
35
|
+
│ info Get detailed information about a code entity or package. │
|
|
36
|
+
│ mcp-server Start the MCP server for Claude Code integration. │
|
|
37
|
+
│ install-mcp Install MCP server configuration for Claude Code. │
|
|
38
|
+
│ sync Update @athena hash tags in docstrings. │
|
|
39
|
+
│ status Check docstring hash synchronization status. │
|
|
40
|
+
│ uninstall-mcp Remove MCP server configuration from Claude Code. │
|
|
41
|
+
╰────────────────────────────────────────────────────────────────────────────────╯
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Generally, you will install `athena` and then:
|
|
45
|
+
- Run `athena sync` in your codebase. This will add Athena's hashes to all the docstrings and allow `athena` to detect code changes that have invalidated the docstrings.
|
|
46
|
+
- After code changes, run `athena status` to see a table of all the functions that have been updated and may have had their docstrings invalidated.
|
|
47
|
+
- Update the necessary docstrings and then run `athena sync` again to update all the necessary hashes.
|
|
48
|
+
|
|
49
|
+
If you want to find an entity in the codebase, then just run `athena locate <entity>` to get details on the file and lines the entity occupies:
|
|
50
|
+
```bash
|
|
51
|
+
> athena locate get_task
|
|
52
|
+
Kind Path Extent
|
|
53
|
+
method src/tasktree/parser.py 277-286
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Once you know where a thing is, then you can ask for info about it:
|
|
57
|
+
```bash
|
|
58
|
+
> athena info src/tasktree/parser.py:get_task
|
|
59
|
+
{
|
|
60
|
+
"method": {
|
|
61
|
+
"name": "Recipe.get_task",
|
|
62
|
+
"path": "src/tasktree/parser.py",
|
|
63
|
+
"extent": {
|
|
64
|
+
"start": 277,
|
|
65
|
+
"end": 286
|
|
66
|
+
},
|
|
67
|
+
"sig": {
|
|
68
|
+
"name": "get_task",
|
|
69
|
+
"args": [
|
|
70
|
+
{
|
|
71
|
+
"name": "self"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "name",
|
|
75
|
+
"type": "str"
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"return_type": "Task | None"
|
|
79
|
+
},
|
|
80
|
+
"summary": "Get task by name.\n\n Args:\n name: Task name (may be namespaced like 'build.compile')\n\n Returns:\n Task if found, None otherwise\n "
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Install Claude MCP integrations
|
|
86
|
+
|
|
87
|
+
Athena includes Model Context Protocol (MCP) integration, exposing code navigation capabilities as first-class tools in Claude Code.
|
|
88
|
+
|
|
89
|
+
### Benefits
|
|
90
|
+
|
|
91
|
+
- **Native tool discovery** — Tools appear in Claude Code's capabilities list
|
|
92
|
+
- **Structured I/O** — Type-safe parameters and responses
|
|
93
|
+
|
|
94
|
+
### Available Tools
|
|
95
|
+
|
|
96
|
+
- **`ack_locate`** — Find Python entity location (file path + line range)
|
|
97
|
+
|
|
98
|
+
### Installation
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
athena install-mcp
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This automatically configures Claude Code by adding the MCP server entry to your config file. You will need to restart Claude Code for changes to take effect.
|
|
105
|
+
|
|
106
|
+
**Uninstalling:**
|
|
107
|
+
|
|
108
|
+
If you don't like using your Anthropic tokens more efficiently to generate better code, for some reason, then:
|
|
109
|
+
```bash
|
|
110
|
+
athena uninstall-mcp
|
|
111
|
+
```
|
|
112
|
+
to remove the MCP integration
|
|
113
|
+
|
|
114
|
+
## Usage Workflow
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
cd /path/to/repository
|
|
118
|
+
athena locate validateSession # Find the locations of entities in the codebase
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Contributing
|
|
122
|
+
|
|
123
|
+
This is an active development project. Early-stage contributions welcome, particularly:
|
|
124
|
+
|
|
125
|
+
- Tree-sitter AST extraction improvements
|
|
126
|
+
- Language-specific signature formatting
|
|
127
|
+
- LLM prompt engineering for summary quality
|
|
128
|
+
- Performance benchmarking
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT - See LICENSE
|
athena/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Athena - A semantic code analysis tool designed to help Claude Code navigate repositories efficiently while dramatically reducing token consumption."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
__version__ = version("athena")
|
|
7
|
+
except Exception:
|
|
8
|
+
__version__ = "0.0.0.dev0+local" # Fallback for development
|
athena/__main__.py
ADDED
athena/cli.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json as json_module
|
|
3
|
+
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from athena import __version__
|
|
11
|
+
from athena.info import get_entity_info
|
|
12
|
+
from athena.locate import locate_entity
|
|
13
|
+
from athena.models import ClassInfo, FunctionInfo, MethodInfo, ModuleInfo, PackageInfo
|
|
14
|
+
from athena.repository import RepositoryNotFoundError, find_repository_root
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(
|
|
17
|
+
help="Athena Code Knowledge - semantic code analysis tool",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
@app.command()
|
|
24
|
+
def locate(
|
|
25
|
+
entity_name: str,
|
|
26
|
+
json: bool = typer.Option(False, "--json", "-j", help="Output as JSON instead of table")
|
|
27
|
+
):
|
|
28
|
+
"""Locate entities (functions, classes, methods) by name.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
entity_name: The name of the entity to search for
|
|
32
|
+
json: If True, output JSON format; otherwise output as table (default)
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
entities = locate_entity(entity_name)
|
|
36
|
+
|
|
37
|
+
if json:
|
|
38
|
+
# Convert entities to dictionaries and remove the name field (internal only)
|
|
39
|
+
results = []
|
|
40
|
+
for entity in entities:
|
|
41
|
+
entity_dict = asdict(entity)
|
|
42
|
+
del entity_dict["name"] # Name is only for internal filtering
|
|
43
|
+
results.append(entity_dict)
|
|
44
|
+
|
|
45
|
+
# Output as JSON
|
|
46
|
+
typer.echo(json_module.dumps(results, indent=2))
|
|
47
|
+
else:
|
|
48
|
+
# Output as table
|
|
49
|
+
from rich.table import Table
|
|
50
|
+
|
|
51
|
+
table = Table(show_header=True, header_style="bold cyan", box=None)
|
|
52
|
+
table.add_column("Kind", style="green")
|
|
53
|
+
table.add_column("Path", style="blue")
|
|
54
|
+
table.add_column("Extent", style="yellow")
|
|
55
|
+
|
|
56
|
+
for entity in entities:
|
|
57
|
+
extent_str = f"{entity.extent.start}-{entity.extent.end}"
|
|
58
|
+
table.add_row(entity.kind, entity.path, extent_str)
|
|
59
|
+
|
|
60
|
+
console.print(table)
|
|
61
|
+
|
|
62
|
+
except RepositoryNotFoundError as e:
|
|
63
|
+
typer.echo(f"Error: {e}", err=True)
|
|
64
|
+
raise typer.Exit(code=1)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
typer.echo(f"Error: {e}", err=True)
|
|
67
|
+
raise typer.Exit(code=2)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command()
|
|
71
|
+
def info(location: str):
|
|
72
|
+
"""Get detailed information about a code entity or package.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
location: Path to entity in format "file_path:entity_name",
|
|
76
|
+
"file_path" for module-level info,
|
|
77
|
+
or "directory_path" for package info
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
ack info src/auth/session.py:validateSession
|
|
81
|
+
ack info src/auth/session.py
|
|
82
|
+
ack info src/athena
|
|
83
|
+
"""
|
|
84
|
+
# Parse location string
|
|
85
|
+
if ":" in location:
|
|
86
|
+
file_path, entity_name = location.rsplit(":", 1)
|
|
87
|
+
# Handle empty entity name after colon
|
|
88
|
+
if not entity_name:
|
|
89
|
+
entity_name = None
|
|
90
|
+
else:
|
|
91
|
+
file_path = location
|
|
92
|
+
entity_name = None
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# Get entity info
|
|
96
|
+
entity_info = get_entity_info(file_path, entity_name)
|
|
97
|
+
except (FileNotFoundError, ValueError, RepositoryNotFoundError) as e:
|
|
98
|
+
typer.echo(f"Error: {e}", err=True)
|
|
99
|
+
raise typer.Exit(code=1)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
typer.echo(f"Error: {e}", err=True)
|
|
102
|
+
raise typer.Exit(code=2)
|
|
103
|
+
|
|
104
|
+
# Check if entity was found
|
|
105
|
+
if entity_info is None:
|
|
106
|
+
typer.echo(f"Error: Entity '{entity_name}' not found in {file_path}", err=True)
|
|
107
|
+
raise typer.Exit(code=1)
|
|
108
|
+
|
|
109
|
+
# Wrap in discriminated structure
|
|
110
|
+
if isinstance(entity_info, FunctionInfo):
|
|
111
|
+
output = {"function": asdict(entity_info)}
|
|
112
|
+
elif isinstance(entity_info, ClassInfo):
|
|
113
|
+
output = {"class": asdict(entity_info)}
|
|
114
|
+
elif isinstance(entity_info, MethodInfo):
|
|
115
|
+
output = {"method": asdict(entity_info)}
|
|
116
|
+
elif isinstance(entity_info, ModuleInfo):
|
|
117
|
+
output = {"module": asdict(entity_info)}
|
|
118
|
+
elif isinstance(entity_info, PackageInfo):
|
|
119
|
+
output = {"package": asdict(entity_info)}
|
|
120
|
+
else:
|
|
121
|
+
typer.echo(f"Error: Unknown entity type: {type(entity_info)}", err=True)
|
|
122
|
+
raise typer.Exit(code=2)
|
|
123
|
+
|
|
124
|
+
# Filter out None values (especially summary field)
|
|
125
|
+
# When summary is None, we want to omit it entirely from JSON
|
|
126
|
+
def filter_none(d):
|
|
127
|
+
if isinstance(d, dict):
|
|
128
|
+
return {k: filter_none(v) for k, v in d.items() if v is not None}
|
|
129
|
+
elif isinstance(d, list):
|
|
130
|
+
return [filter_none(item) for item in d]
|
|
131
|
+
else:
|
|
132
|
+
return d
|
|
133
|
+
|
|
134
|
+
output = filter_none(output)
|
|
135
|
+
|
|
136
|
+
# Output as JSON
|
|
137
|
+
typer.echo(json_module.dumps(output, indent=2))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command()
|
|
141
|
+
def mcp_server():
|
|
142
|
+
"""Start the MCP server for Claude Code integration.
|
|
143
|
+
|
|
144
|
+
This command starts the Model Context Protocol server that exposes
|
|
145
|
+
Athena's code navigation tools to Claude Code via structured JSON-RPC.
|
|
146
|
+
"""
|
|
147
|
+
from athena.mcp_server import main
|
|
148
|
+
|
|
149
|
+
asyncio.run(main())
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@app.command()
|
|
153
|
+
def install_mcp():
|
|
154
|
+
"""Install MCP server configuration for Claude Code.
|
|
155
|
+
|
|
156
|
+
This command automatically configures Claude Code to use the Athena
|
|
157
|
+
MCP server by adding the appropriate entry to the Claude config file.
|
|
158
|
+
"""
|
|
159
|
+
from athena.mcp_config import install_mcp_config
|
|
160
|
+
|
|
161
|
+
success, message = install_mcp_config()
|
|
162
|
+
|
|
163
|
+
if success:
|
|
164
|
+
typer.echo(f"✓ {message}")
|
|
165
|
+
typer.echo("\nRestart Claude Code for changes to take effect.")
|
|
166
|
+
else:
|
|
167
|
+
typer.echo(f"✗ {message}", err=True)
|
|
168
|
+
raise typer.Exit(code=1)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.command()
|
|
172
|
+
def sync(
|
|
173
|
+
entity: Optional[str] = typer.Argument(None, help="Entity to sync (module, class, function, etc.)"),
|
|
174
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force hash recalculation even if valid"),
|
|
175
|
+
recursive: bool = typer.Option(False, "--recursive", "-r", help="Apply recursively to sub-entities"),
|
|
176
|
+
):
|
|
177
|
+
"""Update @athena hash tags in docstrings.
|
|
178
|
+
|
|
179
|
+
Updates or inserts @athena hash tags in entity docstrings based on their
|
|
180
|
+
current code structure. Hashes are computed from the AST and embedded
|
|
181
|
+
in docstrings for staleness detection.
|
|
182
|
+
|
|
183
|
+
If no entity is specified, syncs the entire project recursively.
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
athena sync # Sync entire project
|
|
187
|
+
athena sync src/module.py # Sync all entities in module
|
|
188
|
+
athena sync src/module.py:MyClass # Sync specific class
|
|
189
|
+
athena sync src/module.py:MyClass.method # Sync specific method
|
|
190
|
+
athena sync src/package --recursive # Sync package recursively
|
|
191
|
+
athena sync src/module.py:func --force # Force update even if hash matches
|
|
192
|
+
"""
|
|
193
|
+
from athena.sync import sync_entity, sync_recursive
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
repo_root = find_repository_root()
|
|
197
|
+
except RepositoryNotFoundError as e:
|
|
198
|
+
typer.echo(f"Error: {e}", err=True)
|
|
199
|
+
raise typer.Exit(code=255)
|
|
200
|
+
|
|
201
|
+
# If no entity specified, sync entire project
|
|
202
|
+
if entity is None:
|
|
203
|
+
entity = "."
|
|
204
|
+
recursive = True
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
if recursive:
|
|
208
|
+
# Use recursive sync
|
|
209
|
+
update_count = sync_recursive(entity, force, repo_root)
|
|
210
|
+
if update_count > 0:
|
|
211
|
+
typer.echo(f"Updated {update_count} entities")
|
|
212
|
+
else:
|
|
213
|
+
typer.echo("No updates needed")
|
|
214
|
+
else:
|
|
215
|
+
# Use single entity sync
|
|
216
|
+
updated = sync_entity(entity, force, repo_root)
|
|
217
|
+
if updated:
|
|
218
|
+
typer.echo("Updated 1 entity")
|
|
219
|
+
else:
|
|
220
|
+
typer.echo("No updates needed")
|
|
221
|
+
|
|
222
|
+
except NotImplementedError as e:
|
|
223
|
+
typer.echo(f"Error: {e}", err=True)
|
|
224
|
+
typer.echo("\nNote: Module and package-level sync requires --recursive flag")
|
|
225
|
+
raise typer.Exit(code=1)
|
|
226
|
+
except (ValueError, FileNotFoundError) as e:
|
|
227
|
+
typer.echo(f"Error: {e}", err=True)
|
|
228
|
+
raise typer.Exit(code=1)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
typer.echo(f"Error: {e}", err=True)
|
|
231
|
+
raise typer.Exit(code=2)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@app.command()
|
|
235
|
+
def status(
|
|
236
|
+
entity: Optional[str] = typer.Argument(None, help="Entity to check status for"),
|
|
237
|
+
recursive: bool = typer.Option(False, "--recursive", "-r", help="Check entity and all sub-entities"),
|
|
238
|
+
):
|
|
239
|
+
"""Check docstring hash synchronization status.
|
|
240
|
+
|
|
241
|
+
Displays which entities have out-of-sync @athena hash tags. An entity is
|
|
242
|
+
out-of-sync if it has no hash tag or if the tag doesn't match the current
|
|
243
|
+
code structure.
|
|
244
|
+
|
|
245
|
+
If no entity is specified, checks the entire project.
|
|
246
|
+
|
|
247
|
+
Examples:
|
|
248
|
+
athena status src/module.py:MyClass # Check specific class
|
|
249
|
+
athena status src/module.py:MyClass -r # Check class and methods
|
|
250
|
+
athena status src/module.py --recursive # Check all entities in module
|
|
251
|
+
"""
|
|
252
|
+
from athena.status import check_status, check_status_recursive, filter_out_of_sync
|
|
253
|
+
from rich.table import Table
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
repo_root = find_repository_root()
|
|
257
|
+
except RepositoryNotFoundError as e:
|
|
258
|
+
typer.echo(f"Error: {e}", err=True)
|
|
259
|
+
raise typer.Exit(code=255)
|
|
260
|
+
|
|
261
|
+
# If no entity specified, check entire project recursively
|
|
262
|
+
if entity is None:
|
|
263
|
+
entity = "."
|
|
264
|
+
recursive = True
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
if recursive:
|
|
268
|
+
statuses = check_status_recursive(entity, repo_root)
|
|
269
|
+
else:
|
|
270
|
+
statuses = check_status(entity, repo_root)
|
|
271
|
+
out_of_sync = filter_out_of_sync(statuses)
|
|
272
|
+
|
|
273
|
+
if not out_of_sync:
|
|
274
|
+
typer.echo("All entities are in sync")
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
typer.echo(f"{len(out_of_sync)} entities need updating")
|
|
278
|
+
typer.echo()
|
|
279
|
+
|
|
280
|
+
table = Table(show_header=True, header_style="bold cyan", box=None)
|
|
281
|
+
table.add_column("Kind", style="green")
|
|
282
|
+
table.add_column("Path", style="blue")
|
|
283
|
+
table.add_column("Extent", style="yellow")
|
|
284
|
+
table.add_column("Recorded Hash", style="magenta")
|
|
285
|
+
table.add_column("Calc. Hash", style="magenta")
|
|
286
|
+
|
|
287
|
+
for status_item in out_of_sync:
|
|
288
|
+
recorded = status_item.recorded_hash or "<NONE>"
|
|
289
|
+
table.add_row(
|
|
290
|
+
status_item.kind,
|
|
291
|
+
status_item.path,
|
|
292
|
+
status_item.extent,
|
|
293
|
+
recorded,
|
|
294
|
+
status_item.calculated_hash
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
console.print(table)
|
|
298
|
+
|
|
299
|
+
except NotImplementedError as e:
|
|
300
|
+
typer.echo(f"Error: {e}", err=True)
|
|
301
|
+
raise typer.Exit(code=1)
|
|
302
|
+
except (ValueError, FileNotFoundError) as e:
|
|
303
|
+
typer.echo(f"Error: {e}", err=True)
|
|
304
|
+
raise typer.Exit(code=1)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
typer.echo(f"Error: {e}", err=True)
|
|
307
|
+
raise typer.Exit(code=2)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@app.command()
|
|
311
|
+
def uninstall_mcp():
|
|
312
|
+
"""Remove MCP server configuration from Claude Code.
|
|
313
|
+
|
|
314
|
+
This command removes the Athena MCP server entry from the Claude
|
|
315
|
+
configuration file.
|
|
316
|
+
"""
|
|
317
|
+
from athena.mcp_config import uninstall_mcp_config
|
|
318
|
+
|
|
319
|
+
success, message = uninstall_mcp_config()
|
|
320
|
+
|
|
321
|
+
if success:
|
|
322
|
+
typer.echo(f"✓ {message}")
|
|
323
|
+
typer.echo("\nRestart Claude Code for changes to take effect.")
|
|
324
|
+
else:
|
|
325
|
+
typer.echo(f"✗ {message}", err=True)
|
|
326
|
+
raise typer.Exit(code=1)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _version_callback(value: bool):
|
|
330
|
+
"""Show version and exit."""
|
|
331
|
+
if value:
|
|
332
|
+
console.print(f"athena version {__version__}")
|
|
333
|
+
raise typer.Exit()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@app.callback(invoke_without_command=True)
|
|
337
|
+
def main(
|
|
338
|
+
ctx: typer.Context,
|
|
339
|
+
version: Optional[bool] = typer.Option(
|
|
340
|
+
None,
|
|
341
|
+
"--version",
|
|
342
|
+
"-v",
|
|
343
|
+
callback=_version_callback,
|
|
344
|
+
is_eager=True,
|
|
345
|
+
help="Show version and exit",
|
|
346
|
+
)):
|
|
347
|
+
pass
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Module for updating docstrings in source code files."""
|
|
2
|
+
|
|
3
|
+
from athena.models import Location
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def update_docstring_in_source(
|
|
7
|
+
source_code: str, entity_location: Location, new_docstring: str
|
|
8
|
+
) -> str:
|
|
9
|
+
"""Replace or insert docstring in source code for a given entity.
|
|
10
|
+
|
|
11
|
+
This function updates the docstring at the specified location in the source code.
|
|
12
|
+
It handles:
|
|
13
|
+
- Entities with existing docstrings (updates them)
|
|
14
|
+
- Entities without docstrings (inserts new docstring)
|
|
15
|
+
- Preservation of indentation and formatting
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
source_code: Full source code string
|
|
19
|
+
entity_location: Location of the entity (start/end line numbers, 0-indexed)
|
|
20
|
+
new_docstring: New docstring content (without triple quotes)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Updated source code with new docstring
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If entity_location is invalid
|
|
27
|
+
"""
|
|
28
|
+
lines = source_code.splitlines(keepends=True)
|
|
29
|
+
|
|
30
|
+
# Validate location
|
|
31
|
+
if entity_location.start < 0 or entity_location.end >= len(lines):
|
|
32
|
+
raise ValueError(f"Invalid entity location: {entity_location}")
|
|
33
|
+
|
|
34
|
+
# Find the definition line (function/class declaration)
|
|
35
|
+
# The entity location may include decorators, so we need to find the actual def/class line
|
|
36
|
+
def_line_idx = entity_location.start
|
|
37
|
+
|
|
38
|
+
# Search for the actual def/class line (skip any decorators)
|
|
39
|
+
for i in range(entity_location.start, min(entity_location.end + 1, len(lines))):
|
|
40
|
+
stripped = lines[i].lstrip()
|
|
41
|
+
if stripped.startswith('def ') or stripped.startswith('class '):
|
|
42
|
+
def_line_idx = i
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
# Determine indentation of the entity
|
|
46
|
+
def_line = lines[def_line_idx]
|
|
47
|
+
indent = len(def_line) - len(def_line.lstrip())
|
|
48
|
+
entity_indent = " " * indent
|
|
49
|
+
|
|
50
|
+
# Determine the body start (line after def/class declaration)
|
|
51
|
+
# For multi-line signatures, we need to find the line with the closing colon
|
|
52
|
+
# Search forward from def_line_idx to find a line ending with ':'
|
|
53
|
+
body_start_idx = def_line_idx + 1
|
|
54
|
+
for i in range(def_line_idx, min(entity_location.end + 1, len(lines))):
|
|
55
|
+
line = lines[i]
|
|
56
|
+
# Check if line ends with ':' (ignoring trailing whitespace and comments)
|
|
57
|
+
stripped = line.rstrip()
|
|
58
|
+
if stripped.endswith(':'):
|
|
59
|
+
body_start_idx = i + 1
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
# Check if there's already a docstring
|
|
63
|
+
# A docstring is the first non-empty statement in the body
|
|
64
|
+
docstring_start_idx = None
|
|
65
|
+
docstring_end_idx = None
|
|
66
|
+
|
|
67
|
+
# Skip empty lines after definition
|
|
68
|
+
search_idx = body_start_idx
|
|
69
|
+
while search_idx <= entity_location.end and search_idx < len(lines):
|
|
70
|
+
line = lines[search_idx]
|
|
71
|
+
stripped = line.strip()
|
|
72
|
+
|
|
73
|
+
if not stripped:
|
|
74
|
+
# Empty line, continue
|
|
75
|
+
search_idx += 1
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# Check if this line starts a docstring (triple quotes)
|
|
79
|
+
if stripped.startswith('"""') or stripped.startswith("'''"):
|
|
80
|
+
# Found docstring start
|
|
81
|
+
docstring_start_idx = search_idx
|
|
82
|
+
quote_type = '"""' if stripped.startswith('"""') else "'''"
|
|
83
|
+
|
|
84
|
+
# Check if it's a single-line docstring
|
|
85
|
+
# Remove leading quote
|
|
86
|
+
after_start_quote = stripped[3:]
|
|
87
|
+
if after_start_quote.endswith(quote_type):
|
|
88
|
+
# Single-line docstring
|
|
89
|
+
docstring_end_idx = search_idx
|
|
90
|
+
else:
|
|
91
|
+
# Multi-line docstring - find the end
|
|
92
|
+
search_idx += 1
|
|
93
|
+
while search_idx <= entity_location.end and search_idx < len(lines):
|
|
94
|
+
if quote_type in lines[search_idx]:
|
|
95
|
+
docstring_end_idx = search_idx
|
|
96
|
+
break
|
|
97
|
+
search_idx += 1
|
|
98
|
+
|
|
99
|
+
break
|
|
100
|
+
else:
|
|
101
|
+
# Non-docstring statement found, no docstring exists
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
# Format the new docstring with proper indentation
|
|
105
|
+
docstring_indent = entity_indent + " " # One level deeper than entity
|
|
106
|
+
formatted_lines = []
|
|
107
|
+
formatted_lines.append(f'{docstring_indent}"""\n')
|
|
108
|
+
|
|
109
|
+
# Add docstring content with proper indentation
|
|
110
|
+
# Strip existing indentation from each line and apply consistent indentation
|
|
111
|
+
for line in new_docstring.splitlines():
|
|
112
|
+
stripped = line.lstrip()
|
|
113
|
+
if stripped: # Non-empty line
|
|
114
|
+
formatted_lines.append(f"{docstring_indent}{stripped}\n")
|
|
115
|
+
else: # Empty line
|
|
116
|
+
formatted_lines.append("\n")
|
|
117
|
+
|
|
118
|
+
formatted_lines.append(f'{docstring_indent}"""\n')
|
|
119
|
+
|
|
120
|
+
# Now insert or replace the docstring
|
|
121
|
+
if docstring_start_idx is not None and docstring_end_idx is not None:
|
|
122
|
+
# Replace existing docstring
|
|
123
|
+
# Remove old docstring lines
|
|
124
|
+
result_lines = (
|
|
125
|
+
lines[:docstring_start_idx]
|
|
126
|
+
+ formatted_lines
|
|
127
|
+
+ lines[docstring_end_idx + 1 :]
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
# Insert new docstring after definition line
|
|
131
|
+
result_lines = lines[:body_start_idx] + formatted_lines + lines[body_start_idx:]
|
|
132
|
+
|
|
133
|
+
return "".join(result_lines)
|