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,19 +1,17 @@
1
1
  """Graph statistics tool for analyzing the graph database."""
2
2
 
3
- import json
4
3
  import sqlite3
5
- from typing import Annotated, Optional, TypedDict, Unpack, final, override
6
- from collections import Counter, defaultdict
4
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
5
+ from collections import defaultdict
7
6
 
8
- from mcp.server.fastmcp import Context as MCPContext
9
7
  from pydantic import Field
8
+ from mcp.server.fastmcp import Context as MCPContext
10
9
 
11
10
  from hanzo_mcp.tools.common.base import BaseTool
12
11
  from hanzo_mcp.tools.common.context import create_tool_context
13
12
  from hanzo_mcp.tools.common.permissions import PermissionManager
14
13
  from hanzo_mcp.tools.database.database_manager import DatabaseManager
15
14
 
16
-
17
15
  ProjectPath = Annotated[
18
16
  Optional[str],
19
17
  Field(
@@ -60,7 +58,9 @@ class GraphStatsParams(TypedDict, total=False):
60
58
  class GraphStatsTool(BaseTool):
61
59
  """Tool for getting graph database statistics."""
62
60
 
63
- def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
61
+ def __init__(
62
+ self, permission_manager: PermissionManager, db_manager: DatabaseManager
63
+ ):
64
64
  """Initialize the graph stats tool.
65
65
 
66
66
  Args:
@@ -128,8 +128,9 @@ Examples:
128
128
  project_db = self.db_manager.get_project_db(project_path)
129
129
  else:
130
130
  import os
131
+
131
132
  project_db = self.db_manager.get_project_for_path(os.getcwd())
132
-
133
+
133
134
  if not project_db:
134
135
  return "Error: Could not find project database"
135
136
 
@@ -138,7 +139,9 @@ Examples:
138
139
  except Exception as e:
139
140
  return f"Error accessing project database: {str(e)}"
140
141
 
141
- await tool_ctx.info(f"Getting graph statistics for project: {project_db.project_path}")
142
+ await tool_ctx.info(
143
+ f"Getting graph statistics for project: {project_db.project_path}"
144
+ )
142
145
 
143
146
  # Get graph connection
144
147
  graph_conn = project_db.get_graph_connection()
@@ -150,69 +153,80 @@ Examples:
150
153
  output.append(f"Project: {project_db.project_path}")
151
154
  output.append(f"Database: {project_db.graph_path}")
152
155
  output.append("")
153
-
156
+
154
157
  # Basic counts
155
158
  if node_type_filter:
156
- cursor.execute("SELECT COUNT(*) FROM nodes WHERE type = ?", (node_type_filter,))
159
+ cursor.execute(
160
+ "SELECT COUNT(*) FROM nodes WHERE type = ?", (node_type_filter,)
161
+ )
157
162
  node_count = cursor.fetchone()[0]
158
163
  output.append(f"Nodes (type='{node_type_filter}'): {node_count:,}")
159
164
  else:
160
165
  cursor.execute("SELECT COUNT(*) FROM nodes")
161
166
  node_count = cursor.fetchone()[0]
162
167
  output.append(f"Total Nodes: {node_count:,}")
163
-
168
+
164
169
  if relationship_filter:
165
- cursor.execute("SELECT COUNT(*) FROM edges WHERE relationship = ?", (relationship_filter,))
170
+ cursor.execute(
171
+ "SELECT COUNT(*) FROM edges WHERE relationship = ?",
172
+ (relationship_filter,),
173
+ )
166
174
  edge_count = cursor.fetchone()[0]
167
- output.append(f"Edges (relationship='{relationship_filter}'): {edge_count:,}")
175
+ output.append(
176
+ f"Edges (relationship='{relationship_filter}'): {edge_count:,}"
177
+ )
168
178
  else:
169
179
  cursor.execute("SELECT COUNT(*) FROM edges")
170
180
  edge_count = cursor.fetchone()[0]
171
181
  output.append(f"Total Edges: {edge_count:,}")
172
-
182
+
173
183
  if node_count == 0:
174
184
  output.append("\nGraph is empty.")
175
185
  return "\n".join(output)
176
-
186
+
177
187
  output.append("")
178
-
188
+
179
189
  # Node type distribution
180
190
  output.append("=== Node Types ===")
181
- cursor.execute("SELECT type, COUNT(*) as count FROM nodes GROUP BY type ORDER BY count DESC")
191
+ cursor.execute(
192
+ "SELECT type, COUNT(*) as count FROM nodes GROUP BY type ORDER BY count DESC"
193
+ )
182
194
  node_types = cursor.fetchall()
183
-
195
+
184
196
  for n_type, count in node_types[:10]:
185
197
  pct = (count / node_count) * 100
186
198
  output.append(f"{n_type}: {count:,} ({pct:.1f}%)")
187
-
199
+
188
200
  if len(node_types) > 10:
189
201
  output.append(f"... and {len(node_types) - 10} more types")
190
-
202
+
191
203
  output.append("")
192
-
204
+
193
205
  # Relationship distribution
194
206
  output.append("=== Relationship Types ===")
195
- cursor.execute("SELECT relationship, COUNT(*) as count FROM edges GROUP BY relationship ORDER BY count DESC")
207
+ cursor.execute(
208
+ "SELECT relationship, COUNT(*) as count FROM edges GROUP BY relationship ORDER BY count DESC"
209
+ )
196
210
  rel_types = cursor.fetchall()
197
-
211
+
198
212
  if rel_types:
199
213
  for rel, count in rel_types[:10]:
200
214
  pct = (count / edge_count) * 100 if edge_count > 0 else 0
201
215
  output.append(f"{rel}: {count:,} ({pct:.1f}%)")
202
-
216
+
203
217
  if len(rel_types) > 10:
204
218
  output.append(f"... and {len(rel_types) - 10} more types")
205
219
  else:
206
220
  output.append("No edges in graph")
207
-
221
+
208
222
  output.append("")
209
-
223
+
210
224
  # Degree statistics
211
225
  output.append("=== Connectivity ===")
212
-
226
+
213
227
  # Calculate degrees
214
228
  degrees = defaultdict(int)
215
-
229
+
216
230
  # Out-degree
217
231
  query = "SELECT source, COUNT(*) FROM edges"
218
232
  if relationship_filter:
@@ -220,10 +234,10 @@ Examples:
220
234
  cursor.execute(query + " GROUP BY source", (relationship_filter,))
221
235
  else:
222
236
  cursor.execute(query + " GROUP BY source")
223
-
237
+
224
238
  for node, out_degree in cursor.fetchall():
225
239
  degrees[node] += out_degree
226
-
240
+
227
241
  # In-degree
228
242
  query = "SELECT target, COUNT(*) FROM edges"
229
243
  if relationship_filter:
@@ -231,72 +245,85 @@ Examples:
231
245
  cursor.execute(query + " GROUP BY target", (relationship_filter,))
232
246
  else:
233
247
  cursor.execute(query + " GROUP BY target")
234
-
248
+
235
249
  for node, in_degree in cursor.fetchall():
236
250
  degrees[node] += in_degree
237
-
251
+
238
252
  if degrees:
239
253
  degree_values = list(degrees.values())
240
254
  avg_degree = sum(degree_values) / len(degree_values)
241
255
  max_degree = max(degree_values)
242
256
  min_degree = min(degree_values)
243
-
257
+
244
258
  output.append(f"Average degree: {avg_degree:.2f}")
245
259
  output.append(f"Max degree: {max_degree}")
246
260
  output.append(f"Min degree: {min_degree}")
247
-
261
+
248
262
  # Most connected nodes
249
263
  output.append("\nMost connected nodes:")
250
264
  sorted_nodes = sorted(degrees.items(), key=lambda x: x[1], reverse=True)
251
-
265
+
252
266
  for node, degree in sorted_nodes[:5]:
253
267
  cursor.execute("SELECT type FROM nodes WHERE id = ?", (node,))
254
268
  node_type = cursor.fetchone()
255
269
  type_str = f" ({node_type[0]})" if node_type else ""
256
270
  output.append(f" {node}{type_str}: {degree} connections")
257
-
271
+
258
272
  # Orphaned nodes
259
- cursor.execute("""
273
+ cursor.execute(
274
+ """
260
275
  SELECT COUNT(*) FROM nodes n
261
276
  WHERE NOT EXISTS (SELECT 1 FROM edges WHERE source = n.id OR target = n.id)
262
- """)
277
+ """
278
+ )
263
279
  orphan_count = cursor.fetchone()[0]
264
280
  if orphan_count > 0:
265
281
  orphan_pct = (orphan_count / node_count) * 100
266
282
  output.append(f"\nOrphaned nodes: {orphan_count} ({orphan_pct:.1f}%)")
267
-
283
+
268
284
  if detailed:
269
285
  output.append("\n=== Detailed Analysis ===")
270
-
286
+
271
287
  # Node properties usage
272
- cursor.execute("SELECT COUNT(*) FROM nodes WHERE properties IS NOT NULL")
288
+ cursor.execute(
289
+ "SELECT COUNT(*) FROM nodes WHERE properties IS NOT NULL"
290
+ )
273
291
  nodes_with_props = cursor.fetchone()[0]
274
292
  if nodes_with_props > 0:
275
293
  props_pct = (nodes_with_props / node_count) * 100
276
- output.append(f"Nodes with properties: {nodes_with_props} ({props_pct:.1f}%)")
277
-
294
+ output.append(
295
+ f"Nodes with properties: {nodes_with_props} ({props_pct:.1f}%)"
296
+ )
297
+
278
298
  # Edge properties usage
279
- cursor.execute("SELECT COUNT(*) FROM edges WHERE properties IS NOT NULL")
299
+ cursor.execute(
300
+ "SELECT COUNT(*) FROM edges WHERE properties IS NOT NULL"
301
+ )
280
302
  edges_with_props = cursor.fetchone()[0]
281
303
  if edges_with_props > 0 and edge_count > 0:
282
304
  props_pct = (edges_with_props / edge_count) * 100
283
- output.append(f"Edges with properties: {edges_with_props} ({props_pct:.1f}%)")
284
-
305
+ output.append(
306
+ f"Edges with properties: {edges_with_props} ({props_pct:.1f}%)"
307
+ )
308
+
285
309
  # Weight distribution
286
- cursor.execute("SELECT MIN(weight), MAX(weight), AVG(weight) FROM edges")
310
+ cursor.execute(
311
+ "SELECT MIN(weight), MAX(weight), AVG(weight) FROM edges"
312
+ )
287
313
  weight_stats = cursor.fetchone()
288
314
  if weight_stats[0] is not None:
289
315
  output.append(f"\nEdge weights:")
290
316
  output.append(f" Min: {weight_stats[0]}")
291
317
  output.append(f" Max: {weight_stats[1]}")
292
318
  output.append(f" Avg: {weight_stats[2]:.2f}")
293
-
319
+
294
320
  # Most common patterns
295
321
  if not relationship_filter:
296
322
  output.append("\n=== Common Patterns ===")
297
-
323
+
298
324
  # Most common node type connections
299
- cursor.execute("""
325
+ cursor.execute(
326
+ """
300
327
  SELECT n1.type, e.relationship, n2.type, COUNT(*) as count
301
328
  FROM edges e
302
329
  JOIN nodes n1 ON e.source = n1.id
@@ -304,33 +331,38 @@ Examples:
304
331
  GROUP BY n1.type, e.relationship, n2.type
305
332
  ORDER BY count DESC
306
333
  LIMIT 10
307
- """)
308
-
334
+ """
335
+ )
336
+
309
337
  patterns = cursor.fetchall()
310
338
  if patterns:
311
339
  output.append("Most common connections:")
312
340
  for src_type, rel, tgt_type, count in patterns:
313
- output.append(f" {src_type} --[{rel}]--> {tgt_type}: {count} times")
314
-
341
+ output.append(
342
+ f" {src_type} --[{rel}]--> {tgt_type}: {count} times"
343
+ )
344
+
315
345
  # Component analysis (simplified)
316
346
  output.append("\n=== Graph Structure ===")
317
-
347
+
318
348
  # Check if graph is fully connected (simplified)
319
- cursor.execute("""
349
+ cursor.execute(
350
+ """
320
351
  SELECT COUNT(DISTINCT node_id) FROM (
321
352
  SELECT source as node_id FROM edges
322
353
  UNION
323
354
  SELECT target as node_id FROM edges
324
355
  )
325
- """)
356
+ """
357
+ )
326
358
  connected_nodes = cursor.fetchone()[0]
327
-
359
+
328
360
  if connected_nodes < node_count:
329
361
  output.append(f"Connected nodes: {connected_nodes} / {node_count}")
330
362
  output.append("Graph has disconnected components")
331
363
  else:
332
364
  output.append("All nodes are connected")
333
-
365
+
334
366
  return "\n".join(output)
335
367
 
336
368
  except sqlite3.Error as e: