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 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
@@ -0,0 +1,5 @@
1
+ from athena.cli import app
2
+
3
+
4
+ if __name__ == "__main__":
5
+ app()
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)