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/entity_path.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Entity path parsing and resolution.
|
|
2
|
+
|
|
3
|
+
Entity path format: dir/package/module:class.method
|
|
4
|
+
Examples:
|
|
5
|
+
- src/athena/cli.py:app
|
|
6
|
+
- src/athena/parsers/python_parser.py:PythonParser.parse_athena_tag
|
|
7
|
+
- models.py:Entity
|
|
8
|
+
- src/athena
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class EntityPath:
|
|
18
|
+
"""Represents a parsed entity path.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
file_path: Path to the file (without entity specification)
|
|
22
|
+
entity_name: Name of the entity within the file (class.method or function)
|
|
23
|
+
None for module-level or package-level operations
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
file_path: str
|
|
27
|
+
entity_name: str | None = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def is_package(self) -> bool:
|
|
31
|
+
"""Check if this path represents a package (directory)."""
|
|
32
|
+
return not self.file_path.endswith(".py") and self.entity_name is None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def is_module(self) -> bool:
|
|
36
|
+
"""Check if this path represents a module (file without entity)."""
|
|
37
|
+
return self.file_path.endswith(".py") and self.entity_name is None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_class(self) -> bool:
|
|
41
|
+
"""Check if this path represents a class (no dot in entity name)."""
|
|
42
|
+
return self.entity_name is not None and "." not in self.entity_name
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def is_method(self) -> bool:
|
|
46
|
+
"""Check if this path represents a method (dot in entity name)."""
|
|
47
|
+
return self.entity_name is not None and "." in self.entity_name
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def class_name(self) -> str | None:
|
|
51
|
+
"""Extract class name if this is a method path."""
|
|
52
|
+
if self.is_method:
|
|
53
|
+
return self.entity_name.split(".")[0]
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def method_name(self) -> str | None:
|
|
58
|
+
"""Extract method name if this is a method path."""
|
|
59
|
+
if self.is_method:
|
|
60
|
+
return self.entity_name.split(".")[1]
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_entity_path(path: str) -> EntityPath:
|
|
65
|
+
"""Parse an entity path string into components.
|
|
66
|
+
|
|
67
|
+
Format: [directory/]file.py[:entity[.subentity]]
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
path: Entity path string to parse
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
EntityPath object with parsed components
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: If path format is invalid
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
>>> parse_entity_path("src/foo/bar.py:Baz.bax")
|
|
80
|
+
EntityPath(file_path='src/foo/bar.py', entity_name='Baz.bax')
|
|
81
|
+
|
|
82
|
+
>>> parse_entity_path("models.py:Entity")
|
|
83
|
+
EntityPath(file_path='models.py', entity_name='Entity')
|
|
84
|
+
|
|
85
|
+
>>> parse_entity_path("src/athena/cli.py")
|
|
86
|
+
EntityPath(file_path='src/athena/cli.py', entity_name=None)
|
|
87
|
+
|
|
88
|
+
>>> parse_entity_path("src/athena")
|
|
89
|
+
EntityPath(file_path='src/athena', entity_name=None)
|
|
90
|
+
"""
|
|
91
|
+
if not path or not path.strip():
|
|
92
|
+
raise ValueError("Entity path cannot be empty")
|
|
93
|
+
|
|
94
|
+
# Split on colon to separate file path from entity name
|
|
95
|
+
if ":" in path:
|
|
96
|
+
file_path, entity_name = path.split(":", 1)
|
|
97
|
+
entity_name = entity_name.strip()
|
|
98
|
+
if not entity_name:
|
|
99
|
+
entity_name = None
|
|
100
|
+
else:
|
|
101
|
+
file_path = path
|
|
102
|
+
entity_name = None
|
|
103
|
+
|
|
104
|
+
file_path = file_path.strip()
|
|
105
|
+
|
|
106
|
+
if not file_path:
|
|
107
|
+
raise ValueError("File path component cannot be empty")
|
|
108
|
+
|
|
109
|
+
return EntityPath(file_path=file_path, entity_name=entity_name)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def resolve_entity_path(entity_path: EntityPath, repo_root: Path) -> Path | None:
|
|
113
|
+
"""Resolve an EntityPath to an actual file system path.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
entity_path: Parsed entity path
|
|
117
|
+
repo_root: Root directory of the repository
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Resolved Path object if file/directory exists, None otherwise
|
|
121
|
+
"""
|
|
122
|
+
# Construct full path
|
|
123
|
+
full_path = repo_root / entity_path.file_path
|
|
124
|
+
|
|
125
|
+
# If it's meant to be a package, check if it's a valid directory
|
|
126
|
+
if entity_path.is_package:
|
|
127
|
+
# Check if the directory exists
|
|
128
|
+
if not full_path.is_dir():
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
# For the repository root (.), we don't require __init__.py
|
|
132
|
+
# For other directories, require __init__.py to be a valid package
|
|
133
|
+
if entity_path.file_path == ".":
|
|
134
|
+
return full_path
|
|
135
|
+
|
|
136
|
+
init_path = full_path / "__init__.py"
|
|
137
|
+
if init_path.exists():
|
|
138
|
+
# Return the directory path, not __init__.py
|
|
139
|
+
return full_path
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
# For files, check if path exists
|
|
143
|
+
if full_path.exists():
|
|
144
|
+
return full_path
|
|
145
|
+
|
|
146
|
+
return None
|
athena/hashing.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Hash generation infrastructure for code entities using tree-sitter AST."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _is_docstring_node(node, parent_node) -> bool:
|
|
8
|
+
"""Check if a node is a docstring (first statement in body that's a string).
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
node: The node to check
|
|
12
|
+
parent_node: The parent node (should be a block)
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
True if this is a docstring node
|
|
16
|
+
"""
|
|
17
|
+
# Docstrings are expression_statements containing a string
|
|
18
|
+
if node.type != "expression_statement":
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
# Must be the first child of a block (body)
|
|
22
|
+
if parent_node is None or parent_node.type != "block":
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
# Check if this is the first child of the block
|
|
26
|
+
if parent_node.children and parent_node.children[0] == node:
|
|
27
|
+
# Check if it contains a string
|
|
28
|
+
for child in node.children:
|
|
29
|
+
if child.type == "string":
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def serialize_ast_node(node, source_code: str) -> str:
|
|
36
|
+
"""Serialize a tree-sitter AST node to a stable string representation.
|
|
37
|
+
|
|
38
|
+
This serialization includes node types and names, which forms the basis for
|
|
39
|
+
generating content hashes. The serialization is designed to be stable across
|
|
40
|
+
whitespace changes but sensitive to semantic changes.
|
|
41
|
+
|
|
42
|
+
Docstrings are excluded from serialization to ensure hash stability when
|
|
43
|
+
@athena tags are added or updated.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
node: Tree-sitter AST node to serialize
|
|
47
|
+
source_code: Source code string for extracting identifiers
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Serialized string representation of the AST structure
|
|
51
|
+
"""
|
|
52
|
+
parts = []
|
|
53
|
+
|
|
54
|
+
def serialize(n, parent=None, depth: int = 0):
|
|
55
|
+
"""Recursively serialize the node and its children."""
|
|
56
|
+
# Skip docstring nodes
|
|
57
|
+
if _is_docstring_node(n, parent):
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# For nodes with meaningful text content, include the text
|
|
61
|
+
if n.type in ("identifier", "integer", "float", "string"):
|
|
62
|
+
text = source_code.encode("utf8")[n.start_byte : n.end_byte].decode("utf8")
|
|
63
|
+
parts.append(f"{n.type}:{text}")
|
|
64
|
+
else:
|
|
65
|
+
# Add node type
|
|
66
|
+
parts.append(f"{n.type}")
|
|
67
|
+
|
|
68
|
+
# Recursively serialize children
|
|
69
|
+
for child in n.children:
|
|
70
|
+
serialize(child, n, depth + 1)
|
|
71
|
+
|
|
72
|
+
serialize(node)
|
|
73
|
+
return "|".join(parts)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def compute_hash(content: str) -> str:
|
|
77
|
+
"""Compute SHA-256 hash and truncate to 12 hex characters.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
content: Content string to hash
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
12-character hex hash string
|
|
84
|
+
"""
|
|
85
|
+
hash_obj = hashlib.sha256(content.encode("utf8"))
|
|
86
|
+
return hash_obj.hexdigest()[:12]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def compute_function_hash(node, source_code: str) -> str:
|
|
90
|
+
"""Compute hash for a function (signature + body).
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
node: Tree-sitter function_definition node
|
|
94
|
+
source_code: Source code string
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
12-character hex hash
|
|
98
|
+
"""
|
|
99
|
+
# Serialize the entire function node (includes signature and body)
|
|
100
|
+
serialization = serialize_ast_node(node, source_code)
|
|
101
|
+
return compute_hash(serialization)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def compute_class_hash(node, source_code: str) -> str:
|
|
105
|
+
"""Compute hash for a class (declaration + method signatures + implementations).
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
node: Tree-sitter class_definition node
|
|
109
|
+
source_code: Source code string
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
12-character hex hash
|
|
113
|
+
"""
|
|
114
|
+
# Serialize the entire class node (includes declaration, methods, etc.)
|
|
115
|
+
serialization = serialize_ast_node(node, source_code)
|
|
116
|
+
return compute_hash(serialization)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def compute_module_hash(entities_docstrings: list[str]) -> str:
|
|
120
|
+
"""Compute hash for a module based on non-whitespace from entity docstrings.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
entities_docstrings: List of docstring contents from module entities
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
12-character hex hash
|
|
127
|
+
"""
|
|
128
|
+
# Concatenate all docstrings with non-whitespace characters only
|
|
129
|
+
combined = ""
|
|
130
|
+
for docstring in entities_docstrings:
|
|
131
|
+
if docstring:
|
|
132
|
+
# Remove all whitespace
|
|
133
|
+
no_whitespace = re.sub(r"\s+", "", docstring)
|
|
134
|
+
combined += no_whitespace
|
|
135
|
+
|
|
136
|
+
return compute_hash(combined)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def compute_package_hash(module_docstrings: list[str]) -> str:
|
|
140
|
+
"""Compute hash for a package based on non-whitespace from module docstrings.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
module_docstrings: List of module docstrings from package
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
12-character hex hash
|
|
147
|
+
"""
|
|
148
|
+
# Same logic as module hash - concatenate non-whitespace
|
|
149
|
+
combined = ""
|
|
150
|
+
for docstring in module_docstrings:
|
|
151
|
+
if docstring:
|
|
152
|
+
# Remove all whitespace
|
|
153
|
+
no_whitespace = re.sub(r"\s+", "", docstring)
|
|
154
|
+
combined += no_whitespace
|
|
155
|
+
|
|
156
|
+
return compute_hash(combined)
|
athena/info.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from athena.models import EntityInfo, PackageInfo
|
|
4
|
+
from athena.parsers import get_parser_for_file
|
|
5
|
+
from athena.repository import find_repository_root, get_relative_path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_entity_info(
|
|
9
|
+
file_path: str,
|
|
10
|
+
entity_name: str | None = None,
|
|
11
|
+
root: Path | None = None
|
|
12
|
+
) -> EntityInfo | None:
|
|
13
|
+
"""Get detailed information about an entity in a file or package.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
file_path: Path to file or directory (can be absolute or relative to repo root)
|
|
17
|
+
entity_name: Name of entity, or None for module/package-level info
|
|
18
|
+
root: Repository root (auto-detected if None)
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
EntityInfo object, or None if file/entity not found
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
FileNotFoundError: If file/directory doesn't exist
|
|
25
|
+
ValueError: If file type not supported or directory missing __init__.py
|
|
26
|
+
"""
|
|
27
|
+
# Auto-detect repository root if not provided
|
|
28
|
+
if root is None:
|
|
29
|
+
root = find_repository_root(Path.cwd())
|
|
30
|
+
|
|
31
|
+
# Resolve file path
|
|
32
|
+
file_path_obj = Path(file_path)
|
|
33
|
+
if not file_path_obj.is_absolute():
|
|
34
|
+
file_path_obj = root / file_path_obj
|
|
35
|
+
|
|
36
|
+
# Check path exists
|
|
37
|
+
if not file_path_obj.exists():
|
|
38
|
+
raise FileNotFoundError(f"Path not found: {file_path}")
|
|
39
|
+
|
|
40
|
+
# Handle directory (package) case
|
|
41
|
+
if file_path_obj.is_dir():
|
|
42
|
+
if entity_name is not None:
|
|
43
|
+
raise ValueError(f"Cannot specify entity name for package: {file_path}")
|
|
44
|
+
|
|
45
|
+
# Look for __init__.py
|
|
46
|
+
init_file = file_path_obj / "__init__.py"
|
|
47
|
+
if not init_file.exists():
|
|
48
|
+
raise ValueError(f"Package missing __init__.py: {file_path}")
|
|
49
|
+
|
|
50
|
+
# Get parser and extract module docstring from __init__.py
|
|
51
|
+
parser = get_parser_for_file(init_file)
|
|
52
|
+
if parser is None:
|
|
53
|
+
raise ValueError(f"Cannot parse __init__.py in: {file_path}")
|
|
54
|
+
|
|
55
|
+
source_code = init_file.read_text()
|
|
56
|
+
# Get relative path for the directory
|
|
57
|
+
relative_path = get_relative_path(file_path_obj, root)
|
|
58
|
+
|
|
59
|
+
# Extract module-level info from __init__.py
|
|
60
|
+
module_info = parser.extract_entity_info(source_code, str(init_file), None)
|
|
61
|
+
|
|
62
|
+
# Convert to PackageInfo
|
|
63
|
+
if module_info is not None:
|
|
64
|
+
return PackageInfo(
|
|
65
|
+
path=relative_path,
|
|
66
|
+
summary=module_info.summary if hasattr(module_info, 'summary') else None
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
return PackageInfo(path=relative_path, summary=None)
|
|
70
|
+
|
|
71
|
+
# Handle file case
|
|
72
|
+
# Get parser for file
|
|
73
|
+
parser = get_parser_for_file(file_path_obj)
|
|
74
|
+
if parser is None:
|
|
75
|
+
raise ValueError(f"Unsupported file type: {file_path}")
|
|
76
|
+
|
|
77
|
+
# Read source code
|
|
78
|
+
source_code = file_path_obj.read_text()
|
|
79
|
+
|
|
80
|
+
# Get relative path for EntityInfo.path
|
|
81
|
+
relative_path = get_relative_path(file_path_obj, root)
|
|
82
|
+
|
|
83
|
+
# Call parser to extract entity info
|
|
84
|
+
return parser.extract_entity_info(source_code, relative_path, entity_name)
|
athena/locate.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from athena.models import Entity
|
|
4
|
+
from athena.parsers import get_parser_for_file
|
|
5
|
+
from athena.repository import find_python_files, find_repository_root, get_relative_path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def locate_entity(name: str, root: Path | None = None) -> list[Entity]:
|
|
9
|
+
"""Locate all entities with the given name in the repository.
|
|
10
|
+
|
|
11
|
+
For methods, the search matches both the full qualified name (ClassName.method_name)
|
|
12
|
+
and the short method name (method_name).
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
name: The entity name to search for
|
|
16
|
+
root: Repository root (defaults to auto-detected root)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
List of Entity objects matching the given name
|
|
20
|
+
"""
|
|
21
|
+
if root is None:
|
|
22
|
+
root = find_repository_root()
|
|
23
|
+
|
|
24
|
+
entities = []
|
|
25
|
+
|
|
26
|
+
for file_path in find_python_files(root):
|
|
27
|
+
parser = get_parser_for_file(file_path)
|
|
28
|
+
if parser is None:
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
source_code = file_path.read_text(encoding="utf-8")
|
|
33
|
+
relative_path = get_relative_path(file_path, root)
|
|
34
|
+
|
|
35
|
+
file_entities = parser.extract_entities(source_code, relative_path)
|
|
36
|
+
|
|
37
|
+
# Filter entities by name
|
|
38
|
+
for entity in file_entities:
|
|
39
|
+
# Exact match
|
|
40
|
+
if entity.name == name:
|
|
41
|
+
entities.append(entity)
|
|
42
|
+
# For methods, also match the short name (without class prefix)
|
|
43
|
+
elif entity.kind == "method" and "." in entity.name:
|
|
44
|
+
method_name = entity.name.split(".", 1)[1]
|
|
45
|
+
if method_name == name:
|
|
46
|
+
entities.append(entity)
|
|
47
|
+
except Exception:
|
|
48
|
+
# Skip files that can't be read or parsed
|
|
49
|
+
# This allows the scan to continue even if some files fail
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
return entities
|
athena/mcp_config.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""MCP configuration management for Claude Code integration."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_claude_config_path() -> Path:
|
|
10
|
+
"""Get the Claude Code configuration file path for the current OS.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Path to claude_desktop_config.json
|
|
14
|
+
|
|
15
|
+
Raises:
|
|
16
|
+
RuntimeError: If OS is not supported
|
|
17
|
+
"""
|
|
18
|
+
system = platform.system()
|
|
19
|
+
|
|
20
|
+
if system == "Darwin": # macOS
|
|
21
|
+
return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
22
|
+
elif system == "Linux":
|
|
23
|
+
return Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
|
|
24
|
+
elif system == "Windows":
|
|
25
|
+
return Path(os.environ["APPDATA"]) / "Claude" / "claude_desktop_config.json"
|
|
26
|
+
else:
|
|
27
|
+
raise RuntimeError(f"Unsupported operating system: {system}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def install_mcp_config() -> tuple[bool, str]:
|
|
31
|
+
"""Install MCP server configuration for Claude Code.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Tuple of (success, message)
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
config_path = get_claude_config_path()
|
|
38
|
+
|
|
39
|
+
# Ensure config directory exists
|
|
40
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
# Load existing config or create new one
|
|
43
|
+
if config_path.exists():
|
|
44
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
45
|
+
config = json.load(f)
|
|
46
|
+
else:
|
|
47
|
+
config = {}
|
|
48
|
+
|
|
49
|
+
# Ensure mcpServers section exists
|
|
50
|
+
if "mcpServers" not in config:
|
|
51
|
+
config["mcpServers"] = {}
|
|
52
|
+
|
|
53
|
+
# Check if ack is already configured
|
|
54
|
+
if "ack" in config["mcpServers"]:
|
|
55
|
+
return (False, "MCP server already configured")
|
|
56
|
+
|
|
57
|
+
# Add ack MCP server configuration
|
|
58
|
+
config["mcpServers"]["ack"] = {
|
|
59
|
+
"command": "ack",
|
|
60
|
+
"args": ["mcp-server"]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Write updated config
|
|
64
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
65
|
+
json.dump(config, f, indent=2)
|
|
66
|
+
|
|
67
|
+
return (True, f"MCP server configured at {config_path}")
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return (False, f"Failed to install MCP config: {e}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def uninstall_mcp_config() -> tuple[bool, str]:
|
|
74
|
+
"""Remove MCP server configuration from Claude Code.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Tuple of (success, message)
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
config_path = get_claude_config_path()
|
|
81
|
+
|
|
82
|
+
if not config_path.exists():
|
|
83
|
+
return (False, "Claude config file not found")
|
|
84
|
+
|
|
85
|
+
# Load existing config
|
|
86
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
87
|
+
config = json.load(f)
|
|
88
|
+
|
|
89
|
+
# Check if ack is configured
|
|
90
|
+
if "mcpServers" not in config or "ack" not in config["mcpServers"]:
|
|
91
|
+
return (False, "MCP server not configured")
|
|
92
|
+
|
|
93
|
+
# Remove ack configuration
|
|
94
|
+
del config["mcpServers"]["ack"]
|
|
95
|
+
|
|
96
|
+
# Write updated config
|
|
97
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
98
|
+
json.dump(config, f, indent=2)
|
|
99
|
+
|
|
100
|
+
return (True, f"MCP server removed from {config_path}")
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return (False, f"Failed to uninstall MCP config: {e}")
|