hanzo-mcp 0.9.0__py3-none-any.whl → 0.9.2__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 (135) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/analytics/posthog_analytics.py +14 -1
  3. hanzo_mcp/cli.py +108 -4
  4. hanzo_mcp/server.py +11 -0
  5. hanzo_mcp/tools/__init__.py +3 -16
  6. hanzo_mcp/tools/agent/__init__.py +5 -0
  7. hanzo_mcp/tools/agent/agent.py +5 -0
  8. hanzo_mcp/tools/agent/agent_tool.py +3 -17
  9. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +623 -0
  10. hanzo_mcp/tools/agent/clarification_tool.py +7 -1
  11. hanzo_mcp/tools/agent/claude_desktop_auth.py +16 -6
  12. hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
  13. hanzo_mcp/tools/agent/cli_tools.py +26 -0
  14. hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
  15. hanzo_mcp/tools/agent/critic_tool.py +7 -1
  16. hanzo_mcp/tools/agent/iching_tool.py +5 -0
  17. hanzo_mcp/tools/agent/network_tool.py +5 -0
  18. hanzo_mcp/tools/agent/review_tool.py +7 -1
  19. hanzo_mcp/tools/agent/swarm_alias.py +5 -0
  20. hanzo_mcp/tools/agent/swarm_tool.py +701 -0
  21. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +554 -0
  22. hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
  23. hanzo_mcp/tools/common/auto_timeout.py +254 -0
  24. hanzo_mcp/tools/common/base.py +4 -0
  25. hanzo_mcp/tools/common/batch_tool.py +5 -0
  26. hanzo_mcp/tools/common/config_tool.py +5 -0
  27. hanzo_mcp/tools/common/critic_tool.py +5 -0
  28. hanzo_mcp/tools/common/paginated_base.py +4 -0
  29. hanzo_mcp/tools/common/permissions.py +38 -12
  30. hanzo_mcp/tools/common/personality.py +673 -980
  31. hanzo_mcp/tools/common/stats.py +5 -0
  32. hanzo_mcp/tools/common/thinking_tool.py +5 -0
  33. hanzo_mcp/tools/common/timeout_parser.py +103 -0
  34. hanzo_mcp/tools/common/tool_disable.py +5 -0
  35. hanzo_mcp/tools/common/tool_enable.py +5 -0
  36. hanzo_mcp/tools/common/tool_list.py +5 -0
  37. hanzo_mcp/tools/config/config_tool.py +5 -0
  38. hanzo_mcp/tools/config/mode_tool.py +5 -0
  39. hanzo_mcp/tools/database/graph.py +5 -0
  40. hanzo_mcp/tools/database/graph_add.py +5 -0
  41. hanzo_mcp/tools/database/graph_query.py +5 -0
  42. hanzo_mcp/tools/database/graph_remove.py +5 -0
  43. hanzo_mcp/tools/database/graph_search.py +5 -0
  44. hanzo_mcp/tools/database/graph_stats.py +5 -0
  45. hanzo_mcp/tools/database/sql.py +5 -0
  46. hanzo_mcp/tools/database/sql_query.py +2 -0
  47. hanzo_mcp/tools/database/sql_search.py +5 -0
  48. hanzo_mcp/tools/database/sql_stats.py +5 -0
  49. hanzo_mcp/tools/editor/neovim_command.py +5 -0
  50. hanzo_mcp/tools/editor/neovim_edit.py +7 -2
  51. hanzo_mcp/tools/editor/neovim_session.py +5 -0
  52. hanzo_mcp/tools/filesystem/__init__.py +23 -26
  53. hanzo_mcp/tools/filesystem/ast_tool.py +3 -4
  54. hanzo_mcp/tools/filesystem/base.py +2 -18
  55. hanzo_mcp/tools/filesystem/batch_search.py +825 -0
  56. hanzo_mcp/tools/filesystem/content_replace.py +5 -3
  57. hanzo_mcp/tools/filesystem/diff.py +5 -0
  58. hanzo_mcp/tools/filesystem/directory_tree.py +34 -281
  59. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +345 -0
  60. hanzo_mcp/tools/filesystem/edit.py +6 -5
  61. hanzo_mcp/tools/filesystem/find.py +177 -311
  62. hanzo_mcp/tools/filesystem/find_files.py +370 -0
  63. hanzo_mcp/tools/filesystem/git_search.py +5 -3
  64. hanzo_mcp/tools/filesystem/grep.py +454 -0
  65. hanzo_mcp/tools/filesystem/multi_edit.py +6 -5
  66. hanzo_mcp/tools/filesystem/read.py +10 -9
  67. hanzo_mcp/tools/filesystem/rules_tool.py +6 -4
  68. hanzo_mcp/tools/filesystem/search_tool.py +728 -0
  69. hanzo_mcp/tools/filesystem/symbols_tool.py +510 -0
  70. hanzo_mcp/tools/filesystem/tree.py +273 -0
  71. hanzo_mcp/tools/filesystem/watch.py +6 -1
  72. hanzo_mcp/tools/filesystem/write.py +13 -7
  73. hanzo_mcp/tools/jupyter/jupyter.py +30 -2
  74. hanzo_mcp/tools/jupyter/notebook_edit.py +298 -0
  75. hanzo_mcp/tools/jupyter/notebook_read.py +148 -0
  76. hanzo_mcp/tools/llm/consensus_tool.py +8 -6
  77. hanzo_mcp/tools/llm/llm_manage.py +5 -0
  78. hanzo_mcp/tools/llm/llm_tool.py +2 -0
  79. hanzo_mcp/tools/llm/llm_unified.py +5 -0
  80. hanzo_mcp/tools/llm/provider_tools.py +5 -0
  81. hanzo_mcp/tools/lsp/lsp_tool.py +475 -622
  82. hanzo_mcp/tools/mcp/mcp_add.py +7 -2
  83. hanzo_mcp/tools/mcp/mcp_remove.py +15 -2
  84. hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
  85. hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
  86. hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
  87. hanzo_mcp/tools/memory/memory_tools.py +17 -0
  88. hanzo_mcp/tools/search/find_tool.py +5 -3
  89. hanzo_mcp/tools/search/unified_search.py +3 -1
  90. hanzo_mcp/tools/shell/__init__.py +2 -14
  91. hanzo_mcp/tools/shell/base_process.py +4 -2
  92. hanzo_mcp/tools/shell/bash_tool.py +2 -0
  93. hanzo_mcp/tools/shell/command_executor.py +7 -7
  94. hanzo_mcp/tools/shell/logs.py +5 -0
  95. hanzo_mcp/tools/shell/npx.py +5 -0
  96. hanzo_mcp/tools/shell/npx_background.py +5 -0
  97. hanzo_mcp/tools/shell/npx_tool.py +5 -0
  98. hanzo_mcp/tools/shell/open.py +5 -0
  99. hanzo_mcp/tools/shell/pkill.py +5 -0
  100. hanzo_mcp/tools/shell/process_tool.py +5 -0
  101. hanzo_mcp/tools/shell/processes.py +5 -0
  102. hanzo_mcp/tools/shell/run_background.py +5 -0
  103. hanzo_mcp/tools/shell/run_command.py +2 -0
  104. hanzo_mcp/tools/shell/run_command_windows.py +5 -0
  105. hanzo_mcp/tools/shell/streaming_command.py +5 -0
  106. hanzo_mcp/tools/shell/uvx.py +5 -0
  107. hanzo_mcp/tools/shell/uvx_background.py +5 -0
  108. hanzo_mcp/tools/shell/uvx_tool.py +5 -0
  109. hanzo_mcp/tools/shell/zsh_tool.py +3 -0
  110. hanzo_mcp/tools/todo/todo.py +5 -0
  111. hanzo_mcp/tools/todo/todo_read.py +142 -0
  112. hanzo_mcp/tools/todo/todo_write.py +367 -0
  113. hanzo_mcp/tools/vector/__init__.py +42 -95
  114. hanzo_mcp/tools/vector/index_tool.py +5 -0
  115. hanzo_mcp/tools/vector/vector.py +5 -0
  116. hanzo_mcp/tools/vector/vector_index.py +5 -0
  117. hanzo_mcp/tools/vector/vector_search.py +5 -0
  118. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/METADATA +1 -1
  119. hanzo_mcp-0.9.2.dist-info/RECORD +195 -0
  120. hanzo_mcp/tools/common/path_utils.py +0 -34
  121. hanzo_mcp/tools/compiler/__init__.py +0 -8
  122. hanzo_mcp/tools/compiler/sandboxed_compiler.py +0 -681
  123. hanzo_mcp/tools/environment/__init__.py +0 -8
  124. hanzo_mcp/tools/environment/environment_detector.py +0 -594
  125. hanzo_mcp/tools/filesystem/search.py +0 -1160
  126. hanzo_mcp/tools/framework/__init__.py +0 -8
  127. hanzo_mcp/tools/framework/framework_modes.py +0 -714
  128. hanzo_mcp/tools/memory/conversation_memory.py +0 -636
  129. hanzo_mcp/tools/shell/run_tool.py +0 -56
  130. hanzo_mcp/tools/vector/node_tool.py +0 -538
  131. hanzo_mcp/tools/vector/unified_vector.py +0 -384
  132. hanzo_mcp-0.9.0.dist-info/RECORD +0 -191
  133. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/WHEEL +0 -0
  134. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/entry_points.txt +0 -0
  135. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/top_level.txt +0 -0
@@ -1,636 +0,0 @@
1
- """Conversation memory with vector store integration.
2
-
3
- This module provides conversation history management with vector embeddings
4
- for semantic search through past conversations.
5
- """
6
-
7
- import os
8
- import json
9
- import time
10
- import hashlib
11
- import logging
12
- import asyncio
13
- from typing import Dict, List, Any, Optional, Tuple
14
- from pathlib import Path
15
- from dataclasses import dataclass, field, asdict
16
- from datetime import datetime
17
- from enum import Enum
18
-
19
- import numpy as np
20
- from hanzo_mcp.types import MCPResourceDocument
21
- from hanzo_mcp.tools.common.base import BaseTool
22
-
23
-
24
- class MessageRole(Enum):
25
- """Message role in conversation."""
26
- USER = "user"
27
- ASSISTANT = "assistant"
28
- SYSTEM = "system"
29
- TOOL = "tool"
30
-
31
-
32
- @dataclass
33
- class Message:
34
- """Represents a message in conversation."""
35
- role: MessageRole
36
- content: str
37
- timestamp: float = field(default_factory=time.time)
38
- metadata: Dict[str, Any] = field(default_factory=dict)
39
- embedding: Optional[np.ndarray] = None
40
- id: Optional[str] = None
41
-
42
- def __post_init__(self):
43
- if not self.id:
44
- # Generate unique ID
45
- content_hash = hashlib.md5(
46
- f"{self.role.value}{self.content}{self.timestamp}".encode()
47
- ).hexdigest()[:8]
48
- self.id = f"msg_{content_hash}"
49
-
50
- def to_dict(self) -> Dict[str, Any]:
51
- """Convert to dictionary."""
52
- data = {
53
- "id": self.id,
54
- "role": self.role.value,
55
- "content": self.content,
56
- "timestamp": self.timestamp,
57
- "metadata": self.metadata,
58
- }
59
-
60
- if self.embedding is not None:
61
- data["embedding"] = self.embedding.tolist()
62
-
63
- return data
64
-
65
- @classmethod
66
- def from_dict(cls, data: Dict[str, Any]) -> "Message":
67
- """Create from dictionary."""
68
- embedding = None
69
- if "embedding" in data:
70
- embedding = np.array(data["embedding"])
71
-
72
- return cls(
73
- id=data.get("id"),
74
- role=MessageRole(data["role"]),
75
- content=data["content"],
76
- timestamp=data.get("timestamp", time.time()),
77
- metadata=data.get("metadata", {}),
78
- embedding=embedding,
79
- )
80
-
81
-
82
- @dataclass
83
- class Conversation:
84
- """Represents a conversation session."""
85
- id: str
86
- messages: List[Message] = field(default_factory=list)
87
- created_at: float = field(default_factory=time.time)
88
- updated_at: float = field(default_factory=time.time)
89
- metadata: Dict[str, Any] = field(default_factory=dict)
90
- summary: Optional[str] = None
91
- topics: List[str] = field(default_factory=list)
92
-
93
- def add_message(self, message: Message):
94
- """Add message to conversation."""
95
- self.messages.append(message)
96
- self.updated_at = time.time()
97
-
98
- def to_dict(self) -> Dict[str, Any]:
99
- """Convert to dictionary."""
100
- return {
101
- "id": self.id,
102
- "messages": [m.to_dict() for m in self.messages],
103
- "created_at": self.created_at,
104
- "updated_at": self.updated_at,
105
- "metadata": self.metadata,
106
- "summary": self.summary,
107
- "topics": self.topics,
108
- }
109
-
110
- @classmethod
111
- def from_dict(cls, data: Dict[str, Any]) -> "Conversation":
112
- """Create from dictionary."""
113
- messages = [Message.from_dict(m) for m in data.get("messages", [])]
114
-
115
- return cls(
116
- id=data["id"],
117
- messages=messages,
118
- created_at=data.get("created_at", time.time()),
119
- updated_at=data.get("updated_at", time.time()),
120
- metadata=data.get("metadata", {}),
121
- summary=data.get("summary"),
122
- topics=data.get("topics", []),
123
- )
124
-
125
-
126
- class EmbeddingProvider:
127
- """Provides text embeddings for vector search."""
128
-
129
- def __init__(self, model: str = "local"):
130
- self.model = model
131
- self.logger = logging.getLogger(__name__)
132
-
133
- if model == "local":
134
- # Use simple TF-IDF or word embeddings
135
- self._init_local_embedder()
136
- elif model == "openai":
137
- # Use OpenAI embeddings
138
- self._init_openai_embedder()
139
- elif model == "sentence-transformers":
140
- # Use sentence transformers
141
- self._init_sentence_transformers()
142
-
143
- def _init_local_embedder(self):
144
- """Initialize local embedder."""
145
- # Simple word vector approach
146
- self.vocab = {}
147
- self.embedding_dim = 384
148
-
149
- def _init_openai_embedder(self):
150
- """Initialize OpenAI embedder."""
151
- # Requires OpenAI API key
152
- pass
153
-
154
- def _init_sentence_transformers(self):
155
- """Initialize sentence transformers."""
156
- try:
157
- from sentence_transformers import SentenceTransformer
158
- self.model = SentenceTransformer('all-MiniLM-L6-v2')
159
- self.embedding_dim = 384
160
- except ImportError:
161
- self.logger.warning("sentence-transformers not installed, falling back to local")
162
- self._init_local_embedder()
163
-
164
- async def embed_text(self, text: str) -> np.ndarray:
165
- """Generate embedding for text."""
166
-
167
- if self.model == "local":
168
- # Simple hash-based embedding
169
- words = text.lower().split()
170
- embedding = np.zeros(self.embedding_dim)
171
-
172
- for word in words:
173
- # Hash word to get consistent index
174
- index = hash(word) % self.embedding_dim
175
- embedding[index] += 1
176
-
177
- # Normalize
178
- norm = np.linalg.norm(embedding)
179
- if norm > 0:
180
- embedding = embedding / norm
181
-
182
- return embedding
183
-
184
- elif self.model == "sentence-transformers":
185
- # Use sentence transformers
186
- try:
187
- from sentence_transformers import SentenceTransformer
188
- model = SentenceTransformer('all-MiniLM-L6-v2')
189
- embedding = model.encode(text)
190
- return embedding
191
- except:
192
- # Fallback to local
193
- return await self.embed_text(text)
194
-
195
- else:
196
- # Placeholder for other models
197
- return np.random.randn(self.embedding_dim)
198
-
199
- def cosine_similarity(self, a: np.ndarray, b: np.ndarray) -> float:
200
- """Calculate cosine similarity between embeddings."""
201
- dot_product = np.dot(a, b)
202
- norm_a = np.linalg.norm(a)
203
- norm_b = np.linalg.norm(b)
204
-
205
- if norm_a == 0 or norm_b == 0:
206
- return 0.0
207
-
208
- return dot_product / (norm_a * norm_b)
209
-
210
-
211
- class ConversationMemory(BaseTool):
212
- """Conversation memory with vector search."""
213
-
214
- name = "conversation_memory"
215
- description = """Manage conversation history with vector search.
216
-
217
- Actions:
218
- - add: Add message to current conversation
219
- - search: Search through conversation history
220
- - recall: Recall specific conversation
221
- - summarize: Summarize conversation
222
- - topics: Extract topics from conversation
223
- - export: Export conversations
224
- - import: Import conversations
225
- - stats: Get memory statistics
226
-
227
- This tool maintains conversation history with vector embeddings
228
- for semantic search across all past conversations.
229
- """
230
-
231
- def __init__(self, storage_path: Optional[str] = None):
232
- super().__init__()
233
- self.logger = logging.getLogger(__name__)
234
-
235
- # Storage
236
- if storage_path:
237
- self.storage_path = Path(storage_path)
238
- else:
239
- self.storage_path = Path.home() / ".hanzo" / "conversations"
240
-
241
- self.storage_path.mkdir(parents=True, exist_ok=True)
242
-
243
- # Current conversation
244
- self.current_conversation_id = self._generate_conversation_id()
245
- self.conversations: Dict[str, Conversation] = {}
246
- self.current_conversation = Conversation(id=self.current_conversation_id)
247
- self.conversations[self.current_conversation_id] = self.current_conversation
248
-
249
- # Embeddings
250
- self.embedder = EmbeddingProvider(model="local")
251
-
252
- # Load existing conversations
253
- self._load_conversations()
254
-
255
- def _generate_conversation_id(self) -> str:
256
- """Generate unique conversation ID."""
257
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
258
- random_suffix = hashlib.md5(os.urandom(16)).hexdigest()[:6]
259
- return f"conv_{timestamp}_{random_suffix}"
260
-
261
- def _load_conversations(self):
262
- """Load conversations from storage."""
263
- for conv_file in self.storage_path.glob("conv_*.json"):
264
- try:
265
- with open(conv_file, "r") as f:
266
- data = json.load(f)
267
- conv = Conversation.from_dict(data)
268
- self.conversations[conv.id] = conv
269
- except Exception as e:
270
- self.logger.error(f"Failed to load {conv_file}: {e}")
271
-
272
- def _save_conversation(self, conversation: Conversation):
273
- """Save conversation to storage."""
274
- conv_file = self.storage_path / f"{conversation.id}.json"
275
-
276
- with open(conv_file, "w") as f:
277
- json.dump(conversation.to_dict(), f, indent=2)
278
-
279
- async def add_message(
280
- self,
281
- role: str,
282
- content: str,
283
- metadata: Optional[Dict[str, Any]] = None,
284
- ) -> Message:
285
- """Add message to current conversation."""
286
-
287
- # Create message
288
- message = Message(
289
- role=MessageRole(role.lower()),
290
- content=content,
291
- metadata=metadata or {},
292
- )
293
-
294
- # Generate embedding
295
- message.embedding = await self.embedder.embed_text(content)
296
-
297
- # Add to conversation
298
- self.current_conversation.add_message(message)
299
-
300
- # Save periodically
301
- if len(self.current_conversation.messages) % 10 == 0:
302
- self._save_conversation(self.current_conversation)
303
-
304
- return message
305
-
306
- async def search_messages(
307
- self,
308
- query: str,
309
- limit: int = 10,
310
- conversation_id: Optional[str] = None,
311
- role_filter: Optional[str] = None,
312
- ) -> List[Tuple[Message, float, str]]:
313
- """Search messages using vector similarity."""
314
-
315
- # Generate query embedding
316
- query_embedding = await self.embedder.embed_text(query)
317
-
318
- # Search across conversations
319
- results = []
320
-
321
- conversations = (
322
- [self.conversations[conversation_id]]
323
- if conversation_id and conversation_id in self.conversations
324
- else self.conversations.values()
325
- )
326
-
327
- for conv in conversations:
328
- for msg in conv.messages:
329
- # Apply role filter
330
- if role_filter and msg.role.value != role_filter:
331
- continue
332
-
333
- # Calculate similarity
334
- if msg.embedding is not None:
335
- similarity = self.embedder.cosine_similarity(
336
- query_embedding, msg.embedding
337
- )
338
-
339
- results.append((msg, similarity, conv.id))
340
-
341
- # Sort by similarity
342
- results.sort(key=lambda x: x[1], reverse=True)
343
-
344
- return results[:limit]
345
-
346
- async def summarize_conversation(
347
- self,
348
- conversation_id: Optional[str] = None,
349
- ) -> str:
350
- """Generate conversation summary."""
351
-
352
- conv = (
353
- self.conversations.get(conversation_id, self.current_conversation)
354
- if conversation_id
355
- else self.current_conversation
356
- )
357
-
358
- if not conv.messages:
359
- return "No messages in conversation"
360
-
361
- # Simple extractive summary
362
- # In production, use LLM for abstractive summary
363
- important_messages = []
364
-
365
- for msg in conv.messages:
366
- if msg.role == MessageRole.USER:
367
- # Include user questions
368
- important_messages.append(f"User: {msg.content[:100]}...")
369
- elif msg.role == MessageRole.ASSISTANT:
370
- # Include key assistant responses
371
- if any(keyword in msg.content.lower() for keyword in
372
- ["important", "summary", "conclusion", "result"]):
373
- important_messages.append(f"Assistant: {msg.content[:100]}...")
374
-
375
- summary = "\n".join(important_messages[:5])
376
-
377
- # Store summary
378
- conv.summary = summary
379
- self._save_conversation(conv)
380
-
381
- return summary
382
-
383
- async def extract_topics(
384
- self,
385
- conversation_id: Optional[str] = None,
386
- ) -> List[str]:
387
- """Extract topics from conversation."""
388
-
389
- conv = (
390
- self.conversations.get(conversation_id, self.current_conversation)
391
- if conversation_id
392
- else self.current_conversation
393
- )
394
-
395
- # Simple keyword extraction
396
- # In production, use NLP for topic modeling
397
- text = " ".join(msg.content for msg in conv.messages)
398
- words = text.lower().split()
399
-
400
- # Count word frequencies
401
- word_freq = {}
402
- stopwords = {"the", "is", "at", "which", "on", "a", "an", "and", "or", "but"}
403
-
404
- for word in words:
405
- if word not in stopwords and len(word) > 3:
406
- word_freq[word] = word_freq.get(word, 0) + 1
407
-
408
- # Get top topics
409
- topics = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)
410
- topic_words = [word for word, freq in topics[:10] if freq > 2]
411
-
412
- # Store topics
413
- conv.topics = topic_words
414
- self._save_conversation(conv)
415
-
416
- return topic_words
417
-
418
- def get_statistics(self) -> Dict[str, Any]:
419
- """Get memory statistics."""
420
-
421
- total_messages = sum(len(c.messages) for c in self.conversations.values())
422
- total_conversations = len(self.conversations)
423
-
424
- # Message distribution
425
- role_distribution = {}
426
- for conv in self.conversations.values():
427
- for msg in conv.messages:
428
- role = msg.role.value
429
- role_distribution[role] = role_distribution.get(role, 0) + 1
430
-
431
- # Storage size
432
- total_size = sum(
433
- f.stat().st_size
434
- for f in self.storage_path.glob("conv_*.json")
435
- )
436
-
437
- return {
438
- "total_conversations": total_conversations,
439
- "total_messages": total_messages,
440
- "current_conversation_id": self.current_conversation_id,
441
- "current_conversation_messages": len(self.current_conversation.messages),
442
- "role_distribution": role_distribution,
443
- "storage_size_bytes": total_size,
444
- "storage_size_mb": total_size / (1024 * 1024),
445
- }
446
-
447
- def export_conversations(
448
- self,
449
- format: str = "json",
450
- conversation_ids: Optional[List[str]] = None,
451
- ) -> Dict[str, Any]:
452
- """Export conversations."""
453
-
454
- conversations = (
455
- {cid: self.conversations[cid] for cid in conversation_ids
456
- if cid in self.conversations}
457
- if conversation_ids
458
- else self.conversations
459
- )
460
-
461
- if format == "json":
462
- return {
463
- "conversations": [c.to_dict() for c in conversations.values()],
464
- "exported_at": time.time(),
465
- "total": len(conversations),
466
- }
467
-
468
- elif format == "markdown":
469
- md_content = []
470
-
471
- for conv in conversations.values():
472
- md_content.append(f"# Conversation {conv.id}")
473
- md_content.append(f"Created: {datetime.fromtimestamp(conv.created_at)}")
474
-
475
- if conv.summary:
476
- md_content.append(f"\n## Summary\n{conv.summary}")
477
-
478
- if conv.topics:
479
- md_content.append(f"\n## Topics\n{', '.join(conv.topics)}")
480
-
481
- md_content.append("\n## Messages\n")
482
-
483
- for msg in conv.messages:
484
- timestamp = datetime.fromtimestamp(msg.timestamp)
485
- md_content.append(f"\n**{msg.role.value.title()}** ({timestamp}):")
486
- md_content.append(msg.content)
487
-
488
- md_content.append("\n---\n")
489
-
490
- return {
491
- "content": "\n".join(md_content),
492
- "format": "markdown",
493
- "total": len(conversations),
494
- }
495
-
496
- return {"error": f"Unknown format: {format}"}
497
-
498
- async def run(
499
- self,
500
- action: str,
501
- content: Optional[str] = None,
502
- role: str = "user",
503
- query: Optional[str] = None,
504
- conversation_id: Optional[str] = None,
505
- limit: int = 10,
506
- format: str = "json",
507
- **kwargs,
508
- ) -> MCPResourceDocument:
509
- """Execute memory action."""
510
-
511
- if action == "add":
512
- # Add message
513
- if not content:
514
- return MCPResourceDocument(data={"error": "Content required"})
515
-
516
- message = await self.add_message(role, content, kwargs.get("metadata"))
517
-
518
- return MCPResourceDocument(
519
- data={
520
- "message_id": message.id,
521
- "conversation_id": self.current_conversation_id,
522
- "timestamp": message.timestamp,
523
- "message_count": len(self.current_conversation.messages),
524
- }
525
- )
526
-
527
- elif action == "search":
528
- # Search messages
529
- if not query:
530
- return MCPResourceDocument(data={"error": "Query required"})
531
-
532
- results = await self.search_messages(
533
- query, limit, conversation_id, kwargs.get("role_filter")
534
- )
535
-
536
- return MCPResourceDocument(
537
- data={
538
- "query": query,
539
- "results": [
540
- {
541
- "message_id": msg.id,
542
- "conversation_id": cid,
543
- "role": msg.role.value,
544
- "content": msg.content[:200],
545
- "similarity": float(score),
546
- "timestamp": msg.timestamp,
547
- }
548
- for msg, score, cid in results
549
- ],
550
- "total": len(results),
551
- }
552
- )
553
-
554
- elif action == "recall":
555
- # Recall conversation
556
- conv = self.conversations.get(
557
- conversation_id or self.current_conversation_id
558
- )
559
-
560
- if not conv:
561
- return MCPResourceDocument(
562
- data={"error": f"Conversation not found: {conversation_id}"}
563
- )
564
-
565
- return MCPResourceDocument(data=conv.to_dict())
566
-
567
- elif action == "summarize":
568
- # Summarize conversation
569
- summary = await self.summarize_conversation(conversation_id)
570
-
571
- return MCPResourceDocument(
572
- data={
573
- "conversation_id": conversation_id or self.current_conversation_id,
574
- "summary": summary,
575
- }
576
- )
577
-
578
- elif action == "topics":
579
- # Extract topics
580
- topics = await self.extract_topics(conversation_id)
581
-
582
- return MCPResourceDocument(
583
- data={
584
- "conversation_id": conversation_id or self.current_conversation_id,
585
- "topics": topics,
586
- }
587
- )
588
-
589
- elif action == "export":
590
- # Export conversations
591
- result = self.export_conversations(
592
- format, kwargs.get("conversation_ids")
593
- )
594
-
595
- return MCPResourceDocument(data=result)
596
-
597
- elif action == "stats":
598
- # Get statistics
599
- stats = self.get_statistics()
600
-
601
- return MCPResourceDocument(data=stats)
602
-
603
- elif action == "new":
604
- # Start new conversation
605
- self.current_conversation_id = self._generate_conversation_id()
606
- self.current_conversation = Conversation(id=self.current_conversation_id)
607
- self.conversations[self.current_conversation_id] = self.current_conversation
608
-
609
- return MCPResourceDocument(
610
- data={
611
- "conversation_id": self.current_conversation_id,
612
- "created": True,
613
- }
614
- )
615
-
616
- else:
617
- return MCPResourceDocument(
618
- data={
619
- "error": f"Unknown action: {action}",
620
- "valid_actions": [
621
- "add", "search", "recall", "summarize",
622
- "topics", "export", "stats", "new"
623
- ],
624
- }
625
- )
626
-
627
- async def call(self, **kwargs) -> str:
628
- """Tool interface for MCP."""
629
- result = await self.run(**kwargs)
630
- return result.to_json_string()
631
-
632
-
633
- # Factory function
634
- def create_conversation_memory(storage_path: Optional[str] = None):
635
- """Create conversation memory tool."""
636
- return ConversationMemory(storage_path)
@@ -1,56 +0,0 @@
1
- """Run tool for command execution with automatic backgrounding."""
2
-
3
- from typing import Optional, override
4
- from mcp.server import FastMCP
5
- from mcp.server.fastmcp import Context as MCPContext
6
-
7
- from hanzo_mcp.tools.shell.zsh_tool import ShellTool
8
-
9
-
10
- class RunTool(ShellTool):
11
- """Tool for running commands with automatic backgrounding (alias of shell tool)."""
12
-
13
- name = "run"
14
-
15
- def register(self, server: FastMCP) -> None:
16
- """Register the tool with the MCP server."""
17
- tool_self = self
18
-
19
- @server.tool(name=self.name, description=self.description)
20
- async def run(
21
- ctx: MCPContext,
22
- command: str,
23
- cwd: Optional[str] = None,
24
- env: Optional[dict[str, str]] = None,
25
- timeout: Optional[int] = None,
26
- ) -> str:
27
- return await tool_self.run(ctx, command=command, cwd=cwd, env=env, timeout=timeout)
28
-
29
- @property
30
- @override
31
- def description(self) -> str:
32
- """Get the tool description."""
33
- return """Execute shell commands with automatic backgrounding for long-running processes.
34
-
35
- Automatically selects the best available shell:
36
- - Zsh if available (with .zshrc)
37
- - User's preferred shell ($SHELL)
38
- - Bash as fallback
39
-
40
- Commands that run for more than 2 minutes will automatically continue in the background.
41
- You can check their status and logs using the 'process' tool.
42
-
43
- Usage:
44
- run "ls -la"
45
- run "python server.py" # Auto-backgrounds after 2 minutes
46
- run "git status && git diff"
47
- run "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
48
-
49
- @override
50
- def get_tool_name(self) -> str:
51
- """Get the tool name."""
52
- return "run"
53
-
54
-
55
- # Create instance
56
- run_tool = RunTool()