run-command-tool 0.1.0__tar.gz
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.
- run_command_tool-0.1.0/PKG-INFO +7 -0
- run_command_tool-0.1.0/README.md +0 -0
- run_command_tool-0.1.0/pyproject.toml +9 -0
- run_command_tool-0.1.0/run_command_tool/__init__.py +0 -0
- run_command_tool-0.1.0/run_command_tool/main.py +188 -0
- run_command_tool-0.1.0/run_command_tool.egg-info/PKG-INFO +7 -0
- run_command_tool-0.1.0/run_command_tool.egg-info/SOURCES.txt +9 -0
- run_command_tool-0.1.0/run_command_tool.egg-info/dependency_links.txt +1 -0
- run_command_tool-0.1.0/run_command_tool.egg-info/requires.txt +1 -0
- run_command_tool-0.1.0/run_command_tool.egg-info/top_level.txt +1 -0
- run_command_tool-0.1.0/setup.cfg +4 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
run_command Tool
|
|
3
|
+
================
|
|
4
|
+
Execute a shell command as a subprocess and return its stdout, stderr,
|
|
5
|
+
and exit code.
|
|
6
|
+
|
|
7
|
+
Parameters:
|
|
8
|
+
command - The full command line to run (required).
|
|
9
|
+
cwd - Absolute working directory path (optional).
|
|
10
|
+
Defaults to the current process working directory.
|
|
11
|
+
timeout - Max seconds to wait before killing the process (optional).
|
|
12
|
+
Default 60 seconds. Set 0 to disable.
|
|
13
|
+
wait_ms_before_async- Wait this many milliseconds for output, then return
|
|
14
|
+
without waiting for the process to finish (background mode).
|
|
15
|
+
Default 0 = wait until the process exits.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from typing import Any, Dict, Optional
|
|
23
|
+
|
|
24
|
+
from openchadpy.tool_base import ToolBase
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
DEFAULT_TIMEOUT_S = 60
|
|
29
|
+
MAX_OUTPUT_CHARS = 50_000 # cap stdout/stderr to avoid flooding context
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Tool(ToolBase):
|
|
33
|
+
name = "run_command"
|
|
34
|
+
description = (
|
|
35
|
+
"Execute a shell command as a subprocess and return its stdout, stderr, "
|
|
36
|
+
"and exit code. "
|
|
37
|
+
"Use for running scripts, build tools, tests, package managers, or any "
|
|
38
|
+
"CLI command. "
|
|
39
|
+
"Set wait_ms_before_async to a small value to launch a long-running "
|
|
40
|
+
"process in the background and get early output."
|
|
41
|
+
)
|
|
42
|
+
input_schema = {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"command": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "The full command line to execute.",
|
|
48
|
+
},
|
|
49
|
+
"cwd": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": (
|
|
52
|
+
"Absolute path of the working directory. "
|
|
53
|
+
"Defaults to the current process working directory."
|
|
54
|
+
),
|
|
55
|
+
},
|
|
56
|
+
"timeout": {
|
|
57
|
+
"type": "integer",
|
|
58
|
+
"description": (
|
|
59
|
+
"Max seconds to wait before killing the process. "
|
|
60
|
+
"Default 60. Set 0 to disable the timeout."
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
"wait_ms_before_async": {
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"description": (
|
|
66
|
+
"Wait this many milliseconds for output, then return early "
|
|
67
|
+
"without waiting for the process to finish (background / fire-and-forget mode). "
|
|
68
|
+
"Default 0 = wait until process exits."
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
"required": ["command"],
|
|
73
|
+
}
|
|
74
|
+
allowed_callers = ["direct", "code_execution"]
|
|
75
|
+
|
|
76
|
+
async def execute(self, **kwargs) -> Dict[str, Any]: # noqa: C901
|
|
77
|
+
command: str = kwargs.get("command", "").strip()
|
|
78
|
+
cwd: Optional[str] = kwargs.get("cwd")
|
|
79
|
+
timeout_s: int = int(kwargs.get("timeout", DEFAULT_TIMEOUT_S))
|
|
80
|
+
wait_ms: int = int(kwargs.get("wait_ms_before_async", 0))
|
|
81
|
+
|
|
82
|
+
if not command:
|
|
83
|
+
return {"error": "command is required and must not be empty."}
|
|
84
|
+
|
|
85
|
+
# Validate / resolve cwd
|
|
86
|
+
if cwd:
|
|
87
|
+
if not os.path.isdir(cwd):
|
|
88
|
+
return {"error": f"cwd does not exist or is not a directory: {cwd!r}"}
|
|
89
|
+
else:
|
|
90
|
+
cwd = os.getcwd()
|
|
91
|
+
|
|
92
|
+
# Choose shell
|
|
93
|
+
is_windows = sys.platform.startswith("win")
|
|
94
|
+
if is_windows:
|
|
95
|
+
# Use PowerShell on Windows for consistency with the IDE
|
|
96
|
+
shell_args = ["powershell", "-NonInteractive", "-Command", command]
|
|
97
|
+
else:
|
|
98
|
+
shell_args = ["/bin/sh", "-c", command]
|
|
99
|
+
|
|
100
|
+
logger.info(f"[run_command] cwd={cwd!r} cmd={command!r}")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
proc = await asyncio.create_subprocess_exec(
|
|
104
|
+
*shell_args,
|
|
105
|
+
stdout=asyncio.subprocess.PIPE,
|
|
106
|
+
stderr=asyncio.subprocess.PIPE,
|
|
107
|
+
cwd=cwd,
|
|
108
|
+
env={**os.environ, "PAGER": "cat"},
|
|
109
|
+
)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.error(f"[run_command] failed to start process: {e}")
|
|
112
|
+
return {"error": f"Failed to start process: {e}"}
|
|
113
|
+
|
|
114
|
+
# ── Background mode: wait only wait_ms then return early ──────
|
|
115
|
+
if wait_ms > 0:
|
|
116
|
+
try:
|
|
117
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
118
|
+
proc.communicate(), timeout=wait_ms / 1000.0
|
|
119
|
+
)
|
|
120
|
+
exit_code = proc.returncode
|
|
121
|
+
background = False
|
|
122
|
+
except asyncio.TimeoutError:
|
|
123
|
+
# Process is still running; collect whatever was buffered
|
|
124
|
+
stdout_bytes = b""
|
|
125
|
+
stderr_bytes = b""
|
|
126
|
+
exit_code = None
|
|
127
|
+
background = True
|
|
128
|
+
else:
|
|
129
|
+
# ── Foreground mode: wait up to timeout ───────────────────
|
|
130
|
+
try:
|
|
131
|
+
wait_coro = proc.communicate()
|
|
132
|
+
if timeout_s > 0:
|
|
133
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
134
|
+
wait_coro, timeout=float(timeout_s)
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
stdout_bytes, stderr_bytes = await wait_coro
|
|
138
|
+
exit_code = proc.returncode
|
|
139
|
+
background = False
|
|
140
|
+
except asyncio.TimeoutError:
|
|
141
|
+
proc.kill()
|
|
142
|
+
try:
|
|
143
|
+
stdout_bytes, stderr_bytes = await proc.communicate()
|
|
144
|
+
except Exception:
|
|
145
|
+
stdout_bytes = b""
|
|
146
|
+
stderr_bytes = b""
|
|
147
|
+
exit_code = proc.returncode
|
|
148
|
+
background = False
|
|
149
|
+
return {
|
|
150
|
+
"error": (
|
|
151
|
+
f"Command timed out after {timeout_s}s and was killed. "
|
|
152
|
+
"Increase timeout or use wait_ms_before_async for long-running commands."
|
|
153
|
+
),
|
|
154
|
+
"stdout": _decode_cap(stdout_bytes),
|
|
155
|
+
"stderr": _decode_cap(stderr_bytes),
|
|
156
|
+
"exit_code": exit_code,
|
|
157
|
+
"cwd": cwd,
|
|
158
|
+
"command": command,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
stdout_str = _decode_cap(stdout_bytes)
|
|
162
|
+
stderr_str = _decode_cap(stderr_bytes)
|
|
163
|
+
|
|
164
|
+
result: Dict[str, Any] = {
|
|
165
|
+
"command": command,
|
|
166
|
+
"cwd": cwd,
|
|
167
|
+
"exit_code": exit_code,
|
|
168
|
+
"background": background,
|
|
169
|
+
"stdout": stdout_str,
|
|
170
|
+
"stderr": stderr_str,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if exit_code is not None and exit_code != 0:
|
|
174
|
+
result["warning"] = f"Process exited with code {exit_code}."
|
|
175
|
+
|
|
176
|
+
logger.info(
|
|
177
|
+
f"[run_command] exit_code={exit_code} background={background} "
|
|
178
|
+
f"stdout_len={len(stdout_str)} stderr_len={len(stderr_str)}"
|
|
179
|
+
)
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _decode_cap(data: bytes) -> str:
|
|
184
|
+
"""Decode bytes and cap to MAX_OUTPUT_CHARS to avoid context flooding."""
|
|
185
|
+
text = data.decode("utf-8", errors="replace")
|
|
186
|
+
if len(text) > MAX_OUTPUT_CHARS:
|
|
187
|
+
text = text[:MAX_OUTPUT_CHARS] + f"\n... [truncated, {len(text)} total chars]"
|
|
188
|
+
return text
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
run_command_tool/__init__.py
|
|
4
|
+
run_command_tool/main.py
|
|
5
|
+
run_command_tool.egg-info/PKG-INFO
|
|
6
|
+
run_command_tool.egg-info/SOURCES.txt
|
|
7
|
+
run_command_tool.egg-info/dependency_links.txt
|
|
8
|
+
run_command_tool.egg-info/requires.txt
|
|
9
|
+
run_command_tool.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openchadpy>=0.1.23
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
run_command_tool
|