hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.0__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.

Files changed (178) hide show
  1. hanzo_mcp/__init__.py +6 -0
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
@@ -1,110 +1,109 @@
1
1
  """Mock implementation of infinity_embedded for testing on unsupported platforms."""
2
2
 
3
- import json
4
- import hashlib
5
3
  import random
4
+ from typing import Any, Dict, List
6
5
  from pathlib import Path
7
- from typing import Dict, List, Any, Optional
8
- from datetime import datetime
9
6
 
10
7
 
11
8
  class MockTable:
12
9
  """Mock implementation of an Infinity table."""
13
-
10
+
14
11
  def __init__(self, name: str, schema: Dict[str, Any]):
15
12
  self.name = name
16
13
  self.schema = schema
17
14
  self.data = []
18
15
  self._id_counter = 0
19
-
16
+
20
17
  def insert(self, records: List[Dict[str, Any]]):
21
18
  """Insert records into the table."""
22
19
  for record in records:
23
20
  # Add an internal ID if not present
24
- if 'id' not in record:
25
- record['_internal_id'] = self._id_counter
21
+ if "id" not in record:
22
+ record["_internal_id"] = self._id_counter
26
23
  self._id_counter += 1
27
24
  self.data.append(record)
28
-
25
+
29
26
  def delete(self, condition: str):
30
27
  """Delete records matching condition."""
31
28
  # Simple implementation - just clear for now
32
29
  self.data = [r for r in self.data if not self._eval_condition(r, condition)]
33
-
30
+
34
31
  def output(self, columns: List[str]):
35
32
  """Start a query chain."""
36
33
  return MockQuery(self, columns)
37
-
34
+
38
35
  def _eval_condition(self, record: Dict[str, Any], condition: str) -> bool:
39
36
  """Evaluate a simple condition."""
40
37
  # Very basic implementation
41
- if '=' in condition:
42
- field, value = condition.split('=', 1)
38
+ if "=" in condition:
39
+ field, value = condition.split("=", 1)
43
40
  field = field.strip()
44
41
  value = value.strip().strip("'\"")
45
- return str(record.get(field, '')) == value
42
+ return str(record.get(field, "")) == value
46
43
  return False
47
44
 
48
45
 
49
46
  class MockQuery:
50
47
  """Mock query builder."""
51
-
48
+
52
49
  def __init__(self, table: MockTable, columns: List[str]):
53
50
  self.table = table
54
51
  self.columns = columns
55
52
  self.filters = []
56
53
  self.vector_search = None
57
54
  self.limit_value = None
58
-
55
+
59
56
  def filter(self, condition: str):
60
57
  """Add a filter condition."""
61
58
  self.filters.append(condition)
62
59
  return self
63
-
64
- def match_dense(self, column: str, vector: List[float], dtype: str, metric: str, limit: int):
60
+
61
+ def match_dense(
62
+ self, column: str, vector: List[float], dtype: str, metric: str, limit: int
63
+ ):
65
64
  """Add vector search."""
66
65
  self.vector_search = {
67
- 'column': column,
68
- 'vector': vector,
69
- 'dtype': dtype,
70
- 'metric': metric,
71
- 'limit': limit
66
+ "column": column,
67
+ "vector": vector,
68
+ "dtype": dtype,
69
+ "metric": metric,
70
+ "limit": limit,
72
71
  }
73
72
  self.limit_value = limit
74
73
  return self
75
-
74
+
76
75
  def to_pl(self):
77
76
  """Execute query and return polars-like result."""
78
77
  results = self.table.data.copy()
79
-
78
+
80
79
  # Apply filters
81
80
  for condition in self.filters:
82
81
  results = [r for r in results if self.table._eval_condition(r, condition)]
83
-
82
+
84
83
  # Apply vector search (mock similarity)
85
84
  if self.vector_search:
86
85
  # Add mock scores
87
86
  for r in results:
88
- r['score'] = random.uniform(0.5, 1.0)
87
+ r["score"] = random.uniform(0.5, 1.0)
89
88
  # Sort by score
90
- results.sort(key=lambda x: x.get('score', 0), reverse=True)
89
+ results.sort(key=lambda x: x.get("score", 0), reverse=True)
91
90
  # Limit results
92
91
  if self.limit_value:
93
- results = results[:self.limit_value]
94
-
92
+ results = results[: self.limit_value]
93
+
95
94
  # Return mock polars DataFrame
96
95
  return MockDataFrame(results)
97
96
 
98
97
 
99
98
  class MockDataFrame:
100
99
  """Mock polars DataFrame."""
101
-
100
+
102
101
  def __init__(self, data: List[Dict[str, Any]]):
103
102
  self.data = data
104
-
103
+
105
104
  def __len__(self):
106
105
  return len(self.data)
107
-
106
+
108
107
  def iter_rows(self, named: bool = False):
109
108
  """Iterate over rows."""
110
109
  if named:
@@ -119,17 +118,17 @@ class MockDataFrame:
119
118
 
120
119
  class MockDatabase:
121
120
  """Mock implementation of an Infinity database."""
122
-
121
+
123
122
  def __init__(self, name: str):
124
123
  self.name = name
125
124
  self.tables = {}
126
-
125
+
127
126
  def create_table(self, name: str, schema: Dict[str, Any]) -> MockTable:
128
127
  """Create a new table."""
129
128
  table = MockTable(name, schema)
130
129
  self.tables[name] = table
131
130
  return table
132
-
131
+
133
132
  def get_table(self, name: str) -> MockTable:
134
133
  """Get an existing table."""
135
134
  if name not in self.tables:
@@ -139,19 +138,19 @@ class MockDatabase:
139
138
 
140
139
  class MockInfinity:
141
140
  """Mock implementation of Infinity connection."""
142
-
141
+
143
142
  def __init__(self, path: str):
144
143
  self.path = Path(path)
145
144
  self.databases = {}
146
145
  # Ensure directory exists
147
146
  self.path.mkdir(parents=True, exist_ok=True)
148
-
147
+
149
148
  def get_database(self, name: str) -> MockDatabase:
150
149
  """Get or create a database."""
151
150
  if name not in self.databases:
152
151
  self.databases[name] = MockDatabase(name)
153
152
  return self.databases[name]
154
-
153
+
155
154
  def disconnect(self):
156
155
  """Disconnect from Infinity."""
157
156
  pass
@@ -159,4 +158,4 @@ class MockInfinity:
159
158
 
160
159
  def connect(path: str) -> MockInfinity:
161
160
  """Connect to Infinity (mock implementation)."""
162
- return MockInfinity(path)
161
+ return MockInfinity(path)
@@ -1,19 +1,20 @@
1
1
  """Project-aware vector database management for Hanzo AI."""
2
2
 
3
- import os
3
+ import asyncio
4
+ from typing import Any, Dict, List, Tuple, Optional
4
5
  from pathlib import Path
5
- from typing import Dict, List, Optional, Set, Tuple, Any
6
6
  from dataclasses import dataclass
7
- import asyncio
8
7
  from concurrent.futures import ThreadPoolExecutor
9
8
 
10
- from .infinity_store import InfinityVectorStore, SearchResult
11
- from hanzo_mcp.tools.config.index_config import IndexConfig, IndexScope
9
+ from hanzo_mcp.tools.config.index_config import IndexScope, IndexConfig
10
+
11
+ from .infinity_store import SearchResult, InfinityVectorStore
12
12
 
13
13
 
14
14
  @dataclass
15
15
  class ProjectInfo:
16
16
  """Information about a detected project."""
17
+
17
18
  root_path: Path
18
19
  llm_md_path: Path
19
20
  db_path: Path
@@ -22,7 +23,7 @@ class ProjectInfo:
22
23
 
23
24
  class ProjectVectorManager:
24
25
  """Manages project-aware vector databases."""
25
-
26
+
26
27
  def __init__(
27
28
  self,
28
29
  global_db_path: Optional[str] = None,
@@ -30,7 +31,7 @@ class ProjectVectorManager:
30
31
  dimension: int = 1536,
31
32
  ):
32
33
  """Initialize the project vector manager.
33
-
34
+
34
35
  Args:
35
36
  global_db_path: Path for global vector store (default: ~/.config/hanzo/db)
36
37
  embedding_model: Embedding model to use
@@ -38,26 +39,26 @@ class ProjectVectorManager:
38
39
  """
39
40
  self.embedding_model = embedding_model
40
41
  self.dimension = dimension
41
-
42
+
42
43
  # Set up index configuration
43
44
  self.index_config = IndexConfig()
