hanzo-mcp 0.5.1__py3-none-any.whl → 0.6.1__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 hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/cli.py +32 -0
- hanzo_mcp/dev_server.py +246 -0
- hanzo_mcp/prompts/__init__.py +1 -1
- hanzo_mcp/prompts/project_system.py +43 -7
- hanzo_mcp/server.py +5 -1
- hanzo_mcp/tools/__init__.py +168 -6
- hanzo_mcp/tools/agent/__init__.py +1 -1
- hanzo_mcp/tools/agent/agent.py +401 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -4
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +9 -4
- hanzo_mcp/tools/common/batch_tool.py +3 -5
- hanzo_mcp/tools/common/config_tool.py +1 -1
- hanzo_mcp/tools/common/context.py +1 -1
- hanzo_mcp/tools/common/palette.py +344 -0
- hanzo_mcp/tools/common/palette_loader.py +108 -0
- hanzo_mcp/tools/common/stats.py +261 -0
- hanzo_mcp/tools/common/thinking_tool.py +3 -5
- hanzo_mcp/tools/common/tool_disable.py +144 -0
- hanzo_mcp/tools/common/tool_enable.py +182 -0
- hanzo_mcp/tools/common/tool_list.py +260 -0
- hanzo_mcp/tools/config/__init__.py +10 -0
- hanzo_mcp/tools/config/config_tool.py +212 -0
- hanzo_mcp/tools/config/index_config.py +176 -0
- hanzo_mcp/tools/config/palette_tool.py +166 -0
- hanzo_mcp/tools/database/__init__.py +71 -0
- hanzo_mcp/tools/database/database_manager.py +246 -0
- hanzo_mcp/tools/database/graph.py +482 -0
- hanzo_mcp/tools/database/graph_add.py +257 -0
- hanzo_mcp/tools/database/graph_query.py +536 -0
- hanzo_mcp/tools/database/graph_remove.py +267 -0
- hanzo_mcp/tools/database/graph_search.py +348 -0
- hanzo_mcp/tools/database/graph_stats.py +345 -0
- hanzo_mcp/tools/database/sql.py +411 -0
- hanzo_mcp/tools/database/sql_query.py +229 -0
- hanzo_mcp/tools/database/sql_search.py +296 -0
- hanzo_mcp/tools/database/sql_stats.py +254 -0
- hanzo_mcp/tools/editor/__init__.py +11 -0
- hanzo_mcp/tools/editor/neovim_command.py +272 -0
- hanzo_mcp/tools/editor/neovim_edit.py +290 -0
- hanzo_mcp/tools/editor/neovim_session.py +356 -0
- hanzo_mcp/tools/filesystem/__init__.py +52 -13
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +812 -0
- hanzo_mcp/tools/filesystem/content_replace.py +3 -5
- hanzo_mcp/tools/filesystem/diff.py +193 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
- hanzo_mcp/tools/filesystem/edit.py +3 -5
- hanzo_mcp/tools/filesystem/find.py +443 -0
- hanzo_mcp/tools/filesystem/find_files.py +348 -0
- hanzo_mcp/tools/filesystem/git_search.py +505 -0
- hanzo_mcp/tools/filesystem/grep.py +2 -2
- hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
- hanzo_mcp/tools/filesystem/read.py +17 -5
- hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
- hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
- hanzo_mcp/tools/filesystem/tree.py +268 -0
- hanzo_mcp/tools/filesystem/unified_search.py +465 -443
- hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
- hanzo_mcp/tools/filesystem/watch.py +174 -0
- hanzo_mcp/tools/filesystem/write.py +3 -5
- hanzo_mcp/tools/jupyter/__init__.py +9 -12
- hanzo_mcp/tools/jupyter/base.py +1 -1
- hanzo_mcp/tools/jupyter/jupyter.py +326 -0
- hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
- hanzo_mcp/tools/llm/__init__.py +31 -0
- hanzo_mcp/tools/llm/consensus_tool.py +351 -0
- hanzo_mcp/tools/llm/llm_manage.py +413 -0
- hanzo_mcp/tools/llm/llm_tool.py +346 -0
- hanzo_mcp/tools/llm/llm_unified.py +851 -0
- hanzo_mcp/tools/llm/provider_tools.py +412 -0
- hanzo_mcp/tools/mcp/__init__.py +15 -0
- hanzo_mcp/tools/mcp/mcp_add.py +263 -0
- hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
- hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
- hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
- hanzo_mcp/tools/shell/__init__.py +21 -23
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +303 -0
- hanzo_mcp/tools/shell/bash_unified.py +134 -0
- hanzo_mcp/tools/shell/logs.py +265 -0
- hanzo_mcp/tools/shell/npx.py +194 -0
- hanzo_mcp/tools/shell/npx_background.py +254 -0
- hanzo_mcp/tools/shell/npx_unified.py +101 -0
- hanzo_mcp/tools/shell/open.py +107 -0
- hanzo_mcp/tools/shell/pkill.py +262 -0
- hanzo_mcp/tools/shell/process_unified.py +131 -0
- hanzo_mcp/tools/shell/processes.py +279 -0
- hanzo_mcp/tools/shell/run_background.py +326 -0
- hanzo_mcp/tools/shell/run_command.py +3 -4
- hanzo_mcp/tools/shell/run_command_windows.py +3 -4
- hanzo_mcp/tools/shell/uvx.py +187 -0
- hanzo_mcp/tools/shell/uvx_background.py +249 -0
- hanzo_mcp/tools/shell/uvx_unified.py +101 -0
- hanzo_mcp/tools/todo/__init__.py +1 -1
- hanzo_mcp/tools/todo/base.py +1 -1
- hanzo_mcp/tools/todo/todo.py +265 -0
- hanzo_mcp/tools/todo/todo_read.py +3 -5
- hanzo_mcp/tools/todo/todo_write.py +3 -5
- hanzo_mcp/tools/vector/__init__.py +6 -1
- hanzo_mcp/tools/vector/git_ingester.py +3 -0
- hanzo_mcp/tools/vector/index_tool.py +358 -0
- hanzo_mcp/tools/vector/infinity_store.py +98 -0
- hanzo_mcp/tools/vector/project_manager.py +27 -5
- hanzo_mcp/tools/vector/vector.py +311 -0
- hanzo_mcp/tools/vector/vector_index.py +1 -1
- hanzo_mcp/tools/vector/vector_search.py +12 -7
- hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
- hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
- hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
- hanzo_mcp-0.5.1.dist-info/METADATA +0 -276
- hanzo_mcp-0.5.1.dist-info/RECORD +0 -68
- hanzo_mcp-0.5.1.dist-info/entry_points.txt +0 -2
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Tool for managing development tool palettes."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, override
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
6
|
+
|
|
7
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
8
|
+
from hanzo_mcp.tools.common.palette import PaletteRegistry, register_default_palettes
|
|
9
|
+
from mcp.server import FastMCP
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PaletteTool(BaseTool):
|
|
13
|
+
"""Tool for managing tool palettes."""
|
|
14
|
+
|
|
15
|
+
name = "palette"
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
"""Initialize the palette tool."""
|
|
19
|
+
super().__init__()
|
|
20
|
+
# Register default palettes on initialization
|
|
21
|
+
register_default_palettes()
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@override
|
|
25
|
+
def description(self) -> str:
|
|
26
|
+
"""Get the tool description."""
|
|
27
|
+
return """Manage tool palettes. Actions: list (default), activate, show, current.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
palette
|
|
31
|
+
palette --action list
|
|
32
|
+
palette --action activate python
|
|
33
|
+
palette --action show javascript
|
|
34
|
+
palette --action current"""
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
async def run(
|
|
38
|
+
self,
|
|
39
|
+
ctx: MCPContext,
|
|
40
|
+
action: str = "list",
|
|
41
|
+
name: Optional[str] = None,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Manage tool palettes.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
ctx: MCP context
|
|
47
|
+
action: Action to perform (list, activate, show, current)
|
|
48
|
+
name: Palette name (for activate/show actions)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Action result
|
|
52
|
+
"""
|
|
53
|
+
if action == "list":
|
|
54
|
+
palettes = PaletteRegistry.list()
|
|
55
|
+
if not palettes:
|
|
56
|
+
return "No palettes registered"
|
|
57
|
+
|
|
58
|
+
output = ["Available tool palettes:"]
|
|
59
|
+
active = PaletteRegistry.get_active()
|
|
60
|
+
|
|
61
|
+
for palette in sorted(palettes, key=lambda p: p.name):
|
|
62
|
+
marker = " (active)" if active and active.name == palette.name else ""
|
|
63
|
+
author = f" by {palette.author}" if palette.author else ""
|
|
64
|
+
output.append(f"\n{palette.name}{marker}:")
|
|
65
|
+
output.append(f" {palette.description}{author}")
|
|
66
|
+
output.append(f" Tools: {len(palette.tools)} enabled")
|
|
67
|
+
|
|
68
|
+
return "\n".join(output)
|
|
69
|
+
|
|
70
|
+
elif action == "activate":
|
|
71
|
+
if not name:
|
|
72
|
+
return "Error: Palette name required for activate action"
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
PaletteRegistry.set_active(name)
|
|
76
|
+
palette = PaletteRegistry.get(name)
|
|
77
|
+
|
|
78
|
+
output = [f"Activated palette: {palette.name}"]
|
|
79
|
+
if palette.author:
|
|
80
|
+
output.append(f"Author: {palette.author}")
|
|
81
|
+
output.append(f"Description: {palette.description}")
|
|
82
|
+
output.append(f"\nEnabled tools ({len(palette.tools)}):")
|
|
83
|
+
|
|
84
|
+
# Group tools by category
|
|
85
|
+
core_tools = []
|
|
86
|
+
package_tools = []
|
|
87
|
+
other_tools = []
|
|
88
|
+
|
|
89
|
+
for tool in sorted(palette.tools):
|
|
90
|
+
if tool in ["read", "write", "edit", "grep", "tree", "find", "bash"]:
|
|
91
|
+
core_tools.append(tool)
|
|
92
|
+
elif tool in ["npx", "uvx", "pip", "cargo", "gem"]:
|
|
93
|
+
package_tools.append(tool)
|
|
94
|
+
else:
|
|
95
|
+
other_tools.append(tool)
|
|
96
|
+
|
|
97
|
+
if core_tools:
|
|
98
|
+
output.append(f" Core: {', '.join(core_tools)}")
|
|
99
|
+
if package_tools:
|
|
100
|
+
output.append(f" Package managers: {', '.join(package_tools)}")
|
|
101
|
+
if other_tools:
|
|
102
|
+
output.append(f" Specialized: {', '.join(other_tools)}")
|
|
103
|
+
|
|
104
|
+
if palette.environment:
|
|
105
|
+
output.append("\nEnvironment variables:")
|
|
106
|
+
for key, value in palette.environment.items():
|
|
107
|
+
output.append(f" {key}={value}")
|
|
108
|
+
|
|
109
|
+
output.append("\nNote: Restart MCP session for changes to take full effect")
|
|
110
|
+
|
|
111
|
+
return "\n".join(output)
|
|
112
|
+
|
|
113
|
+
except ValueError as e:
|
|
114
|
+
return str(e)
|
|
115
|
+
|
|
116
|
+
elif action == "show":
|
|
117
|
+
if not name:
|
|
118
|
+
return "Error: Palette name required for show action"
|
|
119
|
+
|
|
120
|
+
palette = PaletteRegistry.get(name)
|
|
121
|
+
if not palette:
|
|
122
|
+
return f"Palette '{name}' not found"
|
|
123
|
+
|
|
124
|
+
output = [f"Palette: {palette.name}"]
|
|
125
|
+
if palette.author:
|
|
126
|
+
output.append(f"Author: {palette.author}")
|
|
127
|
+
output.append(f"Description: {palette.description}")
|
|
128
|
+
output.append(f"\nTools ({len(palette.tools)}):")
|
|
129
|
+
|
|
130
|
+
for tool in sorted(palette.tools):
|
|
131
|
+
output.append(f" - {tool}")
|
|
132
|
+
|
|
133
|
+
if palette.environment:
|
|
134
|
+
output.append("\nEnvironment:")
|
|
135
|
+
for key, value in palette.environment.items():
|
|
136
|
+
output.append(f" {key}={value}")
|
|
137
|
+
|
|
138
|
+
return "\n".join(output)
|
|
139
|
+
|
|
140
|
+
elif action == "current":
|
|
141
|
+
active = PaletteRegistry.get_active()
|
|
142
|
+
if not active:
|
|
143
|
+
return "No palette currently active\nUse 'palette --action activate <name>' to activate one"
|
|
144
|
+
|
|
145
|
+
output = [f"Current palette: {active.name}"]
|
|
146
|
+
if active.author:
|
|
147
|
+
output.append(f"Author: {active.author}")
|
|
148
|
+
output.append(f"Description: {active.description}")
|
|
149
|
+
output.append(f"Enabled tools: {len(active.tools)}")
|
|
150
|
+
|
|
151
|
+
return "\n".join(output)
|
|
152
|
+
|
|
153
|
+
else:
|
|
154
|
+
return f"Unknown action: {action}. Use 'list', 'activate', 'show', or 'current'"
|
|
155
|
+
|
|
156
|
+
def register(self, server: FastMCP) -> None:
|
|
157
|
+
"""Register the tool with the MCP server."""
|
|
158
|
+
server.tool(name=self.name, description=self.description)(self.call)
|
|
159
|
+
|
|
160
|
+
async def call(self, **kwargs) -> str:
|
|
161
|
+
"""Call the tool with arguments."""
|
|
162
|
+
return await self.run(None, **kwargs)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Create tool instance
|
|
166
|
+
palette_tool = PaletteTool()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Database tools for Hanzo MCP.
|
|
2
|
+
|
|
3
|
+
This package provides tools for working with embedded SQLite databases
|
|
4
|
+
and graph databases in projects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
8
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
9
|
+
from mcp.server import FastMCP
|
|
10
|
+
|
|
11
|
+
# Import database tools
|
|
12
|
+
from .sql_query import SqlQueryTool
|
|
13
|
+
from .sql_search import SqlSearchTool
|
|
14
|
+
from .sql_stats import SqlStatsTool
|
|
15
|
+
from .graph_add import GraphAddTool
|
|
16
|
+
from .graph_remove import GraphRemoveTool
|
|
17
|
+
from .graph_query import GraphQueryTool
|
|
18
|
+
from .graph_search import GraphSearchTool
|
|
19
|
+
from .graph_stats import GraphStatsTool
|
|
20
|
+
from .database_manager import DatabaseManager
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"register_database_tools",
|
|
24
|
+
"DatabaseManager",
|
|
25
|
+
"SqlQueryTool",
|
|
26
|
+
"SqlSearchTool",
|
|
27
|
+
"SqlStatsTool",
|
|
28
|
+
"GraphAddTool",
|
|
29
|
+
"GraphRemoveTool",
|
|
30
|
+
"GraphQueryTool",
|
|
31
|
+
"GraphSearchTool",
|
|
32
|
+
"GraphStatsTool",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def register_database_tools(
|
|
37
|
+
mcp_server: FastMCP,
|
|
38
|
+
permission_manager: PermissionManager,
|
|
39
|
+
db_manager: DatabaseManager | None = None,
|
|
40
|
+
) -> list[BaseTool]:
|
|
41
|
+
"""Register database tools with the MCP server.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
mcp_server: The FastMCP server instance
|
|
45
|
+
permission_manager: Permission manager for access control
|
|
46
|
+
db_manager: Optional database manager instance
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of registered tools
|
|
50
|
+
"""
|
|
51
|
+
# Create database manager if not provided
|
|
52
|
+
if db_manager is None:
|
|
53
|
+
db_manager = DatabaseManager(permission_manager)
|
|
54
|
+
|
|
55
|
+
# Create tool instances
|
|
56
|
+
tools = [
|
|
57
|
+
SqlQueryTool(permission_manager, db_manager),
|
|
58
|
+
SqlSearchTool(permission_manager, db_manager),
|
|
59
|
+
SqlStatsTool(permission_manager, db_manager),
|
|
60
|
+
GraphAddTool(permission_manager, db_manager),
|
|
61
|
+
GraphRemoveTool(permission_manager, db_manager),
|
|
62
|
+
GraphQueryTool(permission_manager, db_manager),
|
|
63
|
+
GraphSearchTool(permission_manager, db_manager),
|
|
64
|
+
GraphStatsTool(permission_manager, db_manager),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# Register with MCP server
|
|
68
|
+
from hanzo_mcp.tools.common.base import ToolRegistry
|
|
69
|
+
ToolRegistry.register_tools(mcp_server, tools)
|
|
70
|
+
|
|
71
|
+
return tools
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Database manager for project-specific SQLite and graph databases."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any, Optional, Tuple, List
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProjectDatabase:
|
|
14
|
+
"""Manages SQLite and graph databases for a project."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, project_path: str):
|
|
17
|
+
self.project_path = Path(project_path)
|
|
18
|
+
self.db_dir = self.project_path / ".hanzo" / "db"
|
|
19
|
+
self.db_dir.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
|
|
21
|
+
# SQLite database path
|
|
22
|
+
self.sqlite_path = self.db_dir / "project.db"
|
|
23
|
+
self.graph_path = self.db_dir / "graph.db"
|
|
24
|
+
|
|
25
|
+
# Initialize databases
|
|
26
|
+
self._init_sqlite()
|
|
27
|
+
self._init_graph()
|
|
28
|
+
|
|
29
|
+
# Keep graph in memory for performance
|
|
30
|
+
self.graph_conn = sqlite3.connect(":memory:")
|
|
31
|
+
self._init_graph_schema(self.graph_conn)
|
|
32
|
+
self._load_graph_from_disk()
|
|
33
|
+
|
|
34
|
+
def _init_sqlite(self):
|
|
35
|
+
"""Initialize SQLite database with common tables."""
|
|
36
|
+
conn = sqlite3.connect(self.sqlite_path)
|
|
37
|
+
try:
|
|
38
|
+
# Create metadata table
|
|
39
|
+
conn.execute('''
|
|
40
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
41
|
+
key TEXT PRIMARY KEY,
|
|
42
|
+
value TEXT,
|
|
43
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
44
|
+
)
|
|
45
|
+
''')
|
|
46
|
+
|
|
47
|
+
# Create files table
|
|
48
|
+
conn.execute('''
|
|
49
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
50
|
+
path TEXT PRIMARY KEY,
|
|
51
|
+
content TEXT,
|
|
52
|
+
size INTEGER,
|
|
53
|
+
modified_at TIMESTAMP,
|
|
54
|
+
hash TEXT,
|
|
55
|
+
metadata TEXT
|
|
56
|
+
)
|
|
57
|
+
''')
|
|
58
|
+
|
|
59
|
+
# Create symbols table
|
|
60
|
+
conn.execute('''
|
|
61
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
file_path TEXT,
|
|
64
|
+
name TEXT,
|
|
65
|
+
type TEXT,
|
|
66
|
+
line_start INTEGER,
|
|
67
|
+
line_end INTEGER,
|
|
68
|
+
scope TEXT,
|
|
69
|
+
signature TEXT,
|
|
70
|
+
FOREIGN KEY (file_path) REFERENCES files(path)
|
|
71
|
+
)
|
|
72
|
+
''')
|
|
73
|
+
|
|
74
|
+
# Create index for fast searches
|
|
75
|
+
conn.execute('CREATE INDEX IF NOT EXISTS idx_files_path ON files(path)')
|
|
76
|
+
conn.execute('CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)')
|
|
77
|
+
conn.execute('CREATE INDEX IF NOT EXISTS idx_symbols_type ON symbols(type)')
|
|
78
|
+
|
|
79
|
+
conn.commit()
|
|
80
|
+
finally:
|
|
81
|
+
conn.close()
|
|
82
|
+
|
|
83
|
+
def _init_graph(self):
|
|
84
|
+
"""Initialize graph database on disk."""
|
|
85
|
+
conn = sqlite3.connect(self.graph_path)
|
|
86
|
+
try:
|
|
87
|
+
self._init_graph_schema(conn)
|
|
88
|
+
conn.commit()
|
|
89
|
+
finally:
|
|
90
|
+
conn.close()
|
|
91
|
+
|
|
92
|
+
def _init_graph_schema(self, conn: sqlite3.Connection):
|
|
93
|
+
"""Initialize graph database schema."""
|
|
94
|
+
# Nodes table
|
|
95
|
+
conn.execute('''
|
|
96
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
97
|
+
id TEXT PRIMARY KEY,
|
|
98
|
+
type TEXT NOT NULL,
|
|
99
|
+
properties TEXT,
|
|
100
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
101
|
+
)
|
|
102
|
+
''')
|
|
103
|
+
|
|
104
|
+
# Edges table
|
|
105
|
+
conn.execute('''
|
|
106
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
107
|
+
source TEXT NOT NULL,
|
|
108
|
+
target TEXT NOT NULL,
|
|
109
|
+
relationship TEXT NOT NULL,
|
|
110
|
+
weight REAL DEFAULT 1.0,
|
|
111
|
+
properties TEXT,
|
|
112
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
113
|
+
PRIMARY KEY (source, target, relationship),
|
|
114
|
+
FOREIGN KEY (source) REFERENCES nodes(id),
|
|
115
|
+
FOREIGN KEY (target) REFERENCES nodes(id)
|
|
116
|
+
)
|
|
117
|
+
''')
|
|
118
|
+
|
|
119
|
+
# Indexes for graph traversal
|
|
120
|
+
conn.execute('CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source)')
|
|
121
|
+
conn.execute('CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target)')
|
|
122
|
+
conn.execute('CREATE INDEX IF NOT EXISTS idx_edges_relationship ON edges(relationship)')
|
|
123
|
+
conn.execute('CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type)')
|
|
124
|
+
|
|
125
|
+
def _load_graph_from_disk(self):
|
|
126
|
+
"""Load graph from disk into memory."""
|
|
127
|
+
disk_conn = sqlite3.connect(self.graph_path)
|
|
128
|
+
try:
|
|
129
|
+
# Copy nodes
|
|
130
|
+
nodes = disk_conn.execute('SELECT * FROM nodes').fetchall()
|
|
131
|
+
self.graph_conn.executemany(
|
|
132
|
+
'INSERT OR REPLACE INTO nodes VALUES (?, ?, ?, ?)',
|
|
133
|
+
nodes
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Copy edges
|
|
137
|
+
edges = disk_conn.execute('SELECT * FROM edges').fetchall()
|
|
138
|
+
self.graph_conn.executemany(
|
|
139
|
+
'INSERT OR REPLACE INTO edges VALUES (?, ?, ?, ?, ?, ?)',
|
|
140
|
+
edges
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
self.graph_conn.commit()
|
|
144
|
+
finally:
|
|
145
|
+
disk_conn.close()
|
|
146
|
+
|
|
147
|
+
def _save_graph_to_disk(self):
|
|
148
|
+
"""Save in-memory graph to disk."""
|
|
149
|
+
disk_conn = sqlite3.connect(self.graph_path)
|
|
150
|
+
try:
|
|
151
|
+
# Clear existing data
|
|
152
|
+
disk_conn.execute('DELETE FROM edges')
|
|
153
|
+
disk_conn.execute('DELETE FROM nodes')
|
|
154
|
+
|
|
155
|
+
# Copy nodes
|
|
156
|
+
nodes = self.graph_conn.execute('SELECT * FROM nodes').fetchall()
|
|
157
|
+
disk_conn.executemany(
|
|
158
|
+
'INSERT INTO nodes VALUES (?, ?, ?, ?)',
|
|
159
|
+
nodes
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Copy edges
|
|
163
|
+
edges = self.graph_conn.execute('SELECT * FROM edges').fetchall()
|
|
164
|
+
disk_conn.executemany(
|
|
165
|
+
'INSERT INTO edges VALUES (?, ?, ?, ?, ?, ?)',
|
|
166
|
+
edges
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
disk_conn.commit()
|
|
170
|
+
finally:
|
|
171
|
+
disk_conn.close()
|
|
172
|
+
|
|
173
|
+
def get_sqlite_connection(self) -> sqlite3.Connection:
|
|
174
|
+
"""Get SQLite connection."""
|
|
175
|
+
conn = sqlite3.connect(self.sqlite_path)
|
|
176
|
+
conn.row_factory = sqlite3.Row
|
|
177
|
+
return conn
|
|
178
|
+
|
|
179
|
+
def get_graph_connection(self) -> sqlite3.Connection:
|
|
180
|
+
"""Get in-memory graph connection."""
|
|
181
|
+
return self.graph_conn
|
|
182
|
+
|
|
183
|
+
def close(self):
|
|
184
|
+
"""Close connections and save graph to disk."""
|
|
185
|
+
self._save_graph_to_disk()
|
|
186
|
+
self.graph_conn.close()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class DatabaseManager:
|
|
190
|
+
"""Manages databases for multiple projects."""
|
|
191
|
+
|
|
192
|
+
def __init__(self, permission_manager: PermissionManager):
|
|
193
|
+
self.permission_manager = permission_manager
|
|
194
|
+
self.projects: Dict[str, ProjectDatabase] = {}
|
|
195
|
+
self.search_paths: List[str] = []
|
|
196
|
+
|
|
197
|
+
def add_search_path(self, path: str):
|
|
198
|
+
"""Add a path to search for projects."""
|
|
199
|
+
if path not in self.search_paths:
|
|
200
|
+
self.search_paths.append(path)
|
|
201
|
+
|
|
202
|
+
def get_project_db(self, project_path: str) -> ProjectDatabase:
|
|
203
|
+
"""Get or create database for a project."""
|
|
204
|
+
project_path = os.path.abspath(project_path)
|
|
205
|
+
|
|
206
|
+
# Check permissions
|
|
207
|
+
if not self.permission_manager.has_permission(project_path):
|
|
208
|
+
raise PermissionError(f"No permission to access: {project_path}")
|
|
209
|
+
|
|
210
|
+
# Create database if not exists
|
|
211
|
+
if project_path not in self.projects:
|
|
212
|
+
self.projects[project_path] = ProjectDatabase(project_path)
|
|
213
|
+
|
|
214
|
+
return self.projects[project_path]
|
|
215
|
+
|
|
216
|
+
def get_project_for_path(self, file_path: str) -> Optional[ProjectDatabase]:
|
|
217
|
+
"""Find the project database for a given file path."""
|
|
218
|
+
file_path = os.path.abspath(file_path)
|
|
219
|
+
|
|
220
|
+
# Check if file is in a known project
|
|
221
|
+
for project_path in self.projects:
|
|
222
|
+
if file_path.startswith(project_path):
|
|
223
|
+
return self.projects[project_path]
|
|
224
|
+
|
|
225
|
+
# Search up the directory tree for a project
|
|
226
|
+
current = Path(file_path)
|
|
227
|
+
if current.is_file():
|
|
228
|
+
current = current.parent
|
|
229
|
+
|
|
230
|
+
while current != current.parent:
|
|
231
|
+
# Check for project markers
|
|
232
|
+
if (current / ".git").exists() or (current / "LLM.md").exists():
|
|
233
|
+
return self.get_project_db(str(current))
|
|
234
|
+
current = current.parent
|
|
235
|
+
|
|
236
|
+
# No project found, use the directory of the file
|
|
237
|
+
if Path(file_path).is_file():
|
|
238
|
+
return self.get_project_db(str(Path(file_path).parent))
|
|
239
|
+
else:
|
|
240
|
+
return self.get_project_db(file_path)
|
|
241
|
+
|
|
242
|
+
def close_all(self):
|
|
243
|
+
"""Close all project databases."""
|
|
244
|
+
for db in self.projects.values():
|
|
245
|
+
db.close()
|
|
246
|
+
self.projects.clear()
|