devduck 1.1.0__py3-none-any.whl → 1.1.4__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 devduck might be problematic. Click here for more details.

devduck/__init__.py CHANGED
@@ -3,19 +3,31 @@
3
3
  🦆 devduck - extreme minimalist self-adapting agent
4
4
  one file. self-healing. runtime dependencies. adaptive.
5
5
  """
6
+ import os
6
7
  import sys
7
8
  import subprocess
8
- import os
9
+ import threading
9
10
  import platform
10
11
  import socket
11
12
  import logging
12
13
  import tempfile
13
14
  import time
14
15
  import warnings
16
+ import json
15
17
  from pathlib import Path
16
18
  from datetime import datetime
17
19
  from typing import Dict, Any
18
20
  from logging.handlers import RotatingFileHandler
21
+ from strands import Agent, tool
22
+
23
+ # Import system prompt helper for loading prompts from files
24
+ try:
25
+ from devduck.tools.system_prompt import _get_system_prompt
26
+ except ImportError:
27
+ # Fallback if tools module not available yet
28
+ def _get_system_prompt(repository=None, variable_name="SYSTEM_PROMPT"):
29
+ return os.getenv(variable_name, "")
30
+
19
31
 
20
32
  warnings.filterwarnings("ignore", message=".*pkg_resources is deprecated.*")
21
33
  warnings.filterwarnings("ignore", message=".*cache_prompt is deprecated.*")
@@ -594,7 +606,7 @@ class DevDuck:
594
606
  servers = {
595
607
  "tcp": {
596
608
  "port": int(os.getenv("DEVDUCK_TCP_PORT", "9999")),
597
- "enabled": os.getenv("DEVDUCK_ENABLE_TCP", "true").lower()
609
+ "enabled": os.getenv("DEVDUCK_ENABLE_TCP", "false").lower()
598
610
  == "true",
599
611
  },
600
612
  "ws": {
@@ -604,25 +616,93 @@ class DevDuck:
604
616
  },
605
617
  "mcp": {
606
618
  "port": int(os.getenv("DEVDUCK_MCP_PORT", "8000")),
607
- "enabled": os.getenv("DEVDUCK_ENABLE_MCP", "true").lower()
619
+ "enabled": os.getenv("DEVDUCK_ENABLE_MCP", "false").lower()
608
620
  == "true",
609
621
  },
610
622
  "ipc": {
611
623
  "socket_path": os.getenv(
612
624
  "DEVDUCK_IPC_SOCKET", "/tmp/devduck_main.sock"
613
625
  ),
614
- "enabled": os.getenv("DEVDUCK_ENABLE_IPC", "true").lower()
626
+ "enabled": os.getenv("DEVDUCK_ENABLE_IPC", "false").lower()
615
627
  == "true",
616
628
  },
617
629
  }
618
630
 
619
- self.servers = servers
631
+ # Show server configuration status
632
+ enabled_servers = []
633
+ disabled_servers = []
634
+ for server_name, config in servers.items():
635
+ if config.get("enabled", False):
636
+ if "port" in config:
637
+ enabled_servers.append(
638
+ f"{server_name.upper()}:{config['port']}"
639
+ )
640
+ else:
641
+ enabled_servers.append(server_name.upper())
642
+ else:
643
+ disabled_servers.append(server_name.upper())
620
644
 
621
- from strands import Agent, tool
645
+ logger.debug(
646
+ f"🦆 Server config: {', '.join(enabled_servers) if enabled_servers else 'none enabled'}"
647
+ )
648
+ if disabled_servers:
649
+ logger.debug(f"🦆 Disabled: {', '.join(disabled_servers)}")
650
+
651
+ self.servers = servers
622
652
 
623
653
  # Load tools with flexible configuration
624
- # Default tool config - user can override with DEVDUCK_TOOLS env var
625
- default_tools = "devduck.tools:system_prompt,store_in_kb,ipc,tcp,websocket,mcp_server,state_manager,tray,ambient,agentcore_config,agentcore_invoke,agentcore_logs,agentcore_agents,install_tools,create_subagent,use_github;strands_tools:shell,editor,file_read,file_write,image_reader,load_tool,retrieve,calculator,use_agent,environment,mcp_client,speak,slack;strands_fun_tools:listen,cursor,clipboard,screen_reader,bluetooth,yolo_vision"
654
+ # Default tool config
655
+ # Agent can load additional tools on-demand via fetch_github_tool
656
+
657
+ # 🔧 Available DevDuck Tools (load on-demand):
658
+ # - system_prompt: https://github.com/cagataycali/devduck/blob/main/devduck/tools/system_prompt.py
659
+ # - store_in_kb: https://github.com/cagataycali/devduck/blob/main/devduck/tools/store_in_kb.py
660
+ # - ipc: https://github.com/cagataycali/devduck/blob/main/devduck/tools/ipc.py
661
+ # - tcp: https://github.com/cagataycali/devduck/blob/main/devduck/tools/tcp.py
662
+ # - websocket: https://github.com/cagataycali/devduck/blob/main/devduck/tools/websocket.py
663
+ # - mcp_server: https://github.com/cagataycali/devduck/blob/main/devduck/tools/mcp_server.py
664
+ # - scraper: https://github.com/cagataycali/devduck/blob/main/devduck/tools/scraper.py
665
+ # - tray: https://github.com/cagataycali/devduck/blob/main/devduck/tools/tray.py
666
+ # - ambient: https://github.com/cagataycali/devduck/blob/main/devduck/tools/ambient.py
667
+ # - agentcore_config: https://github.com/cagataycali/devduck/blob/main/devduck/tools/agentcore_config.py
668
+ # - agentcore_invoke: https://github.com/cagataycali/devduck/blob/main/devduck/tools/agentcore_invoke.py
669
+ # - agentcore_logs: https://github.com/cagataycali/devduck/blob/main/devduck/tools/agentcore_logs.py
670
+ # - agentcore_agents: https://github.com/cagataycali/devduck/blob/main/devduck/tools/agentcore_agents.py
671
+ # - create_subagent: https://github.com/cagataycali/devduck/blob/main/devduck/tools/create_subagent.py
672
+ # - use_github: https://github.com/cagataycali/devduck/blob/main/devduck/tools/use_github.py
673
+ # - speech_to_speech: https://github.com/cagataycali/devduck/blob/main/devduck/tools/speech_to_speech.py
674
+ # - state_manager: https://github.com/cagataycali/devduck/blob/main/devduck/tools/state_manager.py
675
+
676
+ # 📦 Strands Tools
677
+ # - editor, file_read, file_write, image_reader, load_tool, retrieve
678
+ # - calculator, use_agent, environment, mcp_client, speak, slack
679
+
680
+ # 🎮 Strands Fun Tools
681
+ # - listen, cursor, clipboard, screen_reader, bluetooth, yolo_vision
682
+
683
+ # 🔍 Strands Google
684
+ # - use_google, google_auth
685
+
686
+ # 🔧 Auto-append server tools based on enabled servers
687
+ server_tools_needed = []
688
+ if servers.get("tcp", {}).get("enabled", False):
689
+ server_tools_needed.append("tcp")
690
+ if servers.get("ws", {}).get("enabled", True):
691
+ server_tools_needed.append("websocket")
692
+ if servers.get("mcp", {}).get("enabled", False):
693
+ server_tools_needed.append("mcp_server")
694
+ if servers.get("ipc", {}).get("enabled", False):
695
+ server_tools_needed.append("ipc")
696
+
697
+ # Append to default tools if any server tools are needed
698
+ if server_tools_needed:
699
+ server_tools_str = ",".join(server_tools_needed)
700
+ default_tools = f"devduck.tools:system_prompt,fetch_github_tool,{server_tools_str};strands_tools:shell"
701
+ logger.info(f"Auto-added server tools: {server_tools_str}")
702
+ else:
703
+ default_tools = (
704
+ "devduck.tools:system_prompt,fetch_github_tool;strands_tools:shell"
705
+ )
626
706
 
627
707
  tools_config = os.getenv("DEVDUCK_TOOLS", default_tools)
628
708
  logger.info(f"Loading tools from config: {tools_config}")
@@ -776,8 +856,6 @@ class DevDuck:
776
856
  Returns:
777
857
  List of MCPClient instances ready for direct use in Agent
778
858
  """
779
- import json
780
-
781
859
  mcp_servers_json = os.getenv("MCP_SERVERS")
782
860
  if not mcp_servers_json:
783
861
  logger.debug("No MCP_SERVERS environment variable found")
@@ -1145,7 +1223,7 @@ When you learn something valuable during conversations:
1145
1223
  - Communication: **MINIMAL WORDS**
1146
1224
  - Efficiency: **Speed is paramount**
1147
1225
 
1148
- {os.getenv('SYSTEM_PROMPT', '')}"""
1226
+ {_get_system_prompt()}"""
1149
1227
 
1150
1228
  def _self_heal(self, error):
1151
1229
  """Attempt self-healing when errors occur"""
@@ -1192,7 +1270,6 @@ When you learn something valuable during conversations:
1192
1270
 
1193
1271
  def _is_socket_available(self, socket_path):
1194
1272
  """Check if a Unix socket is available"""
1195
- import os
1196
1273
 
1197
1274
  # If socket file doesn't exist, it's available
1198
1275
  if not os.path.exists(socket_path):
@@ -1430,11 +1507,11 @@ When you learn something valuable during conversations:
1430
1507
  def restart(self):
1431
1508
  """Restart the agent"""
1432
1509
  print("\n🦆 Restarting...")
1510
+ logger.debug("\n🦆 Restarting...")
1433
1511
  self.__init__()
1434
1512
 
1435
1513
  def _start_file_watcher(self):
1436
1514
  """Start background file watcher for auto hot-reload"""
1437
- import threading
1438
1515
 
1439
1516
  logger.info("Starting file watcher for hot-reload")
1440
1517
  # Get the path to this file
@@ -1534,6 +1611,7 @@ When you learn something valuable during conversations:
1534
1611
  self._watcher_running = False
1535
1612
 
1536
1613
  print("\n🦆 Restarting process with fresh code...")
1614
+ logger.debug("\n🦆 Restarting process with fresh code...")
1537
1615
 
1538
1616
  # Restart the entire Python process
1539
1617
  # This ensures all code is freshly loaded
@@ -1677,7 +1755,7 @@ def interactive():
1677
1755
  print(f"📝 Logs: {LOG_DIR}")
1678
1756
  print("Type 'exit', 'quit', or 'q' to quit.")
1679
1757
  print("Prefix with ! to run shell commands (e.g., ! ls -la)")
1680
- print("-" * 50)
1758
+ print("\n\n")
1681
1759
  logger.info("Interactive mode started")
1682
1760
 
1683
1761
  # Set up prompt_toolkit with history
@@ -1705,7 +1783,6 @@ def interactive():
1705
1783
  auto_suggest=AutoSuggestFromHistory(),
1706
1784
  completer=completer,
1707
1785
  complete_while_typing=True,
1708
- mouse_support=False, # breaks scrolling when enabled
1709
1786
  )
1710
1787
 
1711
1788
  # Reset interrupt count on successful prompt
@@ -1733,6 +1810,10 @@ def interactive():
1733
1810
  )
1734
1811
  devduck._agent_executing = False
1735
1812
 
1813
+ # Reset terminal to fix rendering issues after command output
1814
+ print("\r", end="", flush=True)
1815
+ sys.stdout.flush()
1816
+
1736
1817
  # Append shell command to history
1737
1818
  append_to_shell_history(q, result["content"][0]["text"])
1738
1819
 
@@ -1747,6 +1828,9 @@ def interactive():
1747
1828
  except Exception as e:
1748
1829
  devduck._agent_executing = False # Reset on error
1749
1830
  print(f"🦆 Shell command error: {e}")
1831
+ # Reset terminal on error too
1832
+ print("\r", end="", flush=True)
1833
+ sys.stdout.flush()
1750
1834
  continue
1751
1835
 
1752
1836
  # Execute the agent with user input
devduck/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.1.0'
32
- __version_tuple__ = version_tuple = (1, 1, 0)
31
+ __version__ = version = '1.1.4'
32
+ __version_tuple__ = version_tuple = (1, 1, 4)
33
33
 
34
34
  __commit_id__ = commit_id = None
devduck/tools/__init__.py CHANGED
@@ -1,55 +1,47 @@
1
- """DevDuck tools package."""
1
+ """
2
+ DevDuck Tools Package
2
3
 
3
- from .tcp import tcp
4
- from .mcp_server import mcp_server
5
- from .install_tools import install_tools
6
- from .tray import tray
4
+ This module exports all available tools for devduck.
5
+ """
6
+
7
+ from .agentcore_agents import agentcore_agents
8
+ from .agentcore_config import agentcore_config
9
+ from .agentcore_invoke import agentcore_invoke
10
+ from .agentcore_logs import agentcore_logs
7
11
  from .ambient import ambient
8
- from .websocket import websocket
9
- from .ipc import ipc
10
- from .use_github import use_github
11
12
  from .create_subagent import create_subagent
13
+ from .fetch_github_tool import fetch_github_tool
14
+ from .install_tools import install_tools
15
+ from .ipc import ipc
16
+ from .mcp_server import mcp_server
17
+ from .scraper import scraper
18
+ from .speech_to_speech import speech_to_speech
19
+ from .state_manager import state_manager
12
20
  from .store_in_kb import store_in_kb
13
21
  from .system_prompt import system_prompt
14
- from .state_manager import state_manager
15
-
16
- # AgentCore tools (conditionally available)
17
- try:
18
- from .agentcore_config import agentcore_config
19
- from .agentcore_invoke import agentcore_invoke
20
- from .agentcore_logs import agentcore_logs
21
- from .agentcore_agents import agentcore_agents
22
+ from .tcp import tcp
23
+ from .tray import tray
24
+ from .use_github import use_github
25
+ from .websocket import websocket
22
26
 
23
- __all__ = [
24
- "tcp",
25
- "websocket",
26
- "ipc",
27
- "mcp_server",
28
- "install_tools",
29
- "use_github",
30
- "create_subagent",
31
- "store_in_kb",
32
- "system_prompt",
33
- "state_manager",
34
- "tray",
35
- "ambient",
36
- "agentcore_config",
37
- "agentcore_invoke",
38
- "agentcore_logs",
39
- "agentcore_agents",
40
- ]
41
- except ImportError:
42
- __all__ = [
43
- "tcp",
44
- "websocket",
45
- "ipc",
46
- "mcp_server",
47
- "install_tools",
48
- "use_github",
49
- "create_subagent",
50
- "store_in_kb",
51
- "system_prompt",
52
- "state_manager",
53
- "tray",
54
- "ambient",
55
- ]
27
+ __all__ = [
28
+ "agentcore_agents",
29
+ "agentcore_config",
30
+ "agentcore_invoke",
31
+ "agentcore_logs",
32
+ "ambient",
33
+ "create_subagent",
34
+ "fetch_github_tool",
35
+ "install_tools",
36
+ "ipc",
37
+ "mcp_server",
38
+ "scraper",
39
+ "speech_to_speech",
40
+ "state_manager",
41
+ "store_in_kb",
42
+ "system_prompt",
43
+ "tcp",
44
+ "tray",
45
+ "use_github",
46
+ "websocket",
47
+ ]
@@ -0,0 +1,201 @@
1
+ """GitHub Tool Fetcher for Strands Agent.
2
+
3
+ This tool fetches Python tool files from GitHub repositories and loads them
4
+ as available tools in the current Strands agent. It combines HTTP fetching
5
+ with the load_tool functionality to enable dynamic tool loading from remote
6
+ GitHub repositories.
7
+
8
+ Usage with Strands Agents:
9
+ python
10
+ from strands import Agent
11
+ from tools.fetch_github_tool import fetch_github_tool
12
+
13
+ agent = Agent(tools=[fetch_github_tool])
14
+
15
+ # Fetch and load a tool from GitHub
16
+ agent.tool.fetch_github_tool(
17
+ github_url="https://github.com/owner/repo/blob/main/tools/my_tool.py",
18
+ tool_name="my_tool"
19
+ )
20
+
21
+ # Now you can use the fetched tool
22
+ agent.tool.my_tool(param1="value")
23
+
24
+
25
+ Supported GitHub URL formats:
26
+ - https://github.com/owner/repo/blob/branch/path/to/file.py
27
+ - https://github.com/owner/repo/tree/branch/path/to/file.py
28
+ - https://raw.githubusercontent.com/owner/repo/branch/path/to/file.py
29
+ """
30
+
31
+ import os
32
+ import re
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ import requests
37
+ from strands import tool
38
+
39
+
40
+ def parse_github_url(github_url: str) -> dict[str, str]:
41
+ """Parse GitHub URL to extract repository information.
42
+
43
+ Args:
44
+ github_url: GitHub URL to the file
45
+
46
+ Returns:
47
+ Dictionary with owner, repo, branch, and file_path
48
+
49
+ Raises:
50
+ ValueError: If URL format is not supported
51
+ """
52
+ # Handle raw.githubusercontent.com URLs
53
+ raw_pattern = r"https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)"
54
+ raw_match = re.match(raw_pattern, github_url)
55
+
56
+ if raw_match:
57
+ owner, repo, branch, file_path = raw_match.groups()
58
+ return {"owner": owner, "repo": repo, "branch": branch, "file_path": file_path}
59
+
60
+ # Handle github.com/owner/repo/blob/branch/path URLs
61
+ blob_pattern = r"https://github\.com/([^/]+)/([^/]+)/(?:blob|tree)/([^/]+)/(.+)"
62
+ blob_match = re.match(blob_pattern, github_url)
63
+
64
+ if blob_match:
65
+ owner, repo, branch, file_path = blob_match.groups()
66
+ return {"owner": owner, "repo": repo, "branch": branch, "file_path": file_path}
67
+
68
+ raise ValueError(f"Unsupported GitHub URL format: {github_url}")
69
+
70
+
71
+ def build_raw_url(owner: str, repo: str, branch: str, file_path: str) -> str:
72
+ """Build GitHub raw content URL.
73
+
74
+ Args:
75
+ owner: Repository owner
76
+ repo: Repository name
77
+ branch: Branch name
78
+ file_path: Path to file in repository
79
+
80
+ Returns:
81
+ Raw GitHub URL for the file
82
+ """
83
+ return f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{file_path}"
84
+
85
+
86
+ @tool
87
+ def fetch_github_tool(
88
+ github_url: str,
89
+ tool_name: str | None = None,
90
+ local_dir: str = "./github_tools",
91
+ agent: Any = None,
92
+ ) -> dict[str, Any]:
93
+ """Fetch a Python tool file from GitHub and load it as a Strands tool.
94
+
95
+ This tool downloads Python files from GitHub repositories, saves them locally,
96
+ and registers them as available tools in the current Strands agent. It supports
97
+ various GitHub URL formats and automatically handles the conversion to raw content URLs.
98
+
99
+ Args:
100
+ github_url: GitHub URL to the Python tool file. Supports formats like:
101
+ - https://github.com/owner/repo/blob/main/tools/my_tool.py
102
+ - https://github.com/owner/repo/tree/main/tools/my_tool.py
103
+ - https://raw.githubusercontent.com/owner/repo/main/tools/my_tool.py
104
+ tool_name: Name to register the tool under. If not provided, will extract
105
+ from the filename (e.g., "my_tool.py" becomes "my_tool")
106
+ local_dir: Local directory to save the fetched tool file. Defaults to "./github_tools"
107
+ agent: Agent instance (automatically provided by Strands)
108
+
109
+ Returns:
110
+ Dict containing status and response content:
111
+ {
112
+ "status": "success|error",
113
+ "content": [{"text": "Response message"}]
114
+ }
115
+
116
+ Examples:
117
+ # Fetch a tool from GitHub and load it
118
+ agent.tool.fetch_github_tool(
119
+ github_url="https://github.com/cagataycali/my-tools/blob/main/weather_tool.py",
120
+ tool_name="weather"
121
+ )
122
+
123
+ # Tool name can be auto-detected from filename
124
+ agent.tool.fetch_github_tool(
125
+ github_url="https://github.com/cagataycali/my-tools/blob/main/calculator.py"
126
+ )
127
+ """
128
+ try:
129
+ # Parse the GitHub URL
130
+ try:
131
+ url_info = parse_github_url(github_url)
132
+ except ValueError as e:
133
+ return {"status": "error", "content": [{"text": f"❌ {e!s}"}]}
134
+
135
+ # Extract tool name from filename if not provided
136
+ if not tool_name:
137
+ filename = os.path.basename(url_info["file_path"])
138
+ tool_name = os.path.splitext(filename)[0]
139
+
140
+ # Check if it's a Python file first (before making HTTP request)
141
+ if not url_info["file_path"].endswith(".py"):
142
+ return {
143
+ "status": "error",
144
+ "content": [
145
+ {
146
+ "text": f"❌ File must be a Python file (.py), got: {url_info['file_path']}"
147
+ }
148
+ ],
149
+ }
150
+
151
+ # Build raw GitHub URL
152
+ raw_url = build_raw_url(
153
+ url_info["owner"],
154
+ url_info["repo"],
155
+ url_info["branch"],
156
+ url_info["file_path"],
157
+ )
158
+
159
+ # Create local directory if it doesn't exist
160
+ local_path = Path(local_dir)
161
+ local_path.mkdir(parents=True, exist_ok=True)
162
+
163
+ # Download the file
164
+ response = requests.get(raw_url, timeout=30)
165
+ response.raise_for_status()
166
+ # Save the file locally
167
+ local_file_path = local_path / f"{tool_name}.py"
168
+ with open(local_file_path, "w", encoding="utf-8") as f:
169
+ f.write(response.text)
170
+
171
+ # Load the tool using load_tool functionality
172
+ if agent and hasattr(agent, "tool_registry"):
173
+ agent.tool_registry.load_tool_from_filepath(
174
+ tool_name=tool_name, tool_path=str(local_file_path)
175
+ )
176
+
177
+ success_message = f"""✅ Successfully fetched and loaded GitHub tool!
178
+
179
+ 📂 **Source:** {github_url}
180
+ 🏷️ **Tool Name:** {tool_name}
181
+ 💾 **Local Path:** {local_file_path}
182
+ 🔧 **Status:** Ready to use
183
+
184
+ You can now use the tool with: agent.tool.{tool_name}(...)"""
185
+
186
+ return {"status": "success", "content": [{"text": success_message}]}
187
+ else:
188
+ return {
189
+ "status": "error",
190
+ "content": [
191
+ {"text": "❌ Agent instance not available for tool registration"}
192
+ ],
193
+ }
194
+
195
+ except requests.RequestException as e:
196
+ return {
197
+ "status": "error",
198
+ "content": [{"text": f"❌ Failed to download file from GitHub: {e!s}"}],
199
+ }
200
+ except Exception as e:
201
+ return {"status": "error", "content": [{"text": f"❌ Unexpected error: {e!s}"}]}