devduck 0.5.4__py3-none-any.whl → 0.7.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 devduck might be problematic. Click here for more details.
- devduck/__init__.py +1226 -612
- devduck/_version.py +2 -2
- devduck/tools/__init__.py +3 -0
- devduck/tools/agentcore_config.py +1 -0
- devduck/tools/agentcore_invoke.py +1 -0
- devduck/tools/install_tools.py +103 -2
- devduck/tools/ipc.py +4 -1
- devduck/tools/state_manager.py +292 -0
- devduck/tools/tcp.py +6 -0
- devduck/tools/websocket.py +7 -1
- {devduck-0.5.4.dist-info → devduck-0.7.0.dist-info}/METADATA +158 -8
- {devduck-0.5.4.dist-info → devduck-0.7.0.dist-info}/RECORD +16 -15
- {devduck-0.5.4.dist-info → devduck-0.7.0.dist-info}/WHEEL +0 -0
- {devduck-0.5.4.dist-info → devduck-0.7.0.dist-info}/entry_points.txt +0 -0
- {devduck-0.5.4.dist-info → devduck-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {devduck-0.5.4.dist-info → devduck-0.7.0.dist-info}/top_level.txt +0 -0
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 = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.7.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 7, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
devduck/tools/__init__.py
CHANGED
|
@@ -11,6 +11,7 @@ from .use_github import use_github
|
|
|
11
11
|
from .create_subagent import create_subagent
|
|
12
12
|
from .store_in_kb import store_in_kb
|
|
13
13
|
from .system_prompt import system_prompt
|
|
14
|
+
from .state_manager import state_manager
|
|
14
15
|
|
|
15
16
|
# AgentCore tools (conditionally available)
|
|
16
17
|
try:
|
|
@@ -29,6 +30,7 @@ try:
|
|
|
29
30
|
"create_subagent",
|
|
30
31
|
"store_in_kb",
|
|
31
32
|
"system_prompt",
|
|
33
|
+
"state_manager",
|
|
32
34
|
"tray",
|
|
33
35
|
"ambient",
|
|
34
36
|
"agentcore_config",
|
|
@@ -47,6 +49,7 @@ except ImportError:
|
|
|
47
49
|
"create_subagent",
|
|
48
50
|
"store_in_kb",
|
|
49
51
|
"system_prompt",
|
|
52
|
+
"state_manager",
|
|
50
53
|
"tray",
|
|
51
54
|
"ambient",
|
|
52
55
|
]
|
devduck/tools/install_tools.py
CHANGED
|
@@ -29,7 +29,7 @@ def install_tools(
|
|
|
29
29
|
and loading their tools into the agent's registry at runtime.
|
|
30
30
|
|
|
31
31
|
Args:
|
|
32
|
-
action: Action to perform - "install", "load", "install_and_load", "list_loaded"
|
|
32
|
+
action: Action to perform - "install", "load", "install_and_load", "list_loaded", "list_available"
|
|
33
33
|
package: Python package to install (e.g., "strands-agents-tools", "strands-fun-tools")
|
|
34
34
|
module: Module to import tools from (e.g., "strands_tools", "strands_fun_tools")
|
|
35
35
|
tool_names: Optional list of specific tools to load. If None, loads all available tools
|
|
@@ -39,6 +39,13 @@ def install_tools(
|
|
|
39
39
|
Result dictionary with status and content
|
|
40
40
|
|
|
41
41
|
Examples:
|
|
42
|
+
# List available tools in a package (without loading)
|
|
43
|
+
install_tools(
|
|
44
|
+
action="list_available",
|
|
45
|
+
package="strands-fun-tools",
|
|
46
|
+
module="strands_fun_tools"
|
|
47
|
+
)
|
|
48
|
+
|
|
42
49
|
# Install and load all tools from strands-agents-tools
|
|
43
50
|
install_tools(
|
|
44
51
|
action="install_and_load",
|
|
@@ -79,13 +86,15 @@ def install_tools(
|
|
|
79
86
|
return _load_tools_from_module(module, tool_names, agent)
|
|
80
87
|
elif action == "list_loaded":
|
|
81
88
|
return _list_loaded_tools(agent)
|
|
89
|
+
elif action == "list_available":
|
|
90
|
+
return _list_available_tools(package, module)
|
|
82
91
|
else:
|
|
83
92
|
return {
|
|
84
93
|
"status": "error",
|
|
85
94
|
"content": [
|
|
86
95
|
{
|
|
87
96
|
"text": f"❌ Unknown action: {action}\n\n"
|
|
88
|
-
f"Valid actions: install, load, install_and_load, list_loaded"
|
|
97
|
+
f"Valid actions: install, load, install_and_load, list_loaded, list_available"
|
|
89
98
|
}
|
|
90
99
|
],
|
|
91
100
|
}
|
|
@@ -306,3 +315,95 @@ def _list_loaded_tools(agent: Any) -> Dict[str, Any]:
|
|
|
306
315
|
"status": "error",
|
|
307
316
|
"content": [{"text": f"❌ Failed to list tools: {str(e)}"}],
|
|
308
317
|
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _list_available_tools(package: Optional[str], module: str) -> Dict[str, Any]:
|
|
321
|
+
"""List available tools in a package without loading them."""
|
|
322
|
+
if not module:
|
|
323
|
+
return {
|
|
324
|
+
"status": "error",
|
|
325
|
+
"content": [
|
|
326
|
+
{"text": "❌ module parameter is required for list_available action"}
|
|
327
|
+
],
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
# Try to import the module
|
|
332
|
+
try:
|
|
333
|
+
imported_module = importlib.import_module(module)
|
|
334
|
+
logger.info(f"Module {module} already installed")
|
|
335
|
+
except ImportError:
|
|
336
|
+
# Module not installed - try to install package first
|
|
337
|
+
if not package:
|
|
338
|
+
return {
|
|
339
|
+
"status": "error",
|
|
340
|
+
"content": [
|
|
341
|
+
{
|
|
342
|
+
"text": f"❌ Module {module} not found and no package specified to install.\n\n"
|
|
343
|
+
f"Please provide the 'package' parameter to install first."
|
|
344
|
+
}
|
|
345
|
+
],
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
logger.info(f"Module {module} not found, installing package {package}")
|
|
349
|
+
install_result = _install_package(package)
|
|
350
|
+
if install_result["status"] == "error":
|
|
351
|
+
return install_result
|
|
352
|
+
|
|
353
|
+
# Try importing again after installation
|
|
354
|
+
try:
|
|
355
|
+
imported_module = importlib.import_module(module)
|
|
356
|
+
except ImportError as e:
|
|
357
|
+
return {
|
|
358
|
+
"status": "error",
|
|
359
|
+
"content": [
|
|
360
|
+
{
|
|
361
|
+
"text": f"❌ Failed to import {module} even after installing {package}: {str(e)}"
|
|
362
|
+
}
|
|
363
|
+
],
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# Discover tools in the module
|
|
367
|
+
available_tools = {}
|
|
368
|
+
for attr_name in dir(imported_module):
|
|
369
|
+
attr = getattr(imported_module, attr_name)
|
|
370
|
+
# Check if it's a tool (has tool_name and tool_spec attributes)
|
|
371
|
+
if hasattr(attr, "tool_name") and hasattr(attr, "tool_spec"):
|
|
372
|
+
tool_spec = attr.tool_spec
|
|
373
|
+
description = tool_spec.get("description", "No description available")
|
|
374
|
+
available_tools[attr.tool_name] = description
|
|
375
|
+
|
|
376
|
+
if not available_tools:
|
|
377
|
+
return {
|
|
378
|
+
"status": "success",
|
|
379
|
+
"content": [{"text": f"⚠️ No tools found in module: {module}"}],
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# Build result message
|
|
383
|
+
result_lines = [
|
|
384
|
+
f"📦 **Available Tools in {module} ({len(available_tools)})**\n"
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
for tool_name, description in sorted(available_tools.items()):
|
|
388
|
+
# Truncate long descriptions
|
|
389
|
+
if len(description) > 100:
|
|
390
|
+
description = description[:97] + "..."
|
|
391
|
+
|
|
392
|
+
result_lines.append(f"**{tool_name}**")
|
|
393
|
+
result_lines.append(f" {description}\n")
|
|
394
|
+
|
|
395
|
+
result_lines.append(f"\n💡 To load these tools, use:")
|
|
396
|
+
result_lines.append(f" install_tools(action='load', module='{module}')")
|
|
397
|
+
result_lines.append(f" # Or load specific tools:")
|
|
398
|
+
result_lines.append(
|
|
399
|
+
f" install_tools(action='load', module='{module}', tool_names=['tool1', 'tool2'])"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return {"status": "success", "content": [{"text": "\n".join(result_lines)}]}
|
|
403
|
+
|
|
404
|
+
except Exception as e:
|
|
405
|
+
logger.exception(f"Error listing available tools from {module}")
|
|
406
|
+
return {
|
|
407
|
+
"status": "error",
|
|
408
|
+
"content": [{"text": f"❌ Failed to list available tools: {str(e)}"}],
|
|
409
|
+
}
|
devduck/tools/ipc.py
CHANGED
|
@@ -322,8 +322,11 @@ def handle_ipc_client(client_socket, client_id, system_prompt: str, socket_path:
|
|
|
322
322
|
)
|
|
323
323
|
continue
|
|
324
324
|
|
|
325
|
+
except BrokenPipeError:
|
|
326
|
+
# Normal disconnect - client closed connection
|
|
327
|
+
logger.debug(f"IPC client {client_id} disconnected")
|
|
325
328
|
except Exception as e:
|
|
326
|
-
logger.error(f"Error handling IPC client {client_id}: {e}"
|
|
329
|
+
logger.error(f"Error handling IPC client {client_id}: {e}")
|
|
327
330
|
finally:
|
|
328
331
|
try:
|
|
329
332
|
client_socket.close()
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""DevDuck State Manager - Time-travel for agent conversations"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import dill
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Dict, Any
|
|
9
|
+
from strands import tool
|
|
10
|
+
|
|
11
|
+
base_dir = Path(os.getenv("DEVDUCK_HOME", tempfile.gettempdir()))
|
|
12
|
+
states_dir = base_dir / ".devduck" / "states"
|
|
13
|
+
states_dir.mkdir(parents=True, exist_ok=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@tool
|
|
17
|
+
def state_manager(
|
|
18
|
+
action: str,
|
|
19
|
+
state_file: str = None,
|
|
20
|
+
query: str = None,
|
|
21
|
+
metadata: dict = None,
|
|
22
|
+
agent=None, # Parent agent injection
|
|
23
|
+
) -> Dict[str, Any]:
|
|
24
|
+
"""Agent state management with time-travel capabilities.
|
|
25
|
+
|
|
26
|
+
Inspired by cagataycali/research-agent state export pattern.
|
|
27
|
+
|
|
28
|
+
Actions:
|
|
29
|
+
- export: Save current agent state to pkl
|
|
30
|
+
- load: Load and display state from pkl
|
|
31
|
+
- list: List available saved states
|
|
32
|
+
- resume: Load state and continue with new query (ephemeral)
|
|
33
|
+
- modify: Update pkl file metadata
|
|
34
|
+
- delete: Remove saved state
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
action: Operation to perform
|
|
38
|
+
state_file: Path to pkl file (auto-generated for export)
|
|
39
|
+
query: New query for resume action
|
|
40
|
+
metadata: Additional metadata for export/modify
|
|
41
|
+
agent: Parent agent (auto-injected by Strands)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dict with status and content
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
# Save current state
|
|
48
|
+
state_manager(action="export", metadata={"note": "before refactor"})
|
|
49
|
+
|
|
50
|
+
# List saved states
|
|
51
|
+
state_manager(action="list")
|
|
52
|
+
|
|
53
|
+
# Resume from previous state (ephemeral, no mutation)
|
|
54
|
+
state_manager(action="resume", state_file="~/.devduck/states/devduck_20250116_032000.pkl", query="continue analysis")
|
|
55
|
+
|
|
56
|
+
# Modify state metadata
|
|
57
|
+
state_manager(action="modify", state_file="path/to/state.pkl", metadata={"tags": ["important", "refactor"]})
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
if action == "export":
|
|
61
|
+
# Capture current agent state
|
|
62
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
63
|
+
state_file = states_dir / f"devduck_{timestamp}.pkl"
|
|
64
|
+
|
|
65
|
+
# Safe state extraction (avoid complex objects)
|
|
66
|
+
state_data = {
|
|
67
|
+
"version": "1.0",
|
|
68
|
+
"timestamp": datetime.now().isoformat(),
|
|
69
|
+
"system_prompt": agent.system_prompt,
|
|
70
|
+
"tools": list(agent.tool_names),
|
|
71
|
+
"model": {
|
|
72
|
+
"model_id": getattr(agent.model, "model_id", "unknown"),
|
|
73
|
+
"temperature": getattr(agent.model, "temperature", None),
|
|
74
|
+
"provider": getattr(agent.model, "provider", "unknown"),
|
|
75
|
+
},
|
|
76
|
+
"metadata": metadata or {},
|
|
77
|
+
"environment": {
|
|
78
|
+
"cwd": str(Path.cwd()),
|
|
79
|
+
"devduck_version": "0.6.0",
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Try to capture conversation history if available
|
|
84
|
+
if hasattr(agent, "conversation_history"):
|
|
85
|
+
state_data["conversation_history"] = agent.conversation_history
|
|
86
|
+
elif hasattr(agent, "messages"):
|
|
87
|
+
state_data["conversation_history"] = agent.messages
|
|
88
|
+
|
|
89
|
+
# Save with dill
|
|
90
|
+
with open(state_file, "wb") as f:
|
|
91
|
+
dill.dump(state_data, f)
|
|
92
|
+
|
|
93
|
+
size = state_file.stat().st_size
|
|
94
|
+
return {
|
|
95
|
+
"status": "success",
|
|
96
|
+
"content": [
|
|
97
|
+
{
|
|
98
|
+
"text": f"✅ State exported: {state_file}\n"
|
|
99
|
+
f"📦 Size: {size} bytes\n"
|
|
100
|
+
f"🔧 Tools: {len(state_data['tools'])}\n"
|
|
101
|
+
f"📝 Metadata: {metadata or 'none'}"
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
elif action == "list":
|
|
107
|
+
# List all saved states
|
|
108
|
+
states = sorted(
|
|
109
|
+
states_dir.glob("devduck_*.pkl"),
|
|
110
|
+
key=lambda p: p.stat().st_mtime,
|
|
111
|
+
reverse=True,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if not states:
|
|
115
|
+
return {
|
|
116
|
+
"status": "success",
|
|
117
|
+
"content": [{"text": "No saved states found"}],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
output = f"📚 Found {len(states)} saved states:\n\n"
|
|
121
|
+
for i, state_path in enumerate(states[:10], 1): # Show last 10
|
|
122
|
+
try:
|
|
123
|
+
with open(state_path, "rb") as f:
|
|
124
|
+
state_data = dill.load(f)
|
|
125
|
+
|
|
126
|
+
timestamp = state_data.get("timestamp", "unknown")
|
|
127
|
+
tools_count = len(state_data.get("tools", []))
|
|
128
|
+
meta = state_data.get("metadata", {})
|
|
129
|
+
|
|
130
|
+
output += f"{i}. {state_path.name}\n"
|
|
131
|
+
output += f" 📅 {timestamp}\n"
|
|
132
|
+
output += f" 🔧 {tools_count} tools\n"
|
|
133
|
+
if meta:
|
|
134
|
+
output += f" 📝 {meta}\n"
|
|
135
|
+
output += "\n"
|
|
136
|
+
except:
|
|
137
|
+
output += f"{i}. {state_path.name} (corrupted)\n\n"
|
|
138
|
+
|
|
139
|
+
return {"status": "success", "content": [{"text": output}]}
|
|
140
|
+
|
|
141
|
+
elif action == "load":
|
|
142
|
+
# Load and display state
|
|
143
|
+
if not state_file:
|
|
144
|
+
return {
|
|
145
|
+
"status": "error",
|
|
146
|
+
"content": [{"text": "state_file required for load"}],
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
state_path = Path(state_file).expanduser()
|
|
150
|
+
if not state_path.exists():
|
|
151
|
+
return {
|
|
152
|
+
"status": "error",
|
|
153
|
+
"content": [{"text": f"State file not found: {state_path}"}],
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
with open(state_path, "rb") as f:
|
|
157
|
+
state_data = dill.load(f)
|
|
158
|
+
|
|
159
|
+
# Pretty format
|
|
160
|
+
output = f"📦 State: {state_path.name}\n\n"
|
|
161
|
+
output += f"📅 Timestamp: {state_data.get('timestamp')}\n"
|
|
162
|
+
output += f"🤖 Model: {state_data.get('model', {}).get('model_id')}\n"
|
|
163
|
+
output += f"🔧 Tools ({len(state_data.get('tools', []))}): {', '.join(state_data.get('tools', []))}\n"
|
|
164
|
+
output += f"📝 Metadata: {state_data.get('metadata', {})}\n"
|
|
165
|
+
|
|
166
|
+
if "conversation_history" in state_data:
|
|
167
|
+
history = state_data["conversation_history"]
|
|
168
|
+
output += f"\n💬 Conversation: {len(history)} messages\n"
|
|
169
|
+
|
|
170
|
+
return {"status": "success", "content": [{"text": output}]}
|
|
171
|
+
|
|
172
|
+
elif action == "resume":
|
|
173
|
+
# Time-travel: Load state and continue with ephemeral agent
|
|
174
|
+
if not state_file or not query:
|
|
175
|
+
return {
|
|
176
|
+
"status": "error",
|
|
177
|
+
"content": [{"text": "state_file and query required for resume"}],
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
state_path = Path(state_file).expanduser()
|
|
181
|
+
if not state_path.exists():
|
|
182
|
+
return {
|
|
183
|
+
"status": "error",
|
|
184
|
+
"content": [{"text": f"State file not found: {state_path}"}],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
with open(state_path, "rb") as f:
|
|
188
|
+
state_data = dill.load(f)
|
|
189
|
+
|
|
190
|
+
# ✅ Create ephemeral DevDuck instance (no mutation!)
|
|
191
|
+
try:
|
|
192
|
+
from devduck import DevDuck
|
|
193
|
+
|
|
194
|
+
ephemeral_duck = DevDuck(auto_start_servers=False)
|
|
195
|
+
ephemeral_agent = ephemeral_duck.agent
|
|
196
|
+
except Exception as e:
|
|
197
|
+
return {
|
|
198
|
+
"status": "error",
|
|
199
|
+
"content": [{"text": f"Failed to create ephemeral DevDuck: {e}"}],
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Load saved state into ephemeral agent
|
|
203
|
+
ephemeral_agent.system_prompt = state_data["system_prompt"]
|
|
204
|
+
|
|
205
|
+
# Restore conversation history
|
|
206
|
+
if "conversation_history" in state_data:
|
|
207
|
+
saved_history = state_data["conversation_history"]
|
|
208
|
+
|
|
209
|
+
if hasattr(ephemeral_agent, "conversation_history"):
|
|
210
|
+
ephemeral_agent.conversation_history = saved_history
|
|
211
|
+
elif hasattr(ephemeral_agent, "messages"):
|
|
212
|
+
ephemeral_agent.messages = saved_history
|
|
213
|
+
|
|
214
|
+
# Build continuation prompt with context
|
|
215
|
+
continuation_context = f"""
|
|
216
|
+
[Resumed from state: {state_path.name}]
|
|
217
|
+
[Original timestamp: {state_data.get('timestamp')}]
|
|
218
|
+
|
|
219
|
+
{query}
|
|
220
|
+
"""
|
|
221
|
+
# Run ephemeral agent (parent agent unchanged!)
|
|
222
|
+
result = ephemeral_agent(continuation_context)
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
"status": "success",
|
|
226
|
+
"content": [{"text": f"🔄 Resumed from {state_path.name}\n\n{result}"}],
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
elif action == "modify":
|
|
230
|
+
# Modify state metadata
|
|
231
|
+
if not state_file:
|
|
232
|
+
return {
|
|
233
|
+
"status": "error",
|
|
234
|
+
"content": [{"text": "state_file required for modify"}],
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
state_path = Path(state_file).expanduser()
|
|
238
|
+
if not state_path.exists():
|
|
239
|
+
return {
|
|
240
|
+
"status": "error",
|
|
241
|
+
"content": [{"text": f"State file not found: {state_path}"}],
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
with open(state_path, "rb") as f:
|
|
245
|
+
state_data = dill.load(f)
|
|
246
|
+
|
|
247
|
+
# Update metadata
|
|
248
|
+
if metadata:
|
|
249
|
+
state_data["metadata"].update(metadata)
|
|
250
|
+
|
|
251
|
+
# Save back
|
|
252
|
+
with open(state_path, "wb") as f:
|
|
253
|
+
dill.dump(state_data, f)
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"status": "success",
|
|
257
|
+
"content": [
|
|
258
|
+
{
|
|
259
|
+
"text": f"✅ Modified {state_path.name}\n📝 New metadata: {state_data['metadata']}"
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
elif action == "delete":
|
|
265
|
+
# Delete saved state
|
|
266
|
+
if not state_file:
|
|
267
|
+
return {
|
|
268
|
+
"status": "error",
|
|
269
|
+
"content": [{"text": "state_file required for delete"}],
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
state_path = Path(state_file).expanduser()
|
|
273
|
+
if not state_path.exists():
|
|
274
|
+
return {
|
|
275
|
+
"status": "error",
|
|
276
|
+
"content": [{"text": f"State file not found: {state_path}"}],
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
state_path.unlink()
|
|
280
|
+
return {
|
|
281
|
+
"status": "success",
|
|
282
|
+
"content": [{"text": f"🗑️ Deleted {state_path.name}"}],
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
else:
|
|
286
|
+
return {
|
|
287
|
+
"status": "error",
|
|
288
|
+
"content": [{"text": f"Unknown action: {action}"}],
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
return {"status": "error", "content": [{"text": f"Error: {e}"}]}
|
devduck/tools/tcp.py
CHANGED
|
@@ -316,6 +316,12 @@ def run_server(
|
|
|
316
316
|
if SERVER_THREADS[port]["running"]:
|
|
317
317
|
logger.error(f"Error accepting connection: {e}")
|
|
318
318
|
|
|
319
|
+
except OSError as e:
|
|
320
|
+
# Port conflict - handled upstream, no need for scary errors
|
|
321
|
+
if "Address already in use" in str(e):
|
|
322
|
+
logger.debug(f"Port {port} unavailable (handled upstream)")
|
|
323
|
+
else:
|
|
324
|
+
logger.error(f"Server error on {host}:{port}: {e}")
|
|
319
325
|
except Exception as e:
|
|
320
326
|
logger.error(f"Server error on {host}:{port}: {e}")
|
|
321
327
|
finally:
|
devduck/tools/websocket.py
CHANGED
|
@@ -347,8 +347,14 @@ def run_websocket_server(
|
|
|
347
347
|
asyncio.set_event_loop(loop)
|
|
348
348
|
WS_SERVER_THREADS[port]["loop"] = loop
|
|
349
349
|
loop.run_until_complete(start_server())
|
|
350
|
+
except OSError as e:
|
|
351
|
+
# Port conflict - handled upstream, no need for scary errors
|
|
352
|
+
if "Address already in use" in str(e) or "address already in use" in str(e):
|
|
353
|
+
logger.debug(f"Port {port} unavailable (handled upstream)")
|
|
354
|
+
else:
|
|
355
|
+
logger.error(f"WebSocket server error on {host}:{port}: {e}")
|
|
350
356
|
except Exception as e:
|
|
351
|
-
logger.error(f"WebSocket server error on {host}:{port}: {e}"
|
|
357
|
+
logger.error(f"WebSocket server error on {host}:{port}: {e}")
|
|
352
358
|
finally:
|
|
353
359
|
logger.info(f"WebSocket Server on {host}:{port} stopped")
|
|
354
360
|
WS_SERVER_THREADS[port]["running"] = False
|