44
-
45
+
45
46
  # Set up global database path
46
47
  if global_db_path:
47
48
  self.global_db_path = Path(global_db_path)
48
49
  else:
49
50
  self.global_db_path = self.index_config.get_index_path("vector")
50
-
51
+
51
52
  self.global_db_path.mkdir(parents=True, exist_ok=True)
52
-
53
+
53
54
  # Cache for project info and vector stores
54
55
  self.projects: Dict[str, ProjectInfo] = {}
55
56
  self.vector_stores: Dict[str, InfinityVectorStore] = {}
56
57
  self._global_store: Optional[InfinityVectorStore] = None
57
-
58
+
58
59
  # Thread pool for parallel operations
59
60
  self.executor = ThreadPoolExecutor(max_workers=4)
60
-
61
+
61
62
  def _get_global_store(self) -> InfinityVectorStore:
62
63
  """Get or create the global vector store."""
63
64
  if self._global_store is None:
@@ -67,56 +68,56 @@ class ProjectVectorManager:
67
68
  dimension=self.dimension,
68
69
  )
69
70
  return self._global_store
70
-
71
+
71
72
  def detect_projects(self, search_paths: List[str]) -> List[ProjectInfo]:
72
73
  """Detect projects by finding LLM.md files.
73
-
74
+
74
75
  Args:
75
76
  search_paths: List of paths to search for projects
76
-
77
+
77
78
  Returns:
78
79
  List of detected project information
79
80
  """
80
81
  projects = []
81
-
82
+
82
83
  for search_path in search_paths:
83
84
  path = Path(search_path).resolve()
84
-
85
+
85
86
  # Search for LLM.md files
86
87
  for llm_md_path in path.rglob("LLM.md"):
87
88
  project_root = llm_md_path.parent
88
89
  project_name = project_root.name
89
-
90
+
90
91
  # Create .hanzo/db directory in project
91
92
  db_path = project_root / ".hanzo" / "db"
92
93
  db_path.mkdir(parents=True, exist_ok=True)
93
-
94
+
94
95
  project_info = ProjectInfo(
95
96
  root_path=project_root,
96
97
  llm_md_path=llm_md_path,
97
98
  db_path=db_path,
98
99
  name=project_name,
99
100
  )
100
-
101
+
101
102
  projects.append(project_info)
102
-
103
+
103
104
  # Cache project info
104
105
  project_key = str(project_root)
105
106
  self.projects[project_key] = project_info
106
-
107
+
107
108
  return projects
108
-
109
+
109
110
  def get_project_for_path(self, file_path: str) -> Optional[ProjectInfo]:
110
111
  """Find the project that contains a given file path.
111
-
112
+
112
113
  Args:
113
114
  file_path: File path to check
114
-
115
+
115
116
  Returns:
116
117
  Project info if found, None otherwise
117
118
  """
118
119
  path = Path(file_path).resolve()
119
-
120
+
120
121
  # Check each known project
121
122
  for project_key, project_info in self.projects.items():
122
123
  try:
@@ -126,38 +127,40 @@ class ProjectVectorManager:
126
127
  except ValueError:
127
128
  # Path is not within this project
128
129
  continue
129
-
130
+
130
131
  # Try to find project by walking up the directory tree
131
132
  current_path = path.parent if path.is_file() else path
132
-
133
+
133
134
  while current_path != current_path.parent: # Stop at filesystem root
134
135
  llm_md_path = current_path / "LLM.md"
135
136
  if llm_md_path.exists():
136
137
  # Found a project, create and cache it
137
138
  db_path = current_path / ".hanzo" / "db"
138
139
  db_path.mkdir(parents=True, exist_ok=True)
139
-
140
+
140
141
  project_info = ProjectInfo(
141
142
  root_path=current_path,
142
143
  llm_md_path=llm_md_path,
143
144
  db_path=db_path,
144
145
  name=current_path.name,
145
146
  )
146
-
147
+
147
148
  project_key = str(current_path)
148
149
  self.projects[project_key] = project_info
149
150
  return project_info
150
-
151
+
151
152
  current_path = current_path.parent
152
-
153
+
153
154
  return None
154
-
155
- def get_vector_store(self, project_info: Optional[ProjectInfo] = None) -> InfinityVectorStore:
155
+
156
+ def get_vector_store(
157
+ self, project_info: Optional[ProjectInfo] = None
158
+ ) -> InfinityVectorStore:
156
159
  """Get vector store for a project or global store.
157
-
160
+
158
161
  Args:
159
162
  project_info: Project to get store for, None for global store
160
-
163
+
161
164
  Returns:
162
165
  Vector store instance
163
166
  """
@@ -169,23 +172,25 @@ class ProjectVectorManager:
169
172
  return self._get_global_store()
170
173
  else:
171
174
  return self._get_global_store()
172
-
175
+
173
176
  # Use project-specific store
174
177
  project_key = str(project_info.root_path)
175
-
178
+
176
179
  if project_key not in self.vector_stores:
177
180
  # Get index path based on configuration
178
- index_path = self.index_config.get_index_path("vector", str(project_info.root_path))
181
+ index_path = self.index_config.get_index_path(
182
+ "vector", str(project_info.root_path)
183
+ )
179
184
  index_path.mkdir(parents=True, exist_ok=True)
180
-
185
+
181
186
  self.vector_stores[project_key] = InfinityVectorStore(
182
187
  data_path=str(index_path),
183
188
  embedding_model=self.embedding_model,
184
189
  dimension=self.dimension,
185
190
  )
186
-
191
+
187
192
  return self.vector_stores[project_key]
188
-
193
+
189
194
  def add_file_to_appropriate_store(
190
195
  self,
191
196
  file_path: str,
@@ -194,26 +199,26 @@ class ProjectVectorManager:
194
199
  metadata: Dict[str, Any] = None,
195
200
  ) -> Tuple[List[str], Optional[ProjectInfo]]:
196
201
  """Add a file to the appropriate vector store (project or global).
197
-
202
+
198
203
  Args:
199
204
  file_path: Path to file to add
200
205
  chunk_size: Chunk size for text splitting
201
206
  chunk_overlap: Overlap between chunks
202
207
  metadata: Additional metadata
203
-
208
+
204
209
  Returns:
205
210
  Tuple of (document IDs, project info or None for global)
206
211
  """
207
212
  # Check if indexing is enabled
208
213
  if not self.index_config.is_indexing_enabled("vector"):
209
214
  return [], None
210
-
215
+
211
216
  # Find project for this file
212
217
  project_info = self.get_project_for_path(file_path)
213
-
218
+
214
219
  # Get appropriate vector store based on scope configuration
215
220
  vector_store = self.get_vector_store(project_info)
216
-
221
+
217
222
  # Add file metadata
218
223
  file_metadata = metadata or {}
219
224
  if project_info:
@@ -225,7 +230,7 @@ class ProjectVectorManager:
225
230
  else:
226
231
  file_metadata["project_name"] = "global"
227
232
  file_metadata["index_scope"] = "global"
228
-
233
+
229
234
  # Add file to store
230
235
  doc_ids = vector_store.add_file(
231
236
  file_path=file_path,
@@ -233,9 +238,9 @@ class ProjectVectorManager:
233
238
  chunk_overlap=chunk_overlap,
234
239
  metadata=file_metadata,
235
240
  )
236
-
241
+
237
242
  return doc_ids, project_info
238
-
243
+
239
244
  async def search_all_projects(
240
245
  self,
241
246
  query: str,
@@ -245,49 +250,53 @@ class ProjectVectorManager:
245
250
  project_filter: Optional[List[str]] = None,
246
251
  ) -> Dict[str, List[SearchResult]]:
247
252
  """Search across all projects in parallel.
248
-
253
+
249
254
  Args:
250
255
  query: Search query
251
256
  limit_per_project: Maximum results per project
252
257
  score_threshold: Minimum similarity score
253
258
  include_global: Whether to include global store
254
259
  project_filter: List of project names to search (None for all)
255
-
260
+
256
261
  Returns:
257
262
  Dictionary mapping project names to search results
258
263
  """
259
264
  search_tasks = []
260
265
  project_names = []
261
-
266
+
262
267
  # Add global store if requested
263
268
  if include_global:
264
269
  global_store = self._get_global_store()
265
270
  search_tasks.append(
266
271
  asyncio.get_event_loop().run_in_executor(
267
272
  self.executor,
268
- lambda: global_store.search(query, limit_per_project, score_threshold)
273
+ lambda: global_store.search(
274
+ query, limit_per_project, score_threshold
275
+ ),
269
276
  )
270
277
  )
271
278
  project_names.append("global")
272
-
279
+
273
280
  # Add project stores
274
- for project_key, project_info in self.projects.items():
281
+ for _project_key, project_info in self.projects.items():
275
282
  # Apply project filter
276
283
  if project_filter and project_info.name not in project_filter:
277
284
  continue
278
-
285
+
279
286
  vector_store = self.get_vector_store(project_info)
280
287
  search_tasks.append(
281
288
  asyncio.get_event_loop().run_in_executor(
282
289
  self.executor,
283
- lambda vs=vector_store: vs.search(query, limit_per_project, score_threshold)
290
+ lambda vs=vector_store: vs.search(
291
+ query, limit_per_project, score_threshold
292
+ ),
284
293
  )
285
294
  )
286
295
  project_names.append(project_info.name)
287
-
296
+
288
297
  # Execute all searches in parallel
289
298
  results = await asyncio.gather(*search_tasks, return_exceptions=True)
290
-
299
+
291
300
  # Combine results
292
301
  combined_results = {}
293
302
  for i, result in enumerate(results):
@@ -295,14 +304,15 @@ class ProjectVectorManager:
295
304
  if isinstance(result, Exception):
296
305
  # Log error but continue
297
306
  import logging
307
+
298
308
  logger = logging.getLogger(__name__)
299
309
  logger.error(f"Error searching project {project_name}: {result}")
300
310
  combined_results[project_name] = []
301
311
  else:
302
312
  combined_results[project_name] = result
303
-
313
+
304
314
  return combined_results
305
-
315
+
306
316
  def search_project_by_path(
307
317
  self,
308
318
  file_path: str,
@@ -311,33 +321,33 @@ class ProjectVectorManager:
311
321
  score_threshold: float = 0.0,
312
322
  ) -> List[SearchResult]:
313
323
  """Search the project containing a specific file path.
314
-
324
+
315
325
  Args:
316
326
  file_path: File path to determine project
317
327
  query: Search query
318
328
  limit: Maximum results
319
329
  score_threshold: Minimum similarity score
320
-
330
+
321
331
  Returns:
322
332
  Search results from the appropriate project store
323
333
  """
324
334
  project_info = self.get_project_for_path(file_path)
325
335
  vector_store = self.get_vector_store(project_info)
326
-
336
+
327
337
  return vector_store.search(
328
338
  query=query,
329
339
  limit=limit,
330
340
  score_threshold=score_threshold,
331
341
  )
332
-
342
+
333
343
  def get_project_stats(self) -> Dict[str, Dict[str, Any]]:
334
344
  """Get statistics for all projects.
335
-
345
+
336
346
  Returns:
337
347
  Dictionary mapping project names to stats
338
348
  """
339
349
  stats = {}
340
-
350
+
341
351
  # Global store stats
342
352
  try:
343
353
  global_store = self._get_global_store()
@@ -348,9 +358,9 @@ class ProjectVectorManager:
348
358
  }
349
359
  except Exception as e:
350
360
  stats["global"] = {"error": str(e)}
351
-
361
+
352
362
  # Project store stats
353
- for project_key, project_info in self.projects.items():
363
+ for _project_key, project_info in self.projects.items():
354
364
  try:
355
365
  vector_store = self.get_vector_store(project_info)
356
366
  project_files = vector_store.list_files()
@@ -362,24 +372,24 @@ class ProjectVectorManager:
362
372
  }
363
373
  except Exception as e:
364
374
  stats[project_info.name] = {"error": str(e)}
365
-
375
+
366
376
  return stats
367
-
377
+
368
378
  def cleanup(self):
369
379
  """Close all vector stores and cleanup resources."""
370
380
  # Close all project stores
371
381
  for vector_store in self.vector_stores.values():
372
382
  try:
373
383
  vector_store.close()
374
- except:
384
+ except Exception:
375
385
  pass
376
-
386
+
377
387
  # Close global store
378
388
  if self._global_store:
379
389
  try:
380
390
  self._global_store.close()
381
- except:
391
+ except Exception:
382
392
  pass
383
-
393
+
384
394
  # Shutdown executor
385
- self.executor.shutdown(wait=False)
395
+ self.executor.shutdown(wait=False)