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.
- hanzo_mcp/__init__.py +6 -0
- hanzo_mcp/__main__.py +1 -1
- hanzo_mcp/analytics/__init__.py +2 -2
- hanzo_mcp/analytics/posthog_analytics.py +76 -82
- hanzo_mcp/cli.py +31 -36
- hanzo_mcp/cli_enhanced.py +94 -72
- hanzo_mcp/cli_plugin.py +27 -17
- hanzo_mcp/config/__init__.py +2 -2
- hanzo_mcp/config/settings.py +112 -88
- hanzo_mcp/config/tool_config.py +32 -34
- hanzo_mcp/dev_server.py +66 -67
- hanzo_mcp/prompts/__init__.py +94 -12
- hanzo_mcp/prompts/enhanced_prompts.py +809 -0
- hanzo_mcp/prompts/example_custom_prompt.py +6 -5
- hanzo_mcp/prompts/project_todo_reminder.py +0 -1
- hanzo_mcp/prompts/tool_explorer.py +10 -7
- hanzo_mcp/server.py +17 -21
- hanzo_mcp/server_enhanced.py +15 -22
- hanzo_mcp/tools/__init__.py +56 -28
- hanzo_mcp/tools/agent/__init__.py +16 -19
- hanzo_mcp/tools/agent/agent.py +82 -65
- hanzo_mcp/tools/agent/agent_tool.py +152 -122
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
- hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
- hanzo_mcp/tools/agent/clarification_tool.py +11 -10
- hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
- hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
- hanzo_mcp/tools/agent/code_auth.py +102 -107
- hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
- hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
- hanzo_mcp/tools/agent/critic_tool.py +86 -73
- hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/iching_tool.py +404 -139
- hanzo_mcp/tools/agent/network_tool.py +89 -73
- hanzo_mcp/tools/agent/prompt.py +2 -1
- hanzo_mcp/tools/agent/review_tool.py +101 -98
- hanzo_mcp/tools/agent/swarm_alias.py +87 -0
- hanzo_mcp/tools/agent/swarm_tool.py +246 -161
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
- hanzo_mcp/tools/agent/tool_adapter.py +21 -11
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +3 -5
- hanzo_mcp/tools/common/batch_tool.py +46 -39
- hanzo_mcp/tools/common/config_tool.py +120 -84
- hanzo_mcp/tools/common/context.py +1 -5
- hanzo_mcp/tools/common/context_fix.py +5 -3
- hanzo_mcp/tools/common/critic_tool.py +4 -8
- hanzo_mcp/tools/common/decorators.py +58 -56
- hanzo_mcp/tools/common/enhanced_base.py +29 -32
- hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
- hanzo_mcp/tools/common/forgiving_edit.py +91 -87
- hanzo_mcp/tools/common/mode.py +15 -17
- hanzo_mcp/tools/common/mode_loader.py +27 -24
- hanzo_mcp/tools/common/paginated_base.py +61 -53
- hanzo_mcp/tools/common/paginated_response.py +72 -79
- hanzo_mcp/tools/common/pagination.py +50 -53
- hanzo_mcp/tools/common/permissions.py +4 -4
- hanzo_mcp/tools/common/personality.py +186 -138
- hanzo_mcp/tools/common/plugin_loader.py +54 -54
- hanzo_mcp/tools/common/stats.py +65 -47
- hanzo_mcp/tools/common/test_helpers.py +31 -0
- hanzo_mcp/tools/common/thinking_tool.py +4 -8
- hanzo_mcp/tools/common/tool_disable.py +17 -12
- hanzo_mcp/tools/common/tool_enable.py +13 -14
- hanzo_mcp/tools/common/tool_list.py +36 -28
- hanzo_mcp/tools/common/truncate.py +23 -23
- hanzo_mcp/tools/config/__init__.py +4 -4
- hanzo_mcp/tools/config/config_tool.py +42 -29
- hanzo_mcp/tools/config/index_config.py +37 -34
- hanzo_mcp/tools/config/mode_tool.py +175 -55
- hanzo_mcp/tools/database/__init__.py +15 -12
- hanzo_mcp/tools/database/database_manager.py +77 -75
- hanzo_mcp/tools/database/graph.py +137 -91
- hanzo_mcp/tools/database/graph_add.py +30 -18
- hanzo_mcp/tools/database/graph_query.py +178 -102
- hanzo_mcp/tools/database/graph_remove.py +33 -28
- hanzo_mcp/tools/database/graph_search.py +97 -75
- hanzo_mcp/tools/database/graph_stats.py +91 -59
- hanzo_mcp/tools/database/sql.py +107 -79
- hanzo_mcp/tools/database/sql_query.py +30 -24
- hanzo_mcp/tools/database/sql_search.py +29 -25
- hanzo_mcp/tools/database/sql_stats.py +47 -35
- hanzo_mcp/tools/editor/neovim_command.py +25 -28
- hanzo_mcp/tools/editor/neovim_edit.py +21 -23
- hanzo_mcp/tools/editor/neovim_session.py +60 -54
- hanzo_mcp/tools/filesystem/__init__.py +31 -30
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
- hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +316 -224
- hanzo_mcp/tools/filesystem/content_replace.py +4 -4
- hanzo_mcp/tools/filesystem/diff.py +71 -59
- hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
- hanzo_mcp/tools/filesystem/edit.py +4 -4
- hanzo_mcp/tools/filesystem/find.py +173 -80
- hanzo_mcp/tools/filesystem/find_files.py +73 -52
- hanzo_mcp/tools/filesystem/git_search.py +157 -104
- hanzo_mcp/tools/filesystem/grep.py +8 -8
- hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
- hanzo_mcp/tools/filesystem/read.py +12 -10
- hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
- hanzo_mcp/tools/filesystem/search_tool.py +263 -207
- hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
- hanzo_mcp/tools/filesystem/tree.py +35 -33
- hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
- hanzo_mcp/tools/filesystem/watch.py +37 -36
- hanzo_mcp/tools/filesystem/write.py +4 -8
- hanzo_mcp/tools/jupyter/__init__.py +4 -4
- hanzo_mcp/tools/jupyter/base.py +4 -5
- hanzo_mcp/tools/jupyter/jupyter.py +67 -47
- hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
- hanzo_mcp/tools/llm/__init__.py +5 -7
- hanzo_mcp/tools/llm/consensus_tool.py +72 -52
- hanzo_mcp/tools/llm/llm_manage.py +101 -60
- hanzo_mcp/tools/llm/llm_tool.py +226 -166
- hanzo_mcp/tools/llm/provider_tools.py +25 -26
- hanzo_mcp/tools/lsp/__init__.py +1 -1
- hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -3
- hanzo_mcp/tools/mcp/mcp_add.py +27 -25
- hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
- hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
- hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
- hanzo_mcp/tools/memory/__init__.py +39 -21
- hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
- hanzo_mcp/tools/memory/memory_tools.py +90 -108
- hanzo_mcp/tools/search/__init__.py +7 -2
- hanzo_mcp/tools/search/find_tool.py +297 -212
- hanzo_mcp/tools/search/unified_search.py +366 -314
- hanzo_mcp/tools/shell/__init__.py +8 -7
- hanzo_mcp/tools/shell/auto_background.py +56 -49
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +75 -75
- hanzo_mcp/tools/shell/bash_session.py +2 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
- hanzo_mcp/tools/shell/bash_tool.py +24 -31
- hanzo_mcp/tools/shell/command_executor.py +12 -12
- hanzo_mcp/tools/shell/logs.py +43 -33
- hanzo_mcp/tools/shell/npx.py +13 -13
- hanzo_mcp/tools/shell/npx_background.py +24 -21
- hanzo_mcp/tools/shell/npx_tool.py +18 -22
- hanzo_mcp/tools/shell/open.py +19 -21
- hanzo_mcp/tools/shell/pkill.py +31 -26
- hanzo_mcp/tools/shell/process_tool.py +32 -32
- hanzo_mcp/tools/shell/processes.py +57 -58
- hanzo_mcp/tools/shell/run_background.py +24 -25
- hanzo_mcp/tools/shell/run_command.py +5 -5
- hanzo_mcp/tools/shell/run_command_windows.py +5 -5
- hanzo_mcp/tools/shell/session_storage.py +3 -3
- hanzo_mcp/tools/shell/streaming_command.py +141 -126
- hanzo_mcp/tools/shell/uvx.py +24 -25
- hanzo_mcp/tools/shell/uvx_background.py +35 -33
- hanzo_mcp/tools/shell/uvx_tool.py +18 -22
- hanzo_mcp/tools/todo/__init__.py +6 -2
- hanzo_mcp/tools/todo/todo.py +50 -37
- hanzo_mcp/tools/todo/todo_read.py +5 -8
- hanzo_mcp/tools/todo/todo_write.py +5 -7
- hanzo_mcp/tools/vector/__init__.py +40 -28
- hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
- hanzo_mcp/tools/vector/git_ingester.py +170 -179
- hanzo_mcp/tools/vector/index_tool.py +96 -44
- hanzo_mcp/tools/vector/infinity_store.py +283 -228
- hanzo_mcp/tools/vector/mock_infinity.py +39 -40
- hanzo_mcp/tools/vector/project_manager.py +88 -78
- hanzo_mcp/tools/vector/vector.py +59 -42
- hanzo_mcp/tools/vector/vector_index.py +30 -27
- hanzo_mcp/tools/vector/vector_search.py +64 -45
- hanzo_mcp/types.py +6 -4
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
- hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
- hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
"""Streaming command execution with disk-based logging and session management."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
3
|
import os
|
|
6
|
-
import
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
|
-
import tempfile
|
|
4
|
+
import json
|
|
10
5
|
import time
|
|
11
6
|
import uuid
|
|
12
|
-
|
|
7
|
+
import shutil
|
|
8
|
+
import asyncio
|
|
9
|
+
import subprocess
|
|
10
|
+
from typing import Any, Dict, List, Union, Optional
|
|
13
11
|
from pathlib import Path
|
|
14
|
-
from
|
|
12
|
+
from datetime import datetime, timedelta
|
|
15
13
|
|
|
16
|
-
from hanzo_mcp.tools.common.base import BaseTool
|
|
17
14
|
from hanzo_mcp.tools.shell.base_process import BaseProcessTool
|
|
18
15
|
|
|
19
16
|
|
|
20
17
|
class StreamingCommandTool(BaseProcessTool):
|
|
21
18
|
"""Execute commands with disk-based streaming and session persistence.
|
|
22
|
-
|
|
19
|
+
|
|
23
20
|
Features:
|
|
24
21
|
- All output streamed directly to disk (no memory usage)
|
|
25
22
|
- Session-based organization of logs
|
|
@@ -27,46 +24,46 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
27
24
|
- Forgiving parameter handling for AI usage
|
|
28
25
|
- Automatic session detection from MCP context
|
|
29
26
|
"""
|
|
30
|
-
|
|
27
|
+
|
|
31
28
|
name = "streaming_command"
|
|
32
29
|
description = "Run commands with disk-based output streaming and easy resumption"
|
|
33
|
-
|
|
30
|
+
|
|
34
31
|
# Base directory for all session data
|
|
35
32
|
SESSION_BASE_DIR = Path.home() / ".hanzo" / "sessions"
|
|
36
|
-
|
|
33
|
+
|
|
37
34
|
# Chunk size for streaming (25k tokens ≈ 100KB)
|
|
38
35
|
STREAM_CHUNK_SIZE = 100_000
|
|
39
|
-
|
|
36
|
+
|
|
40
37
|
# Session retention
|
|
41
38
|
SESSION_RETENTION_DAYS = 30
|
|
42
|
-
|
|
39
|
+
|
|
43
40
|
def __init__(self):
|
|
44
41
|
"""Initialize the streaming command tool."""
|
|
45
42
|
super().__init__()
|
|
46
43
|
self.session_id = self._get_or_create_session()
|
|
47
44
|
self.session_dir = self.SESSION_BASE_DIR / self.session_id
|
|
48
45
|
self.session_dir.mkdir(parents=True, exist_ok=True)
|
|
49
|
-
|
|
46
|
+
|
|
50
47
|
# Create subdirectories
|
|
51
48
|
self.commands_dir = self.session_dir / "commands"
|
|
52
49
|
self.commands_dir.mkdir(exist_ok=True)
|
|
53
|
-
|
|
50
|
+
|
|
54
51
|
# Session metadata file
|
|
55
52
|
self.session_meta_file = self.session_dir / "session.json"
|
|
56
53
|
self._update_session_metadata()
|
|
57
|
-
|
|
54
|
+
|
|
58
55
|
# Cleanup old sessions on init
|
|
59
56
|
self._cleanup_old_sessions()
|
|
60
|
-
|
|
57
|
+
|
|
61
58
|
def _get_or_create_session(self) -> str:
|
|
62
59
|
"""Get session ID from MCP context or create a new one.
|
|
63
|
-
|
|
60
|
+
|
|
64
61
|
Returns:
|
|
65
62
|
Session ID string
|
|
66
63
|
"""
|
|
67
64
|
# Try to get from environment (MCP might set this)
|
|
68
65
|
session_id = os.environ.get("MCP_SESSION_ID")
|
|
69
|
-
|
|
66
|
+
|
|
70
67
|
if not session_id:
|
|
71
68
|
# Try to get from Claude Desktop session marker
|
|
72
69
|
claude_session = os.environ.get("CLAUDE_SESSION_ID")
|
|
@@ -76,9 +73,9 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
76
73
|
# Generate new session ID with timestamp
|
|
77
74
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
78
75
|
session_id = f"session_{timestamp}_{uuid.uuid4().hex[:8]}"
|
|
79
|
-
|
|
76
|
+
|
|
80
77
|
return session_id
|
|
81
|
-
|
|
78
|
+
|
|
82
79
|
def _update_session_metadata(self) -> None:
|
|
83
80
|
"""Update session metadata file."""
|
|
84
81
|
metadata = {
|
|
@@ -89,9 +86,9 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
89
86
|
"session_id": os.environ.get("MCP_SESSION_ID"),
|
|
90
87
|
"claude_session": os.environ.get("CLAUDE_SESSION_ID"),
|
|
91
88
|
"user": os.environ.get("USER"),
|
|
92
|
-
}
|
|
89
|
+
},
|
|
93
90
|
}
|
|
94
|
-
|
|
91
|
+
|
|
95
92
|
# Merge with existing metadata if present
|
|
96
93
|
if self.session_meta_file.exists():
|
|
97
94
|
try:
|
|
@@ -100,37 +97,39 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
100
97
|
metadata["created"] = existing.get("created", metadata["created"])
|
|
101
98
|
except Exception:
|
|
102
99
|
pass
|
|
103
|
-
|
|
100
|
+
|
|
104
101
|
with open(self.session_meta_file, "w") as f:
|
|
105
102
|
json.dump(metadata, f, indent=2)
|
|
106
|
-
|
|
103
|
+
|
|
107
104
|
def _cleanup_old_sessions(self) -> None:
|
|
108
105
|
"""Remove sessions older than retention period."""
|
|
109
106
|
if not self.SESSION_BASE_DIR.exists():
|
|
110
107
|
return
|
|
111
|
-
|
|
108
|
+
|
|
112
109
|
cutoff = datetime.now() - timedelta(days=self.SESSION_RETENTION_DAYS)
|
|
113
|
-
|
|
110
|
+
|
|
114
111
|
for session_dir in self.SESSION_BASE_DIR.iterdir():
|
|
115
112
|
if not session_dir.is_dir():
|
|
116
113
|
continue
|
|
117
|
-
|
|
114
|
+
|
|
118
115
|
meta_file = session_dir / "session.json"
|
|
119
116
|
if meta_file.exists():
|
|
120
117
|
try:
|
|
121
118
|
with open(meta_file, "r") as f:
|
|
122
119
|
meta = json.load(f)
|
|
123
|
-
last_accessed = datetime.fromisoformat(
|
|
120
|
+
last_accessed = datetime.fromisoformat(
|
|
121
|
+
meta.get("last_accessed", "")
|
|
122
|
+
)
|
|
124
123
|
if last_accessed < cutoff:
|
|
125
124
|
shutil.rmtree(session_dir)
|
|
126
125
|
except Exception:
|
|
127
126
|
# If we can't read metadata, check directory mtime
|
|
128
127
|
if datetime.fromtimestamp(session_dir.stat().st_mtime) < cutoff:
|
|
129
128
|
shutil.rmtree(session_dir)
|
|
130
|
-
|
|
129
|
+
|
|
131
130
|
def _normalize_command_ref(self, ref: Union[str, int, None]) -> Optional[str]:
|
|
132
131
|
"""Normalize various command reference formats.
|
|
133
|
-
|
|
132
|
+
|
|
134
133
|
Args:
|
|
135
134
|
ref: Command reference - can be:
|
|
136
135
|
- Full command ID (UUID)
|
|
@@ -138,15 +137,15 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
138
137
|
- Index number (1, 2, 3...)
|
|
139
138
|
- "last" or "latest"
|
|
140
139
|
- None
|
|
141
|
-
|
|
140
|
+
|
|
142
141
|
Returns:
|
|
143
142
|
Full command ID or None
|
|
144
143
|
"""
|
|
145
144
|
if not ref:
|
|
146
145
|
return None
|
|
147
|
-
|
|
146
|
+
|
|
148
147
|
ref_str = str(ref).strip().lower()
|
|
149
|
-
|
|
148
|
+
|
|
150
149
|
# Handle special cases
|
|
151
150
|
if ref_str in ["last", "latest", "recent"]:
|
|
152
151
|
# Get most recent command
|
|
@@ -155,37 +154,39 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
155
154
|
return None
|
|
156
155
|
latest = max(commands, key=lambda p: p.stat().st_mtime)
|
|
157
156
|
return latest.parent.name
|
|
158
|
-
|
|
157
|
+
|
|
159
158
|
# Handle numeric index (1-based for user friendliness)
|
|
160
159
|
if ref_str.isdigit():
|
|
161
160
|
index = int(ref_str) - 1
|
|
162
|
-
commands = sorted(
|
|
163
|
-
|
|
161
|
+
commands = sorted(
|
|
162
|
+
self.commands_dir.glob("*/metadata.json"),
|
|
163
|
+
key=lambda p: p.stat().st_mtime,
|
|
164
|
+
)
|
|
164
165
|
if 0 <= index < len(commands):
|
|
165
166
|
return commands[index].parent.name
|
|
166
167
|
return None
|
|
167
|
-
|
|
168
|
+
|
|
168
169
|
# Handle short ID (first 8 chars)
|
|
169
170
|
if len(ref_str) >= 8:
|
|
170
171
|
# Could be short or full ID
|
|
171
172
|
for cmd_dir in self.commands_dir.iterdir():
|
|
172
173
|
if cmd_dir.name.startswith(ref_str):
|
|
173
174
|
return cmd_dir.name
|
|
174
|
-
|
|
175
|
+
|
|
175
176
|
return None
|
|
176
|
-
|
|
177
|
+
|
|
177
178
|
async def call(self, ctx: Any, **kwargs) -> Dict[str, Any]:
|
|
178
179
|
"""MCP tool entry point.
|
|
179
|
-
|
|
180
|
+
|
|
180
181
|
Args:
|
|
181
182
|
ctx: MCP context
|
|
182
183
|
**kwargs: Tool arguments
|
|
183
|
-
|
|
184
|
+
|
|
184
185
|
Returns:
|
|
185
186
|
Tool result
|
|
186
187
|
"""
|
|
187
188
|
return await self.run(**kwargs)
|
|
188
|
-
|
|
189
|
+
|
|
189
190
|
async def run(
|
|
190
191
|
self,
|
|
191
192
|
command: Optional[str] = None,
|
|
@@ -199,7 +200,7 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
199
200
|
chunk_size: Optional[Union[int, str]] = None,
|
|
200
201
|
) -> Dict[str, Any]:
|
|
201
202
|
"""Execute or continue reading a command with maximum forgiveness.
|
|
202
|
-
|
|
203
|
+
|
|
203
204
|
Args:
|
|
204
205
|
command/cmd: The command to execute (either works)
|
|
205
206
|
working_dir/cwd: Directory to run in (either works)
|
|
@@ -207,7 +208,7 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
207
208
|
continue_from/resume: Continue reading output from a command
|
|
208
209
|
from_byte: Specific byte position to read from
|
|
209
210
|
chunk_size: Custom chunk size for this read
|
|
210
|
-
|
|
211
|
+
|
|
211
212
|
Returns:
|
|
212
213
|
Command output with metadata for easy continuation
|
|
213
214
|
"""
|
|
@@ -215,7 +216,7 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
215
216
|
command = command or cmd
|
|
216
217
|
working_dir = working_dir or cwd
|
|
217
218
|
continue_from = continue_from or resume
|
|
218
|
-
|
|
219
|
+
|
|
219
220
|
# Convert string numbers to int
|
|
220
221
|
if isinstance(timeout, str) and timeout.isdigit():
|
|
221
222
|
timeout = int(timeout)
|
|
@@ -223,13 +224,13 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
223
224
|
from_byte = int(from_byte)
|
|
224
225
|
if isinstance(chunk_size, str) and chunk_size.isdigit():
|
|
225
226
|
chunk_size = int(chunk_size)
|
|
226
|
-
|
|
227
|
+
|
|
227
228
|
chunk_size = chunk_size or self.STREAM_CHUNK_SIZE
|
|
228
|
-
|
|
229
|
+
|
|
229
230
|
# Handle continuation
|
|
230
231
|
if continue_from:
|
|
231
232
|
return await self._continue_reading(continue_from, from_byte, chunk_size)
|
|
232
|
-
|
|
233
|
+
|
|
233
234
|
# Need a command for new execution
|
|
234
235
|
if not command:
|
|
235
236
|
return {
|
|
@@ -237,10 +238,12 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
237
238
|
"hint": "To continue a previous command, use 'continue_from' with command ID or number.",
|
|
238
239
|
"recent_commands": await self._get_recent_commands(),
|
|
239
240
|
}
|
|
240
|
-
|
|
241
|
+
|
|
241
242
|
# Execute new command
|
|
242
|
-
return await self._execute_new_command(
|
|
243
|
-
|
|
243
|
+
return await self._execute_new_command(
|
|
244
|
+
command, working_dir, timeout, chunk_size
|
|
245
|
+
)
|
|
246
|
+
|
|
244
247
|
async def _execute_new_command(
|
|
245
248
|
self,
|
|
246
249
|
command: str,
|
|
@@ -253,12 +256,12 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
253
256
|
cmd_id = str(uuid.uuid4())
|
|
254
257
|
cmd_dir = self.commands_dir / cmd_id
|
|
255
258
|
cmd_dir.mkdir()
|
|
256
|
-
|
|
259
|
+
|
|
257
260
|
# File paths
|
|
258
261
|
output_file = cmd_dir / "output.log"
|
|
259
262
|
error_file = cmd_dir / "error.log"
|
|
260
263
|
metadata_file = cmd_dir / "metadata.json"
|
|
261
|
-
|
|
264
|
+
|
|
262
265
|
# Save metadata
|
|
263
266
|
metadata = {
|
|
264
267
|
"command_id": cmd_id,
|
|
@@ -268,10 +271,10 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
268
271
|
"timeout": timeout,
|
|
269
272
|
"status": "running",
|
|
270
273
|
}
|
|
271
|
-
|
|
274
|
+
|
|
272
275
|
with open(metadata_file, "w") as f:
|
|
273
276
|
json.dump(metadata, f, indent=2)
|
|
274
|
-
|
|
277
|
+
|
|
275
278
|
# Start process with output redirection
|
|
276
279
|
try:
|
|
277
280
|
process = await asyncio.create_subprocess_shell(
|
|
@@ -280,7 +283,7 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
280
283
|
stderr=asyncio.subprocess.PIPE,
|
|
281
284
|
cwd=working_dir,
|
|
282
285
|
)
|
|
283
|
-
|
|
286
|
+
|
|
284
287
|
# Create tasks for streaming stdout and stderr to files
|
|
285
288
|
async def stream_to_file(stream, file_path):
|
|
286
289
|
"""Stream from async pipe to file."""
|
|
@@ -291,32 +294,38 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
291
294
|
break
|
|
292
295
|
f.write(chunk)
|
|
293
296
|
f.flush() # Ensure immediate write
|
|
294
|
-
|
|
297
|
+
|
|
295
298
|
# Start streaming tasks
|
|
296
|
-
stdout_task = asyncio.create_task(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
+
stdout_task = asyncio.create_task(
|
|
300
|
+
stream_to_file(process.stdout, output_file)
|
|
301
|
+
)
|
|
302
|
+
stderr_task = asyncio.create_task(
|
|
303
|
+
stream_to_file(process.stderr, error_file)
|
|
304
|
+
)
|
|
305
|
+
|
|
299
306
|
# Wait for initial output or timeout
|
|
300
307
|
start_time = time.time()
|
|
301
|
-
initial_timeout = min(
|
|
302
|
-
|
|
308
|
+
initial_timeout = min(
|
|
309
|
+
timeout or 5, 5
|
|
310
|
+
) # Wait max 5 seconds for initial output
|
|
311
|
+
|
|
303
312
|
while time.time() - start_time < initial_timeout:
|
|
304
313
|
if output_file.stat().st_size > 0 or error_file.stat().st_size > 0:
|
|
305
314
|
break
|
|
306
315
|
await asyncio.sleep(0.1)
|
|
307
|
-
|
|
316
|
+
|
|
308
317
|
# Read initial chunk
|
|
309
318
|
output_content = ""
|
|
310
319
|
error_content = ""
|
|
311
|
-
|
|
320
|
+
|
|
312
321
|
if output_file.exists() and output_file.stat().st_size > 0:
|
|
313
322
|
with open(output_file, "r", errors="replace") as f:
|
|
314
323
|
output_content = f.read(chunk_size)
|
|
315
|
-
|
|
324
|
+
|
|
316
325
|
if error_file.exists() and error_file.stat().st_size > 0:
|
|
317
326
|
with open(error_file, "r", errors="replace") as f:
|
|
318
327
|
error_content = f.read(1000) # Just first 1KB of errors
|
|
319
|
-
|
|
328
|
+
|
|
320
329
|
# Check if process completed quickly
|
|
321
330
|
try:
|
|
322
331
|
await asyncio.wait_for(process.wait(), timeout=0.1)
|
|
@@ -325,16 +334,16 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
325
334
|
except asyncio.TimeoutError:
|
|
326
335
|
exit_code = None
|
|
327
336
|
status = "running"
|
|
328
|
-
|
|
337
|
+
|
|
329
338
|
# Update metadata
|
|
330
339
|
metadata["status"] = status
|
|
331
340
|
if exit_code is not None:
|
|
332
341
|
metadata["exit_code"] = exit_code
|
|
333
342
|
metadata["end_time"] = datetime.now().isoformat()
|
|
334
|
-
|
|
343
|
+
|
|
335
344
|
with open(metadata_file, "w") as f:
|
|
336
345
|
json.dump(metadata, f, indent=2)
|
|
337
|
-
|
|
346
|
+
|
|
338
347
|
# Build response
|
|
339
348
|
result = {
|
|
340
349
|
"command_id": cmd_id,
|
|
@@ -345,13 +354,13 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
345
354
|
"bytes_read": len(output_content),
|
|
346
355
|
"session_path": str(cmd_dir),
|
|
347
356
|
}
|
|
348
|
-
|
|
357
|
+
|
|
349
358
|
if error_content:
|
|
350
359
|
result["stderr"] = error_content
|
|
351
|
-
|
|
360
|
+
|
|
352
361
|
if exit_code is not None:
|
|
353
362
|
result["exit_code"] = exit_code
|
|
354
|
-
|
|
363
|
+
|
|
355
364
|
# Add continuation info if more output available
|
|
356
365
|
total_size = output_file.stat().st_size
|
|
357
366
|
if total_size > len(output_content) or status == "running":
|
|
@@ -366,30 +375,30 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
366
375
|
f"Command {'is still running' if status == 'running' else 'has more output'}. "
|
|
367
376
|
f"Use any of: {', '.join(result['continue_hints'])}"
|
|
368
377
|
)
|
|
369
|
-
|
|
378
|
+
|
|
370
379
|
# Ensure tasks complete
|
|
371
380
|
if status == "completed":
|
|
372
381
|
await stdout_task
|
|
373
382
|
await stderr_task
|
|
374
|
-
|
|
383
|
+
|
|
375
384
|
return result
|
|
376
|
-
|
|
385
|
+
|
|
377
386
|
except Exception as e:
|
|
378
387
|
# Update metadata with error
|
|
379
388
|
metadata["status"] = "error"
|
|
380
389
|
metadata["error"] = str(e)
|
|
381
390
|
metadata["end_time"] = datetime.now().isoformat()
|
|
382
|
-
|
|
391
|
+
|
|
383
392
|
with open(metadata_file, "w") as f:
|
|
384
393
|
json.dump(metadata, f, indent=2)
|
|
385
|
-
|
|
394
|
+
|
|
386
395
|
return {
|
|
387
396
|
"error": str(e),
|
|
388
397
|
"command_id": cmd_id,
|
|
389
398
|
"short_id": cmd_id[:8],
|
|
390
399
|
"command": command,
|
|
391
400
|
}
|
|
392
|
-
|
|
401
|
+
|
|
393
402
|
async def _continue_reading(
|
|
394
403
|
self,
|
|
395
404
|
ref: Union[str, int],
|
|
@@ -399,33 +408,33 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
399
408
|
"""Continue reading output from a previous command."""
|
|
400
409
|
# Normalize reference
|
|
401
410
|
cmd_id = self._normalize_command_ref(ref)
|
|
402
|
-
|
|
411
|
+
|
|
403
412
|
if not cmd_id:
|
|
404
413
|
return {
|
|
405
414
|
"error": f"Command not found: {ref}",
|
|
406
415
|
"hint": "Use 'list' to see available commands",
|
|
407
416
|
"recent_commands": await self._get_recent_commands(),
|
|
408
417
|
}
|
|
409
|
-
|
|
418
|
+
|
|
410
419
|
cmd_dir = self.commands_dir / cmd_id
|
|
411
420
|
if not cmd_dir.exists():
|
|
412
421
|
return {"error": f"Command directory not found: {cmd_id}"}
|
|
413
|
-
|
|
422
|
+
|
|
414
423
|
# Load metadata
|
|
415
424
|
metadata_file = cmd_dir / "metadata.json"
|
|
416
425
|
with open(metadata_file, "r") as f:
|
|
417
426
|
metadata = json.load(f)
|
|
418
|
-
|
|
427
|
+
|
|
419
428
|
# Determine start position
|
|
420
429
|
output_file = cmd_dir / "output.log"
|
|
421
430
|
if not output_file.exists():
|
|
422
431
|
return {"error": "No output file found"}
|
|
423
|
-
|
|
432
|
+
|
|
424
433
|
# If no from_byte specified, read from where we left off
|
|
425
434
|
if from_byte is None:
|
|
426
435
|
# Try to determine from previous reads (could track this)
|
|
427
436
|
from_byte = 0 # For now, start from beginning if not specified
|
|
428
|
-
|
|
437
|
+
|
|
429
438
|
# Read chunk
|
|
430
439
|
try:
|
|
431
440
|
with open(output_file, "r", errors="replace") as f:
|
|
@@ -433,10 +442,10 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
433
442
|
content = f.read(chunk_size)
|
|
434
443
|
new_position = f.tell()
|
|
435
444
|
file_size = output_file.stat().st_size
|
|
436
|
-
|
|
445
|
+
|
|
437
446
|
# Check if process is still running
|
|
438
447
|
status = metadata.get("status", "unknown")
|
|
439
|
-
|
|
448
|
+
|
|
440
449
|
# Build response
|
|
441
450
|
result = {
|
|
442
451
|
"command_id": cmd_id,
|
|
@@ -449,13 +458,13 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
449
458
|
"read_to": new_position,
|
|
450
459
|
"total_bytes": file_size,
|
|
451
460
|
}
|
|
452
|
-
|
|
461
|
+
|
|
453
462
|
# Add stderr if needed
|
|
454
463
|
error_file = cmd_dir / "error.log"
|
|
455
464
|
if error_file.exists() and error_file.stat().st_size > 0:
|
|
456
465
|
with open(error_file, "r", errors="replace") as f:
|
|
457
466
|
result["stderr"] = f.read(1000)
|
|
458
|
-
|
|
467
|
+
|
|
459
468
|
# Add continuation info
|
|
460
469
|
if new_position < file_size or status == "running":
|
|
461
470
|
result["has_more"] = True
|
|
@@ -468,82 +477,88 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
468
477
|
f"{file_size - new_position} bytes remaining. "
|
|
469
478
|
f"Use: {result['continue_hints'][0]}"
|
|
470
479
|
)
|
|
471
|
-
|
|
480
|
+
|
|
472
481
|
return result
|
|
473
|
-
|
|
482
|
+
|
|
474
483
|
except Exception as e:
|
|
475
484
|
return {"error": f"Error reading output: {str(e)}"}
|
|
476
|
-
|
|
485
|
+
|
|
477
486
|
async def _get_recent_commands(self, limit: int = 5) -> List[Dict[str, Any]]:
|
|
478
487
|
"""Get list of recent commands for hints."""
|
|
479
488
|
commands = []
|
|
480
|
-
|
|
481
|
-
for cmd_dir in sorted(
|
|
482
|
-
|
|
483
|
-
|
|
489
|
+
|
|
490
|
+
for cmd_dir in sorted(
|
|
491
|
+
self.commands_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True
|
|
492
|
+
)[:limit]:
|
|
484
493
|
try:
|
|
485
494
|
with open(cmd_dir / "metadata.json", "r") as f:
|
|
486
495
|
meta = json.load(f)
|
|
487
|
-
|
|
496
|
+
|
|
488
497
|
output_size = 0
|
|
489
498
|
output_file = cmd_dir / "output.log"
|
|
490
499
|
if output_file.exists():
|
|
491
500
|
output_size = output_file.stat().st_size
|
|
492
|
-
|
|
493
|
-
commands.append(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
501
|
+
|
|
502
|
+
commands.append(
|
|
503
|
+
{
|
|
504
|
+
"id": meta["command_id"][:8],
|
|
505
|
+
"command": (
|
|
506
|
+
meta["command"][:50] + "..."
|
|
507
|
+
if len(meta["command"]) > 50
|
|
508
|
+
else meta["command"]
|
|
509
|
+
),
|
|
510
|
+
"status": meta.get("status", "unknown"),
|
|
511
|
+
"output_size": output_size,
|
|
512
|
+
"time": meta.get("start_time", ""),
|
|
513
|
+
}
|
|
514
|
+
)
|
|
500
515
|
except Exception:
|
|
501
516
|
continue
|
|
502
|
-
|
|
517
|
+
|
|
503
518
|
return commands
|
|
504
|
-
|
|
519
|
+
|
|
505
520
|
async def list(self, limit: Optional[int] = 10) -> Dict[str, Any]:
|
|
506
521
|
"""List recent commands in this session.
|
|
507
|
-
|
|
522
|
+
|
|
508
523
|
Args:
|
|
509
524
|
limit: Maximum number of commands to show
|
|
510
|
-
|
|
525
|
+
|
|
511
526
|
Returns:
|
|
512
527
|
List of recent commands with details
|
|
513
528
|
"""
|
|
514
529
|
commands = await self._get_recent_commands(limit or 10)
|
|
515
|
-
|
|
530
|
+
|
|
516
531
|
return {
|
|
517
532
|
"session_id": self.session_id,
|
|
518
533
|
"session_path": str(self.session_dir),
|
|
519
534
|
"commands": commands,
|
|
520
535
|
"hint": "Use continue_from='<id>' or resume='last' to read output",
|
|
521
536
|
}
|
|
522
|
-
|
|
537
|
+
|
|
523
538
|
async def tail(
|
|
524
539
|
self,
|
|
525
540
|
ref: Optional[Union[str, int]] = None,
|
|
526
541
|
lines: Optional[int] = 20,
|
|
527
542
|
) -> Dict[str, Any]:
|
|
528
543
|
"""Get the tail of a command's output (like 'tail -f').
|
|
529
|
-
|
|
544
|
+
|
|
530
545
|
Args:
|
|
531
546
|
ref: Command reference (defaults to 'last')
|
|
532
547
|
lines: Number of lines to show
|
|
533
|
-
|
|
548
|
+
|
|
534
549
|
Returns:
|
|
535
550
|
Last N lines of output
|
|
536
551
|
"""
|
|
537
552
|
ref = ref or "last"
|
|
538
553
|
cmd_id = self._normalize_command_ref(ref)
|
|
539
|
-
|
|
554
|
+
|
|
540
555
|
if not cmd_id:
|
|
541
556
|
return {"error": f"Command not found: {ref}"}
|
|
542
|
-
|
|
557
|
+
|
|
543
558
|
output_file = self.commands_dir / cmd_id / "output.log"
|
|
544
559
|
if not output_file.exists():
|
|
545
560
|
return {"error": "No output file found"}
|
|
546
|
-
|
|
561
|
+
|
|
547
562
|
try:
|
|
548
563
|
# Use tail command for efficiency
|
|
549
564
|
result = subprocess.run(
|
|
@@ -551,7 +566,7 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
551
566
|
capture_output=True,
|
|
552
567
|
text=True,
|
|
553
568
|
)
|
|
554
|
-
|
|
569
|
+
|
|
555
570
|
return {
|
|
556
571
|
"command_id": cmd_id[:8],
|
|
557
572
|
"output": result.stdout,
|
|
@@ -559,7 +574,7 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
559
574
|
}
|
|
560
575
|
except Exception as e:
|
|
561
576
|
return {"error": f"Error tailing output: {str(e)}"}
|
|
562
|
-
|
|
577
|
+
|
|
563
578
|
def get_params_schema(self) -> Dict[str, Any]:
|
|
564
579
|
"""Get parameter schema - very forgiving."""
|
|
565
580
|
return {
|
|
@@ -604,24 +619,24 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
604
619
|
},
|
|
605
620
|
"required": [], # No required fields for maximum forgiveness
|
|
606
621
|
}
|
|
607
|
-
|
|
622
|
+
|
|
608
623
|
def get_command_args(self, command: str, **kwargs) -> List[str]:
|
|
609
624
|
"""Get the command arguments for subprocess.
|
|
610
|
-
|
|
625
|
+
|
|
611
626
|
Args:
|
|
612
627
|
command: The command or script to run
|
|
613
628
|
**kwargs: Additional arguments (not used for shell commands)
|
|
614
|
-
|
|
629
|
+
|
|
615
630
|
Returns:
|
|
616
631
|
List of command arguments for subprocess
|
|
617
632
|
"""
|
|
618
633
|
# For shell commands, we use shell=True, so return the command as-is
|
|
619
634
|
return [command]
|
|
620
|
-
|
|
635
|
+
|
|
621
636
|
def get_tool_name(self) -> str:
|
|
622
637
|
"""Get the name of the tool being used.
|
|
623
|
-
|
|
638
|
+
|
|
624
639
|
Returns:
|
|
625
640
|
Tool name
|
|
626
641
|
"""
|
|
627
|
-
return "streaming_command"
|
|
642
|
+
return "streaming_command"
|