kader 2.6.2__tar.gz → 2.6.4__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.
Files changed (143) hide show
  1. {kader-2.6.2 → kader-2.6.4}/PKG-INFO +13 -1
  2. {kader-2.6.2 → kader-2.6.4}/README.md +12 -0
  3. {kader-2.6.2 → kader-2.6.4}/cli/app.py +22 -0
  4. {kader-2.6.2 → kader-2.6.4}/kader/tools/exec_commands.py +85 -27
  5. {kader-2.6.2 → kader-2.6.4}/kader/workflows/planner_executor.py +8 -1
  6. {kader-2.6.2 → kader-2.6.4}/pyproject.toml +1 -1
  7. {kader-2.6.2 → kader-2.6.4}/tests/tools/test_exec_commands.py +81 -69
  8. {kader-2.6.2 → kader-2.6.4}/uv.lock +1 -1
  9. {kader-2.6.2 → kader-2.6.4}/.github/workflows/ci.yml +0 -0
  10. {kader-2.6.2 → kader-2.6.4}/.github/workflows/release.yml +0 -0
  11. {kader-2.6.2 → kader-2.6.4}/.gitignore +0 -0
  12. {kader-2.6.2 → kader-2.6.4}/.kader/KADER.md +0 -0
  13. {kader-2.6.2 → kader-2.6.4}/.kader/skills/contributing-to-kader/SKILL.md +0 -0
  14. {kader-2.6.2 → kader-2.6.4}/.kader/skills/contributing-to-kader/assets/contributor_checklist.md +0 -0
  15. {kader-2.6.2 → kader-2.6.4}/.kader/skills/contributing-to-kader/references/kader_agent_instructions.md +0 -0
  16. {kader-2.6.2 → kader-2.6.4}/.kader/skills/contributing-to-kader/scripts/dev_helper.py +0 -0
  17. {kader-2.6.2 → kader-2.6.4}/.opencode/skills/contributing-to-kader/SKILL.md +0 -0
  18. {kader-2.6.2 → kader-2.6.4}/.opencode/skills/contributing-to-kader/assets/contributor_checklist.md +0 -0
  19. {kader-2.6.2 → kader-2.6.4}/.opencode/skills/contributing-to-kader/references/kader_agent_instructions.md +0 -0
  20. {kader-2.6.2 → kader-2.6.4}/.opencode/skills/contributing-to-kader/scripts/dev_helper.py +0 -0
  21. {kader-2.6.2 → kader-2.6.4}/.python-version +0 -0
  22. {kader-2.6.2 → kader-2.6.4}/AGENTS.md +0 -0
  23. {kader-2.6.2 → kader-2.6.4}/CONTRIBUTING.md +0 -0
  24. {kader-2.6.2 → kader-2.6.4}/LICENSE +0 -0
  25. {kader-2.6.2 → kader-2.6.4}/assets/architecture/cli_integration.mmd +0 -0
  26. {kader-2.6.2 → kader-2.6.4}/assets/architecture/cli_message_flow.mmd +0 -0
  27. {kader-2.6.2 → kader-2.6.4}/assets/architecture/cli_tool_confirmation.mmd +0 -0
  28. {kader-2.6.2 → kader-2.6.4}/assets/architecture/memory_flow.mmd +0 -0
  29. {kader-2.6.2 → kader-2.6.4}/assets/architecture/planner_executor_sequence.mmd +0 -0
  30. {kader-2.6.2 → kader-2.6.4}/assets/architecture/planner_executor_workflow.mmd +0 -0
  31. {kader-2.6.2 → kader-2.6.4}/assets/design/v2/code.html +0 -0
  32. {kader-2.6.2 → kader-2.6.4}/assets/design/v2/code1.html +0 -0
  33. {kader-2.6.2 → kader-2.6.4}/assets/design/v2/code2.html +0 -0
  34. {kader-2.6.2 → kader-2.6.4}/assets/design/v2/code3.html +0 -0
  35. {kader-2.6.2 → kader-2.6.4}/assets/design/v2/screen.png +0 -0
  36. {kader-2.6.2 → kader-2.6.4}/assets/design/v2/screen1.png +0 -0
  37. {kader-2.6.2 → kader-2.6.4}/assets/design/v2/screen2.png +0 -0
  38. {kader-2.6.2 → kader-2.6.4}/assets/design/v2/screen3.png +0 -0
  39. {kader-2.6.2 → kader-2.6.4}/cli/README.md +0 -0
  40. {kader-2.6.2 → kader-2.6.4}/cli/__init__.py +0 -0
  41. {kader-2.6.2 → kader-2.6.4}/cli/__main__.py +0 -0
  42. {kader-2.6.2 → kader-2.6.4}/cli/commands/__init__.py +0 -0
  43. {kader-2.6.2 → kader-2.6.4}/cli/commands/base.py +0 -0
  44. {kader-2.6.2 → kader-2.6.4}/cli/commands/initialize.py +0 -0
  45. {kader-2.6.2 → kader-2.6.4}/cli/llm_factory.py +0 -0
  46. {kader-2.6.2 → kader-2.6.4}/cli/utils.py +0 -0
  47. {kader-2.6.2 → kader-2.6.4}/examples/.gitignore +0 -0
  48. {kader-2.6.2 → kader-2.6.4}/examples/README.md +0 -0
  49. {kader-2.6.2 → kader-2.6.4}/examples/anthropic_example.py +0 -0
  50. {kader-2.6.2 → kader-2.6.4}/examples/google_example.py +0 -0
  51. {kader-2.6.2 → kader-2.6.4}/examples/memory_example.py +0 -0
  52. {kader-2.6.2 → kader-2.6.4}/examples/mistral_example.py +0 -0
  53. {kader-2.6.2 → kader-2.6.4}/examples/ollama_example.py +0 -0
  54. {kader-2.6.2 → kader-2.6.4}/examples/openai_compatible_example.py +0 -0
  55. {kader-2.6.2 → kader-2.6.4}/examples/planner_executor_example.py +0 -0
  56. {kader-2.6.2 → kader-2.6.4}/examples/planning_agent_example.py +0 -0
  57. {kader-2.6.2 → kader-2.6.4}/examples/python_developer/main.py +0 -0
  58. {kader-2.6.2 → kader-2.6.4}/examples/python_developer/template.yaml +0 -0
  59. {kader-2.6.2 → kader-2.6.4}/examples/react_agent_example.py +0 -0
  60. {kader-2.6.2 → kader-2.6.4}/examples/simple_agent.py +0 -0
  61. {kader-2.6.2 → kader-2.6.4}/examples/skills/hello_example.py +0 -0
  62. {kader-2.6.2 → kader-2.6.4}/examples/skills/react_agent.py +0 -0
  63. {kader-2.6.2 → kader-2.6.4}/examples/skills/skills/calculator/SKILL.md +0 -0
  64. {kader-2.6.2 → kader-2.6.4}/examples/skills/skills/calculator/scripts/calculate.py +0 -0
  65. {kader-2.6.2 → kader-2.6.4}/examples/skills/skills/github/SKILL.md +0 -0
  66. {kader-2.6.2 → kader-2.6.4}/examples/skills/skills/hello/SKILL.md +0 -0
  67. {kader-2.6.2 → kader-2.6.4}/examples/skills/skills/hello/scripts/hello.py +0 -0
  68. {kader-2.6.2 → kader-2.6.4}/examples/skills/skills/joke/SKILL.md +0 -0
  69. {kader-2.6.2 → kader-2.6.4}/examples/skills/skills/visualization/SKILL.md +0 -0
  70. {kader-2.6.2 → kader-2.6.4}/examples/todo_agent/main.py +0 -0
  71. {kader-2.6.2 → kader-2.6.4}/examples/tools_example.py +0 -0
  72. {kader-2.6.2 → kader-2.6.4}/kader/README.md +0 -0
  73. {kader-2.6.2 → kader-2.6.4}/kader/__init__.py +0 -0
  74. {kader-2.6.2 → kader-2.6.4}/kader/agent/__init__.py +0 -0
  75. {kader-2.6.2 → kader-2.6.4}/kader/agent/agents.py +0 -0
  76. {kader-2.6.2 → kader-2.6.4}/kader/agent/base.py +0 -0
  77. {kader-2.6.2 → kader-2.6.4}/kader/agent/logger.py +0 -0
  78. {kader-2.6.2 → kader-2.6.4}/kader/config.py +0 -0
  79. {kader-2.6.2 → kader-2.6.4}/kader/memory/__init__.py +0 -0
  80. {kader-2.6.2 → kader-2.6.4}/kader/memory/compression.py +0 -0
  81. {kader-2.6.2 → kader-2.6.4}/kader/memory/conversation.py +0 -0
  82. {kader-2.6.2 → kader-2.6.4}/kader/memory/session.py +0 -0
  83. {kader-2.6.2 → kader-2.6.4}/kader/memory/state.py +0 -0
  84. {kader-2.6.2 → kader-2.6.4}/kader/memory/summarization.py +0 -0
  85. {kader-2.6.2 → kader-2.6.4}/kader/memory/types.py +0 -0
  86. {kader-2.6.2 → kader-2.6.4}/kader/prompts/__init__.py +0 -0
  87. {kader-2.6.2 → kader-2.6.4}/kader/prompts/agent_prompts.py +0 -0
  88. {kader-2.6.2 → kader-2.6.4}/kader/prompts/base.py +0 -0
  89. {kader-2.6.2 → kader-2.6.4}/kader/prompts/cli_prompts.py +0 -0
  90. {kader-2.6.2 → kader-2.6.4}/kader/prompts/templates/executor_agent.j2 +0 -0
  91. {kader-2.6.2 → kader-2.6.4}/kader/prompts/templates/init_command_prompt.j2 +0 -0
  92. {kader-2.6.2 → kader-2.6.4}/kader/prompts/templates/kader_planner.j2 +0 -0
  93. {kader-2.6.2 → kader-2.6.4}/kader/prompts/templates/planning_agent.j2 +0 -0
  94. {kader-2.6.2 → kader-2.6.4}/kader/prompts/templates/react_agent.j2 +0 -0
  95. {kader-2.6.2 → kader-2.6.4}/kader/providers/__init__.py +0 -0
  96. {kader-2.6.2 → kader-2.6.4}/kader/providers/anthropic.py +0 -0
  97. {kader-2.6.2 → kader-2.6.4}/kader/providers/base.py +0 -0
  98. {kader-2.6.2 → kader-2.6.4}/kader/providers/google.py +0 -0
  99. {kader-2.6.2 → kader-2.6.4}/kader/providers/mistral.py +0 -0
  100. {kader-2.6.2 → kader-2.6.4}/kader/providers/mock.py +0 -0
  101. {kader-2.6.2 → kader-2.6.4}/kader/providers/ollama.py +0 -0
  102. {kader-2.6.2 → kader-2.6.4}/kader/providers/openai_compatible.py +0 -0
  103. {kader-2.6.2 → kader-2.6.4}/kader/tools/README.md +0 -0
  104. {kader-2.6.2 → kader-2.6.4}/kader/tools/__init__.py +0 -0
  105. {kader-2.6.2 → kader-2.6.4}/kader/tools/agent.py +0 -0
  106. {kader-2.6.2 → kader-2.6.4}/kader/tools/base.py +0 -0
  107. {kader-2.6.2 → kader-2.6.4}/kader/tools/filesys.py +0 -0
  108. {kader-2.6.2 → kader-2.6.4}/kader/tools/filesystem.py +0 -0
  109. {kader-2.6.2 → kader-2.6.4}/kader/tools/protocol.py +0 -0
  110. {kader-2.6.2 → kader-2.6.4}/kader/tools/rag.py +0 -0
  111. {kader-2.6.2 → kader-2.6.4}/kader/tools/skills.py +0 -0
  112. {kader-2.6.2 → kader-2.6.4}/kader/tools/todo.py +0 -0
  113. {kader-2.6.2 → kader-2.6.4}/kader/tools/utils.py +0 -0
  114. {kader-2.6.2 → kader-2.6.4}/kader/tools/web.py +0 -0
  115. {kader-2.6.2 → kader-2.6.4}/kader/utils/__init__.py +0 -0
  116. {kader-2.6.2 → kader-2.6.4}/kader/utils/checkpointer.py +0 -0
  117. {kader-2.6.2 → kader-2.6.4}/kader/utils/context_aggregator.py +0 -0
  118. {kader-2.6.2 → kader-2.6.4}/kader/utils/ignore.py +0 -0
  119. {kader-2.6.2 → kader-2.6.4}/kader/workflows/__init__.py +0 -0
  120. {kader-2.6.2 → kader-2.6.4}/kader/workflows/base.py +0 -0
  121. {kader-2.6.2 → kader-2.6.4}/tests/conftest.py +0 -0
  122. {kader-2.6.2 → kader-2.6.4}/tests/providers/test_anthropic_provider.py +0 -0
  123. {kader-2.6.2 → kader-2.6.4}/tests/providers/test_google.py +0 -0
  124. {kader-2.6.2 → kader-2.6.4}/tests/providers/test_mistral_provider.py +0 -0
  125. {kader-2.6.2 → kader-2.6.4}/tests/providers/test_mock.py +0 -0
  126. {kader-2.6.2 → kader-2.6.4}/tests/providers/test_ollama.py +0 -0
  127. {kader-2.6.2 → kader-2.6.4}/tests/providers/test_openai_compatible_provider.py +0 -0
  128. {kader-2.6.2 → kader-2.6.4}/tests/providers/test_providers_base.py +0 -0
  129. {kader-2.6.2 → kader-2.6.4}/tests/test_agent_logger.py +0 -0
  130. {kader-2.6.2 → kader-2.6.4}/tests/test_agent_logger_integration.py +0 -0
  131. {kader-2.6.2 → kader-2.6.4}/tests/test_base_agent.py +0 -0
  132. {kader-2.6.2 → kader-2.6.4}/tests/test_file_memory.py +0 -0
  133. {kader-2.6.2 → kader-2.6.4}/tests/test_hierarchical_memory.py +0 -0
  134. {kader-2.6.2 → kader-2.6.4}/tests/test_todo_tool.py +0 -0
  135. {kader-2.6.2 → kader-2.6.4}/tests/tools/test_agent_tool.py +0 -0
  136. {kader-2.6.2 → kader-2.6.4}/tests/tools/test_agent_tool_persistence.py +0 -0
  137. {kader-2.6.2 → kader-2.6.4}/tests/tools/test_agent_tool_skills.py +0 -0
  138. {kader-2.6.2 → kader-2.6.4}/tests/tools/test_filesys_tools.py +0 -0
  139. {kader-2.6.2 → kader-2.6.4}/tests/tools/test_filesystem_tools.py +0 -0
  140. {kader-2.6.2 → kader-2.6.4}/tests/tools/test_rag.py +0 -0
  141. {kader-2.6.2 → kader-2.6.4}/tests/tools/test_skills.py +0 -0
  142. {kader-2.6.2 → kader-2.6.4}/tests/tools/test_tools_base.py +0 -0
  143. {kader-2.6.2 → kader-2.6.4}/tests/tools/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kader
3
- Version: 2.6.2
3
+ Version: 2.6.4
4
4
  Summary: kader coding agent
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -69,6 +69,18 @@ uv sync
69
69
  uv run python -m cli
70
70
  ```
71
71
 
72
+ ### Using uv tool
73
+
74
+ With uv tool, you can install Kader globally and run it directly with the `kader` command:
75
+
76
+ ```bash
77
+ # Install Kader globally using uv tool
78
+ uv tool install kader
79
+
80
+ # Run the CLI
81
+ kader
82
+ ```
83
+
72
84
  ### Using pip
73
85
 
74
86
  ```bash
@@ -43,6 +43,18 @@ uv sync
43
43
  uv run python -m cli
44
44
  ```
45
45
 
46
+ ### Using uv tool
47
+
48
+ With uv tool, you can install Kader globally and run it directly with the `kader` command:
49
+
50
+ ```bash
51
+ # Install Kader globally using uv tool
52
+ uv tool install kader
53
+
54
+ # Run the CLI
55
+ kader
56
+ ```
57
+
46
58
  ### Using pip
47
59
 
48
60
  ```bash
@@ -133,11 +133,33 @@ class KaderApp:
133
133
  self.console.print(
134
134
  rf" [kader.green]\[+] {tool_name}[/kader.green] completed successfully"
135
135
  )
136
+ if tool_name == "execute_command" and ":\n" in result:
137
+ output = result.split(":\n", 1)[1]
138
+ self.console.print()
139
+ self.console.print(
140
+ Panel(
141
+ output.strip(),
142
+ title="[kader.orange]Command Output[/kader.orange]",
143
+ border_style="dark_orange",
144
+ padding=(0, 1),
145
+ )
146
+ )
136
147
  else:
137
148
  error_preview = result[:100] + "..." if len(result) > 100 else result
138
149
  self.console.print(
139
150
  rf" [kader.red]\[-] {tool_name}[/kader.red] failed: {error_preview}"
140
151
  )
152
+ if tool_name == "execute_command" and ":\n" in result:
153
+ output = result.split(":\n", 1)[1]
154
+ self.console.print()
155
+ self.console.print(
156
+ Panel(
157
+ output.strip(),
158
+ title="[kader.red]Command Output/Error[/kader.red]",
159
+ border_style="red",
160
+ padding=(0, 1),
161
+ )
162
+ )
141
163
  self._start_spinner()
142
164
 
143
165
  def _tool_confirmation_callback(self, message: str) -> tuple[bool, Optional[str]]:
@@ -173,9 +173,87 @@ class CommandExecutorTool(BaseTool[str]):
173
173
 
174
174
  return True, ""
175
175
 
176
+ def _stream_command_output(self, command: str, timeout: int) -> tuple[int, str]:
177
+ """
178
+ Streams the output of a shell command and collects it.
179
+
180
+ Args:
181
+ command: The command to execute
182
+ timeout: Timeout in seconds
183
+
184
+ Returns:
185
+ Tuple of (return_code, captured_output)
186
+ """
187
+ import time
188
+ from queue import Empty, Queue
189
+ from threading import Thread
190
+
191
+ # Prepare kwargs for Popen based on OS
192
+ kwargs = {
193
+ "stdout": subprocess.PIPE,
194
+ "stderr": subprocess.STDOUT,
195
+ "text": True,
196
+ "shell": True,
197
+ }
198
+
199
+ if self._host_os != "windows":
200
+ kwargs["executable"] = self._shell
201
+
202
+ process = subprocess.Popen(command, **kwargs)
203
+ captured_output = []
204
+
205
+ # Use a queue to capture output asynchronously for timeout handling
206
+ q = Queue()
207
+
208
+ def enqueue_output(out, queue):
209
+ for line in out:
210
+ queue.put(line)
211
+ try:
212
+ out.close()
213
+ except AttributeError:
214
+ pass # mock lists in tests might not have close()
215
+
216
+ t = Thread(target=enqueue_output, args=(process.stdout, q))
217
+ t.daemon = True
218
+ t.start()
219
+
220
+ start_time = time.time()
221
+
222
+ while True:
223
+ # Check for timeout
224
+ if timeout > 0 and time.time() - start_time > timeout:
225
+ process.terminate()
226
+ try:
227
+ process.wait(timeout=1)
228
+ except subprocess.TimeoutExpired:
229
+ process.kill()
230
+ raise subprocess.TimeoutExpired(
231
+ command, timeout, "".join(captured_output).encode()
232
+ )
233
+
234
+ try:
235
+ # Read line with a short timeout to allow checking overall execution timeout
236
+ line = q.get(timeout=0.1)
237
+
238
+ # Collect output
239
+ captured_output.append(line)
240
+ except Empty:
241
+ # Queue is empty, check if process indicates it's done
242
+ # Note: t.is_alive() is checked because poll() might return before stdout is fully read
243
+ if process.poll() is not None and not t.is_alive():
244
+ break
245
+
246
+ # Ensure thread finished
247
+ t.join(timeout=1)
248
+
249
+ # Ensure process finished
250
+ process.wait()
251
+
252
+ return process.returncode, "".join(captured_output).strip()
253
+
176
254
  def execute(self, command: str, timeout: int = 30) -> str:
177
255
  """
178
- Execute a command on the host system.
256
+ Execute a command on the host system and stream its output.
179
257
 
180
258
  Args:
181
259
  command: The command to execute
@@ -190,39 +268,19 @@ class CommandExecutorTool(BaseTool[str]):
190
268
  if not is_valid:
191
269
  return f"Validation Error: {reason}"
192
270
 
193
- # Execute the command
194
- if self._host_os == "windows":
195
- # On Windows, use shell=True to allow for more complex commands
196
- result = subprocess.run(
197
- command, shell=True, capture_output=True, text=True, timeout=timeout
198
- )
199
- else:
200
- # On Unix-like systems, use the appropriate shell
201
- result = subprocess.run(
202
- command,
203
- shell=True, # Using shell=True to handle pipes and redirections
204
- capture_output=True,
205
- text=True,
206
- timeout=timeout,
207
- executable=self._shell,
208
- )
271
+ # Execute and stream the command output
272
+ returncode, output = self._stream_command_output(command, timeout)
209
273
 
210
274
  # Format result as a string
211
- if result.returncode == 0:
212
- output = result.stdout.strip()
275
+ if returncode == 0:
213
276
  if output:
214
277
  return f"Command executed successfully:\n{output}"
215
278
  else:
216
279
  return "Command executed successfully (no output)"
217
280
  else:
218
- stdout = result.stdout.strip()
219
- stderr = result.stderr.strip()
220
-
221
- output_parts = [f"Command failed with exit code {result.returncode}"]
222
- if stdout:
223
- output_parts.append(stdout)
224
- if stderr:
225
- output_parts.append(stderr)
281
+ output_parts = [f"Command failed with exit code {returncode}"]
282
+ if output:
283
+ output_parts.append(output)
226
284
 
227
285
  return (
228
286
  ":\n".join(output_parts)
@@ -17,6 +17,7 @@ from kader.memory.types import get_default_memory_dir
17
17
  from kader.prompts import KaderPlannerPrompt
18
18
  from kader.providers.base import BaseLLMProvider, Message
19
19
  from kader.tools import AgentTool, TodoTool, ToolRegistry
20
+ from kader.tools.filesys import GlobTool, GrepTool, ReadDirectoryTool, ReadFileTool
20
21
  from kader.utils import Checkpointer
21
22
 
22
23
  from .base import BaseWorkflow
@@ -115,12 +116,18 @@ class PlannerExecutorWorkflow(BaseWorkflow):
115
116
  return None
116
117
 
117
118
  def _build_planner(self) -> PlanningAgent:
118
- """Build the PlanningAgent with TodoTool and AgentTool(s)."""
119
+ """Build the PlanningAgent with TodoTool, file system tools, and AgentTool(s)."""
119
120
  registry = ToolRegistry()
120
121
 
121
122
  # TodoTool is added implicitly by PlanningAgent, but we ensure it's there
122
123
  registry.register(TodoTool())
123
124
 
125
+ # Add reading and file system search tools
126
+ registry.register(ReadFileTool())
127
+ registry.register(ReadDirectoryTool())
128
+ registry.register(GlobTool())
129
+ registry.register(GrepTool())
130
+
124
131
  # Add AgentTool(s) for sub-task delegation
125
132
  for executor_name in self.executor_names:
126
133
  agent_tool = AgentTool(
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kader"
3
- version = "2.6.2"
3
+ version = "2.6.4"
4
4
  description = "kader coding agent"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -108,27 +108,27 @@ class TestCommandExecutorTool:
108
108
  assert "Empty command" in reason
109
109
 
110
110
  @patch("platform.system")
111
- @patch("subprocess.run")
112
- def test_execute_success(self, mock_subprocess_run, mock_platform_system):
111
+ @patch("subprocess.Popen")
112
+ def test_execute_success(self, mock_subprocess_popen, mock_platform_system):
113
113
  """Test successful command execution."""
114
114
  # Mock platform and subprocess
115
115
  mock_platform_system.return_value = "Linux"
116
- mock_result = Mock()
117
- mock_result.returncode = 0
118
- mock_result.stdout = "Command output"
119
- mock_result.stderr = ""
120
- mock_subprocess_run.return_value = mock_result
116
+ mock_process = Mock()
117
+ mock_process.returncode = 0
118
+ mock_process.stdout = ["Command output\n"]
119
+ mock_process.poll.return_value = 0
120
+ mock_subprocess_popen.return_value = mock_process
121
121
 
122
122
  tool = CommandExecutorTool()
123
123
  result = tool.execute("echo hello", timeout=10)
124
124
 
125
125
  # Verify subprocess was called correctly
126
- mock_subprocess_run.assert_called_once_with(
126
+ mock_subprocess_popen.assert_called_once_with(
127
127
  "echo hello",
128
- shell=True,
129
- capture_output=True,
128
+ stdout=subprocess.PIPE,
129
+ stderr=subprocess.STDOUT,
130
130
  text=True,
131
- timeout=10,
131
+ shell=True,
132
132
  executable="/bin/bash",
133
133
  )
134
134
 
@@ -137,27 +137,27 @@ class TestCommandExecutorTool:
137
137
  assert "Command output" in result
138
138
 
139
139
  @patch("platform.system")
140
- @patch("subprocess.run")
141
- def test_execute_success_windows(self, mock_subprocess_run, mock_platform_system):
140
+ @patch("subprocess.Popen")
141
+ def test_execute_success_windows(self, mock_subprocess_popen, mock_platform_system):
142
142
  """Test successful command execution on Windows."""
143
143
  # Mock platform and subprocess
144
144
  mock_platform_system.return_value = "Windows"
145
- mock_result = Mock()
146
- mock_result.returncode = 0
147
- mock_result.stdout = "Windows output"
148
- mock_result.stderr = ""
149
- mock_subprocess_run.return_value = mock_result
145
+ mock_process = Mock()
146
+ mock_process.returncode = 0
147
+ mock_process.stdout = ["Windows output\n"]
148
+ mock_process.poll.return_value = 0
149
+ mock_subprocess_popen.return_value = mock_process
150
150
 
151
151
  tool = CommandExecutorTool()
152
152
  result = tool.execute("echo hello")
153
153
 
154
154
  # Verify subprocess was called correctly for Windows
155
- mock_subprocess_run.assert_called_once_with(
155
+ mock_subprocess_popen.assert_called_once_with(
156
156
  "echo hello",
157
- shell=True,
158
- capture_output=True,
157
+ stdout=subprocess.PIPE,
158
+ stderr=subprocess.STDOUT,
159
159
  text=True,
160
- timeout=30, # Default timeout
160
+ shell=True,
161
161
  )
162
162
 
163
163
  # Verify the result
@@ -165,16 +165,16 @@ class TestCommandExecutorTool:
165
165
  assert "Windows output" in result
166
166
 
167
167
  @patch("platform.system")
168
- @patch("subprocess.run")
169
- def test_execute_command_failed(self, mock_subprocess_run, mock_platform_system):
168
+ @patch("subprocess.Popen")
169
+ def test_execute_command_failed(self, mock_subprocess_popen, mock_platform_system):
170
170
  """Test command execution that fails."""
171
171
  # Mock platform and subprocess
172
172
  mock_platform_system.return_value = "Linux"
173
- mock_result = Mock()
174
- mock_result.returncode = 1
175
- mock_result.stdout = ""
176
- mock_result.stderr = "Command failed"
177
- mock_subprocess_run.return_value = mock_result
173
+ mock_process = Mock()
174
+ mock_process.returncode = 1
175
+ mock_process.stdout = ["Command failed\n"]
176
+ mock_process.poll.return_value = 1
177
+ mock_subprocess_popen.return_value = mock_process
178
178
 
179
179
  tool = CommandExecutorTool()
180
180
  result = tool.execute("invalid_command")
@@ -184,38 +184,40 @@ class TestCommandExecutorTool:
184
184
  assert "Command failed" in result
185
185
 
186
186
  @patch("platform.system")
187
- @patch("subprocess.run")
187
+ @patch("subprocess.Popen")
188
188
  def test_execute_command_failed_with_output(
189
- self, mock_subprocess_run, mock_platform_system
189
+ self, mock_subprocess_popen, mock_platform_system
190
190
  ):
191
191
  """Test command execution that fails but has stdout."""
192
192
  # Mock platform and subprocess
193
193
  mock_platform_system.return_value = "Linux"
194
- mock_result = Mock()
195
- mock_result.returncode = 2
196
- mock_result.stdout = "Partial output"
197
- mock_result.stderr = "Error message"
198
- mock_subprocess_run.return_value = mock_result
194
+ mock_process = Mock()
195
+ mock_process.returncode = 2
196
+ mock_process.stdout = ["Partial output\n", "Error message\n"]
197
+ mock_process.poll.return_value = 2
198
+ mock_subprocess_popen.return_value = mock_process
199
199
 
200
200
  tool = CommandExecutorTool()
201
201
  result = tool.execute("problematic_command")
202
202
 
203
- # Verify the result includes both stdout and stderr
203
+ # Verify the result includes both stdout and stderr (which are merged in Popen)
204
204
  assert "Command failed with exit code 2" in result
205
205
  assert "Partial output" in result
206
206
  assert "Error message" in result
207
207
 
208
208
  @patch("platform.system")
209
- @patch("subprocess.run")
210
- def test_execute_command_no_output(self, mock_subprocess_run, mock_platform_system):
209
+ @patch("subprocess.Popen")
210
+ def test_execute_command_no_output(
211
+ self, mock_subprocess_popen, mock_platform_system
212
+ ):
211
213
  """Test command execution with no output."""
212
214
  # Mock platform and subprocess
213
215
  mock_platform_system.return_value = "Linux"
214
- mock_result = Mock()
215
- mock_result.returncode = 0
216
- mock_result.stdout = ""
217
- mock_result.stderr = ""
218
- mock_subprocess_run.return_value = mock_result
216
+ mock_process = Mock()
217
+ mock_process.returncode = 0
218
+ mock_process.stdout = []
219
+ mock_process.poll.return_value = 0
220
+ mock_subprocess_popen.return_value = mock_process
219
221
 
220
222
  tool = CommandExecutorTool()
221
223
  result = tool.execute("no_output_command")
@@ -224,26 +226,36 @@ class TestCommandExecutorTool:
224
226
  assert "Command executed successfully (no output)" in result
225
227
 
226
228
  @patch("platform.system")
227
- @patch("subprocess.run")
228
- def test_execute_command_timeout(self, mock_subprocess_run, mock_platform_system):
229
+ @patch("subprocess.Popen")
230
+ def test_execute_command_timeout(self, mock_subprocess_popen, mock_platform_system):
229
231
  """Test command execution that times out."""
230
- # Mock platform and subprocess to raise TimeoutExpired
232
+ # Mock platform and subprocess
231
233
  mock_platform_system.return_value = "Linux"
232
- mock_subprocess_run.side_effect = subprocess.TimeoutExpired("command", 5)
233
-
234
- tool = CommandExecutorTool()
235
- result = tool.execute("slow_command", timeout=5)
234
+ mock_process = Mock()
235
+ mock_process.stdout = []
236
+ mock_process.poll.return_value = None # Process is still running
237
+ mock_subprocess_popen.return_value = mock_process
238
+
239
+ # We need to simulate the slow process by making the real time pass faster or wait,
240
+ # but time.time() is used, so we need to patch time.time
241
+ with patch("time.time") as mock_time:
242
+ # First call is start time, second is in loop check
243
+ mock_time.side_effect = [100.0, 110.0]
244
+ tool = CommandExecutorTool()
245
+ result = tool.execute("slow_command", timeout=5)
236
246
 
237
247
  # Verify the result
238
248
  assert "Command timed out after 5 seconds" in result
239
249
 
240
250
  @patch("platform.system")
241
- @patch("subprocess.run")
242
- def test_execute_command_exception(self, mock_subprocess_run, mock_platform_system):
251
+ @patch("subprocess.Popen")
252
+ def test_execute_command_exception(
253
+ self, mock_subprocess_popen, mock_platform_system
254
+ ):
243
255
  """Test command execution that raises an exception."""
244
256
  # Mock platform and subprocess to raise a general exception
245
257
  mock_platform_system.return_value = "Linux"
246
- mock_subprocess_run.side_effect = Exception("Unexpected error")
258
+ mock_subprocess_popen.side_effect = Exception("Unexpected error")
247
259
 
248
260
  tool = CommandExecutorTool()
249
261
  result = tool.execute("error_command")
@@ -252,9 +264,9 @@ class TestCommandExecutorTool:
252
264
  assert "Execution Error: Unexpected error" in result
253
265
 
254
266
  @patch("platform.system")
255
- @patch("subprocess.run")
267
+ @patch("subprocess.Popen")
256
268
  def test_execute_invalid_command_windows(
257
- self, mock_subprocess_run, mock_platform_system
269
+ self, mock_subprocess_popen, mock_platform_system
258
270
  ):
259
271
  """Test executing an invalid command on Windows."""
260
272
  mock_platform_system.return_value = "Windows"
@@ -267,13 +279,13 @@ class TestCommandExecutorTool:
267
279
  assert "Unix/Linux command" in result
268
280
  assert "PowerShell" in result
269
281
 
270
- # subprocess.run should not be called
271
- mock_subprocess_run.assert_not_called()
282
+ # subprocess.Popen should not be called
283
+ mock_subprocess_popen.assert_not_called()
272
284
 
273
285
  @patch("platform.system")
274
- @patch("subprocess.run")
286
+ @patch("subprocess.Popen")
275
287
  def test_execute_invalid_command_unix(
276
- self, mock_subprocess_run, mock_platform_system
288
+ self, mock_subprocess_popen, mock_platform_system
277
289
  ):
278
290
  """Test executing an invalid command on Unix."""
279
291
  mock_platform_system.return_value = "Linux"
@@ -286,21 +298,21 @@ class TestCommandExecutorTool:
286
298
  assert "Windows command" in result
287
299
  assert "Unix equivalent" in result
288
300
 
289
- # subprocess.run should not be called
290
- mock_subprocess_run.assert_not_called()
301
+ # subprocess.Popen should not be called
302
+ mock_subprocess_popen.assert_not_called()
291
303
 
292
304
  @patch("platform.system")
293
- @patch("subprocess.run")
305
+ @patch("subprocess.Popen")
294
306
  @pytest.mark.asyncio
295
- async def test_aexecute(self, mock_subprocess_run, mock_platform_system):
307
+ async def test_aexecute(self, mock_subprocess_popen, mock_platform_system):
296
308
  """Test asynchronous command execution."""
297
309
  # Mock platform and subprocess
298
310
  mock_platform_system.return_value = "Linux"
299
- mock_result = Mock()
300
- mock_result.returncode = 0
301
- mock_result.stdout = "Async output"
302
- mock_result.stderr = ""
303
- mock_subprocess_run.return_value = mock_result
311
+ mock_process = Mock()
312
+ mock_process.returncode = 0
313
+ mock_process.stdout = ["Async output\n"]
314
+ mock_process.poll.return_value = 0
315
+ mock_subprocess_popen.return_value = mock_process
304
316
 
305
317
  tool = CommandExecutorTool()
306
318
  result = await tool.aexecute("echo async", timeout=15)
@@ -582,7 +582,7 @@ wheels = [
582
582
 
583
583
  [[package]]
584
584
  name = "kader"
585
- version = "2.6.2"
585
+ version = "2.6.4"
586
586
  source = { editable = "." }
587
587
  dependencies = [
588
588
  { name = "aiofiles" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes