devduck 0.4.1__py3-none-any.whl → 0.5.2__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
@@ -1,260 +1,51 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- 🦆 devduck - extreme minimalist self-adapting agent
4
- one file. self-healing. runtime dependencies. adaptive.
5
- """
2
+ """🦆 devduck - self-adapting agent"""
6
3
  import sys
7
- import subprocess
4
+ import threading
8
5
  import os
9
6
  import platform
10
- import socket
11
7
  import logging
12
8
  import tempfile
13
- from datetime import datetime
9
+ import boto3
14
10
  from pathlib import Path
15
11
  from datetime import datetime
16
- from typing import Dict, Any
12
+ import warnings
17
13
  from logging.handlers import RotatingFileHandler
18
14
 
15
+ warnings.filterwarnings("ignore", message=".*pkg_resources is deprecated.*")
16
+ warnings.filterwarnings("ignore", message=".*cache_prompt is deprecated.*")
17
+
19
18
  os.environ["BYPASS_TOOL_CONSENT"] = "true"
20
19
  os.environ["STRANDS_TOOL_CONSOLE_MODE"] = "enabled"
20
+ os.environ["EDITOR_DISABLE_BACKUP"] = "true"
21
21
 
22
- # 📝 Setup logging system
23
22
  LOG_DIR = Path(tempfile.gettempdir()) / "devduck" / "logs"
24
23
  LOG_DIR.mkdir(parents=True, exist_ok=True)
25
24
  LOG_FILE = LOG_DIR / "devduck.log"
26
25
 
27
- # Configure logger
28
26
  logger = logging.getLogger("devduck")
29
27
  logger.setLevel(logging.DEBUG)
30
-
31
- # File handler with rotation (10MB max, keep 3 backups)
32
- file_handler = RotatingFileHandler(
33
- LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=3, encoding="utf-8"
28
+ logger.addHandler(
29
+ RotatingFileHandler(LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=3)
34
30
  )
35
- file_handler.setLevel(logging.DEBUG)
36
- file_formatter = logging.Formatter(
37
- "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
38
- )
39
- file_handler.setFormatter(file_formatter)
40
-
41
- # Console handler (only warnings and above)
42
- console_handler = logging.StreamHandler()
43
- console_handler.setLevel(logging.WARNING)
44
- console_formatter = logging.Formatter("🦆 %(levelname)s: %(message)s")
45
- console_handler.setFormatter(console_formatter)
46
-
47
- logger.addHandler(file_handler)
48
- logger.addHandler(console_handler)
49
-
50
- logger.info("DevDuck logging system initialized")
51
-
31
+ logger.info("DevDuck initialized")
52
32
 
53
- # 🔧 Self-healing dependency installer
54
- def ensure_deps():
55
- """Install core dependencies at runtime if missing"""
56
- import importlib.metadata
57
33
 
58
- # Only ensure core deps - everything else is optional
59
- core_deps = [
60
- "strands-agents",
61
- "prompt_toolkit",
62
- "strands-agents-tools",
63
- ]
64
-
65
- # Check each package individually using importlib.metadata
66
- for dep in core_deps:
67
- pkg_name = dep.split("[")[0] # Get base package name (strip extras)
68
- try:
69
- # Check if package is installed using metadata (checks PyPI package name)
70
- importlib.metadata.version(pkg_name)
71
- except importlib.metadata.PackageNotFoundError:
72
- print(f"🦆 Installing {dep}...")
73
- logger.debug(f"🦆 Installing {dep}...")
74
- try:
75
- subprocess.check_call(
76
- [sys.executable, "-m", "pip", "install", dep],
77
- stdout=subprocess.DEVNULL,
78
- stderr=subprocess.DEVNULL,
79
- )
80
- except subprocess.CalledProcessError as e:
81
- print(f"🦆 Warning: Failed to install {dep}: {e}")
82
- logger.debug(f"🦆 Warning: Failed to install {dep}: {e}")
83
-
84
-
85
- # 🌍 Environment adaptation
86
- def adapt_to_env():
87
- """Self-adapt based on environment"""
88
- env_info = {
89
- "os": platform.system(),
90
- "arch": platform.machine(),
91
- "python": sys.version_info,
92
- "cwd": str(Path.cwd()),
93
- "home": str(Path.home()),
94
- "shell": os.environ.get("SHELL", "unknown"),
95
- "hostname": socket.gethostname(),
96
- }
97
-
98
- # Adaptive configurations - using common models
99
- if env_info["os"] == "Darwin": # macOS
100
- ollama_host = "http://localhost:11434"
101
- model = "qwen3:1.7b" # Lightweight for macOS
102
- elif env_info["os"] == "Linux":
103
- ollama_host = "http://localhost:11434"
104
- model = "qwen3:30b" # More power on Linux
105
- else: # Windows
106
- ollama_host = "http://localhost:11434"
107
- model = "qwen3:8b" # Conservative for Windows
108
-
109
- return env_info, ollama_host, model
110
-
111
-
112
- # 🔍 Self-awareness: Read own source code
113
34
  def get_own_source_code():
114
- """
115
- Read and return the source code of this agent file.
116
-
117
- Returns:
118
- str: The complete source code for self-awareness
119
- """
35
+ """Read own source code for self-awareness"""
120
36
  try:
121
- # Read this file (__init__.py)
122
- current_file = __file__
123
- with open(current_file, "r", encoding="utf-8") as f:
124
- init_code = f.read()
125
- return f"# devduck/__init__.py\n```python\n{init_code}\n```"
37
+ with open(__file__, "r", encoding="utf-8") as f:
38
+ return f"# devduck/__init__.py\n```python\n{f.read()}\n```"
126
39
  except Exception as e:
127
- return f"Error reading own source code: {e}"
128
-
129
- def view_logs_tool(
130
- action: str = "view",
131
- lines: int = 100,
132
- pattern: str = None,
133
- ) -> Dict[str, Any]:
134
- """
135
- View and manage DevDuck logs.
136
-
137
- Args:
138
- action: Action to perform - "view", "tail", "search", "clear", "stats"
139
- lines: Number of lines to show (for view/tail)
140
- pattern: Search pattern (for search action)
141
-
142
- Returns:
143
- Dict with status and content
144
- """
145
- try:
146
- if action == "view":
147
- if not LOG_FILE.exists():
148
- return {"status": "success", "content": [{"text": "No logs yet"}]}
149
-
150
- with open(LOG_FILE, "r", encoding="utf-8") as f:
151
- all_lines = f.readlines()
152
- recent_lines = (
153
- all_lines[-lines:] if len(all_lines) > lines else all_lines
154
- )
155
- content = "".join(recent_lines)
156
-
157
- return {
158
- "status": "success",
159
- "content": [
160
- {"text": f"Last {len(recent_lines)} log lines:\n\n{content}"}
161
- ],
162
- }
163
-
164
- elif action == "tail":
165
- if not LOG_FILE.exists():
166
- return {"status": "success", "content": [{"text": "No logs yet"}]}
167
-
168
- with open(LOG_FILE, "r", encoding="utf-8") as f:
169
- all_lines = f.readlines()
170
- tail_lines = all_lines[-50:] if len(all_lines) > 50 else all_lines
171
- content = "".join(tail_lines)
172
-
173
- return {
174
- "status": "success",
175
- "content": [{"text": f"Tail (last 50 lines):\n\n{content}"}],
176
- }
177
-
178
- elif action == "search":
179
- if not pattern:
180
- return {
181
- "status": "error",
182
- "content": [{"text": "pattern parameter required for search"}],
183
- }
184
-
185
- if not LOG_FILE.exists():
186
- return {"status": "success", "content": [{"text": "No logs yet"}]}
187
-
188
- with open(LOG_FILE, "r", encoding="utf-8") as f:
189
- matching_lines = [line for line in f if pattern.lower() in line.lower()]
190
-
191
- if not matching_lines:
192
- return {
193
- "status": "success",
194
- "content": [{"text": f"No matches found for pattern: {pattern}"}],
195
- }
196
-
197
- content = "".join(matching_lines[-100:]) # Last 100 matches
198
- return {
199
- "status": "success",
200
- "content": [
201
- {
202
- "text": f"Found {len(matching_lines)} matches (showing last 100):\n\n{content}"
203
- }
204
- ],
205
- }
206
-
207
- elif action == "clear":
208
- if LOG_FILE.exists():
209
- LOG_FILE.unlink()
210
- logger.info("Log file cleared by user")
211
- return {
212
- "status": "success",
213
- "content": [{"text": "Logs cleared successfully"}],
214
- }
215
-
216
- elif action == "stats":
217
- if not LOG_FILE.exists():
218
- return {"status": "success", "content": [{"text": "No logs yet"}]}
219
-
220
- stat = LOG_FILE.stat()
221
- size_mb = stat.st_size / (1024 * 1024)
222
- modified = datetime.fromtimestamp(stat.st_mtime).strftime(
223
- "%Y-%m-%d %H:%M:%S"
224
- )
225
-
226
- with open(LOG_FILE, "r", encoding="utf-8") as f:
227
- total_lines = sum(1 for _ in f)
228
-
229
- stats_text = f"""Log File Statistics:
230
- Path: {LOG_FILE}
231
- Size: {size_mb:.2f} MB
232
- Lines: {total_lines}
233
- Last Modified: {modified}"""
234
-
235
- return {"status": "success", "content": [{"text": stats_text}]}
236
-
237
- else:
238
- return {
239
- "status": "error",
240
- "content": [
241
- {
242
- "text": f"Unknown action: {action}. Valid: view, tail, search, clear, stats"
243
- }
244
- ],
245
- }
246
-
247
- except Exception as e:
248
- logger.error(f"Error in view_logs_tool: {e}")
249
- return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
40
+ return f"Error reading source: {e}"
250
41
 
251
42
 
252
43
  def get_shell_history_file():
253
- """Get the devduck-specific history file path."""
254
- devduck_history = Path.home() / ".devduck_history"
255
- if not devduck_history.exists():
256
- devduck_history.touch(mode=0o600)
257
- return str(devduck_history)
44
+ """Get devduck history file"""
45
+ history = Path.home() / ".devduck_history"
46
+ if not history.exists():
47
+ history.touch(mode=0o600)
48
+ return str(history)
258
49
 
259
50
 
260
51
  def get_shell_history_files():
@@ -334,32 +125,6 @@ def parse_history_line(line, history_type):
334
125
  return None
335
126
 
336
127
 
337
- def get_recent_logs():
338
- """Get the last N lines from the log file for context."""
339
- try:
340
- log_line_count = int(os.getenv("DEVDUCK_LOG_LINE_COUNT", "50"))
341
-
342
- if not LOG_FILE.exists():
343
- return ""
344
-
345
- with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f:
346
- all_lines = f.readlines()
347
-
348
- recent_lines = (
349
- all_lines[-log_line_count:]
350
- if len(all_lines) > log_line_count
351
- else all_lines
352
- )
353
-
354
- if not recent_lines:
355
- return ""
356
-
357
- log_content = "".join(recent_lines)
358
- return f"\n\n## Recent Logs (last {len(recent_lines)} lines):\n```\n{log_content}```\n"
359
- except Exception as e:
360
- return f"\n\n## Recent Logs: Error reading logs - {e}\n"
361
-
362
-
363
128
  def get_last_messages():
364
129
  """Get the last N messages from multiple shell histories for context."""
365
130
  try:
@@ -419,269 +184,353 @@ def get_last_messages():
419
184
  return ""
420
185
 
421
186
 
187
+ def get_recent_logs():
188
+ """Get recent logs for context"""
189
+ try:
190
+ log_count = int(os.getenv("DEVDUCK_LOG_LINE_COUNT", "50"))
191
+
192
+ if not LOG_FILE.exists():
193
+ return ""
194
+
195
+ with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f:
196
+ lines = f.readlines()
197
+
198
+ recent = lines[-log_count:] if len(lines) > log_count else lines
199
+
200
+ if recent:
201
+ return f"\n\n## Recent Logs (last {len(recent)} lines):\n```\n{''.join(recent)}```\n"
202
+ return ""
203
+ except:
204
+ return ""
205
+
206
+
422
207
  def append_to_shell_history(query, response):
423
- """Append the interaction to devduck shell history."""
208
+ """Append interaction to history"""
424
209
  import time
425
210
 
426
211
  try:
427
212
  history_file = get_shell_history_file()
428
213
  timestamp = str(int(time.time()))
214
+ response_summary = (
215
+ str(response).replace("\n", " ")[
216
+ : int(os.getenv("DEVDUCK_RESPONSE_SUMMARY_LENGTH", "10000"))
217
+ ]
218
+ + "..."
219
+ )
429
220
 
430
221
  with open(history_file, "a", encoding="utf-8") as f:
431
222
  f.write(f": {timestamp}:0;# devduck: {query}\n")
432
- response_summary = (
433
- str(response).replace("\n", " ")[
434
- : int(os.getenv("DEVDUCK_RESPONSE_SUMMARY_LENGTH", "10000"))
435
- ]
436
- + "..."
437
- )
438
223
  f.write(f": {timestamp}:0;# devduck_result: {response_summary}\n")
439
224
 
440
225
  os.chmod(history_file, 0o600)
441
- except Exception:
226
+ except:
442
227
  pass
443
228
 
444
229
 
445
- # 🦆 The devduck agent
446
230
  class DevDuck:
231
+ """Minimalist adaptive agent with flexible tool loading"""
232
+
447
233
  def __init__(
448
234
  self,
449
235
  auto_start_servers=True,
450
236
  tcp_port=9999,
451
237
  ws_port=8080,
452
238
  mcp_port=8000,
239
+ ipc_socket=None,
453
240
  enable_tcp=True,
454
241
  enable_ws=True,
455
242
  enable_mcp=True,
243
+ enable_ipc=True,
456
244
  ):
457
245
  """Initialize the minimalist adaptive agent"""
458
- logger.info("Initializing DevDuck agent...")
459
- try:
460
- # Self-heal dependencies
461
- ensure_deps()
246
+ logger.info("Initializing DevDuck...")
247
+
248
+ # Environment detection
249
+ self.os = platform.system()
250
+ self.arch = platform.machine()
251
+ self.model = "qwen3:1.7b" if self.os == "Darwin" else "qwen3:8b"
252
+
253
+ # Hot-reload state
254
+ self._agent_executing = False
255
+ self._reload_pending = False
256
+
257
+ # Server configuration
258
+ self.tcp_port = tcp_port
259
+ self.ws_port = ws_port
260
+ self.mcp_port = mcp_port
261
+ self.ipc_socket = ipc_socket or "/tmp/devduck_main.sock"
262
+ self.enable_tcp = enable_tcp
263
+ self.enable_ws = enable_ws
264
+ self.enable_mcp = enable_mcp
265
+ self.enable_ipc = enable_ipc
266
+
267
+ # Import core dependencies
268
+ from strands import Agent, tool
269
+
270
+ # Load tools with flexible configuration
271
+ tools = self._load_tools_flexible()
272
+
273
+ # Add built-in view_logs tool
274
+ @tool
275
+ def view_logs(action: str = "view", lines: int = 100, pattern: str = None):
276
+ """View and manage DevDuck logs"""
277
+ return self._view_logs_impl(action, lines, pattern)
278
+
279
+ tools.append(view_logs)
280
+
281
+ # Create model
282
+ model = self._create_model()
283
+
284
+ # Create agent
285
+ self.agent = Agent(
286
+ model=model,
287
+ tools=tools,
288
+ system_prompt=self._build_prompt(),
289
+ load_tools_from_directory=True,
290
+ )
462
291
 
463
- # Adapt to environment
464
- self.env_info, self.ollama_host, self.model = adapt_to_env()
292
+ # Auto-start servers
293
+ if auto_start_servers and "--mcp" not in sys.argv:
294
+ self._start_servers()
465
295
 
466
- # Execution state tracking for hot-reload
467
- self._agent_executing = False
468
- self._reload_pending = False
296
+ # Start hot-reload watcher
297
+ self._start_hot_reload()
469
298
 
470
- # Import after ensuring deps
471
- from strands import Agent, tool
299
+ logger.info(f"DevDuck ready with {len(tools)} tools")
472
300
 
473
- # Core tools (always available)
474
- core_tools = []
301
+ def _load_tools_flexible(self):
302
+ """
303
+ Load tools with flexible configuration via DEVDUCK_TOOLS env var.
475
304
 
476
- # Try importing optional tools gracefully
477
- try:
478
- from strands.models.ollama import OllamaModel
479
- except ImportError:
480
- logger.warning(
481
- "strands-agents[ollama] not installed - Ollama model unavailable"
482
- )
483
- OllamaModel = None
305
+ Format: package:tool1,tool2:package2:tool3,tool4
306
+ Example: strands_tools:shell,editor:strands_fun_tools:clipboard
484
307
 
485
- try:
486
- from strands_tools.utils.models.model import create_model
487
- except ImportError:
488
- logger.warning(
489
- "strands-agents-tools not installed - create_model unavailable"
490
- )
491
- create_model = None
308
+ Static tools (always loaded):
309
+ - DevDuck's own tools (tcp, websocket, ipc, etc.)
310
+ - AgentCore tools (if AWS credentials available)
311
+ """
312
+ tools = []
492
313
 
493
- try:
494
- from .tools import (
314
+ # 1. STATIC: Core DevDuck tools (always load)
315
+ try:
316
+ from devduck.tools import (
317
+ tcp,
318
+ websocket,
319
+ ipc,
320
+ mcp_server,
321
+ install_tools,
322
+ use_github,
323
+ create_subagent,
324
+ store_in_kb,
325
+ system_prompt,
326
+ tray,
327
+ ambient,
328
+ )
329
+
330
+ tools.extend(
331
+ [
495
332
  tcp,
496
333
  websocket,
334
+ ipc,
497
335
  mcp_server,
498
336
  install_tools,
499
337
  use_github,
500
338
  create_subagent,
501
339
  store_in_kb,
502
340
  system_prompt,
503
- )
341
+ tray,
342
+ ambient,
343
+ ]
344
+ )
345
+ logger.info("✅ DevDuck core tools loaded")
346
+ except ImportError as e:
347
+ logger.warning(f"DevDuck tools unavailable: {e}")
504
348
 
505
- core_tools.extend(
349
+ # 2. STATIC: AgentCore tools (if AWS credentials available and not disabled)
350
+ if os.getenv("DEVDUCK_DISABLE_AGENTCORE_TOOLS", "false").lower() != "true":
351
+ try:
352
+ boto3.client("sts").get_caller_identity()
353
+ from .tools.agentcore_config import agentcore_config
354
+ from .tools.agentcore_invoke import agentcore_invoke
355
+ from .tools.agentcore_logs import agentcore_logs
356
+ from .tools.agentcore_agents import agentcore_agents
357
+
358
+ tools.extend(
506
359
  [
507
- tcp,
508
- websocket,
509
- mcp_server,
510
- install_tools,
511
- use_github,
512
- create_subagent,
513
- store_in_kb,
514
- system_prompt
360
+ agentcore_config,
361
+ agentcore_invoke,
362
+ agentcore_logs,
363
+ agentcore_agents,
515
364
  ]
516
365
  )
517
- except ImportError as e:
518
- logger.warning(f"devduck.tools import failed: {e}")
366
+ logger.info("✅ AgentCore tools loaded")
367
+ except:
368
+ pass
369
+ else:
370
+ logger.info(
371
+ "⏭️ AgentCore tools disabled (DEVDUCK_DISABLE_AGENTCORE_TOOLS=true)"
372
+ )
519
373
 
520
- # Skip fun tools in --mcp mode (they're not needed for MCP server)
521
- if "--mcp" not in sys.argv:
522
- try:
523
- from strands_fun_tools import (
524
- listen,
525
- cursor,
526
- clipboard,
527
- screen_reader,
528
- yolo_vision,
529
- )
374
+ # 3. FLEXIBLE: Load tools from DEVDUCK_TOOLS env var
375
+ tools_config = os.getenv("DEVDUCK_TOOLS")
530
376
 
531
- core_tools.extend(
532
- [listen, cursor, clipboard, screen_reader, yolo_vision]
533
- )
534
- except ImportError:
535
- logger.info(
536
- "strands-fun-tools not installed - vision/audio tools unavailable (install with: pip install devduck[all])"
537
- )
377
+ if tools_config:
378
+ # Parse: "strands_tools:shell,editor:strands_fun_tools:clipboard"
379
+ tools.extend(self._parse_and_load_tools(tools_config))
380
+ else:
381
+ # Default: Load all common tools
382
+ tools.extend(self._load_default_tools())
383
+
384
+ return tools
385
+
386
+ def _parse_and_load_tools(self, config):
387
+ """
388
+ Parse DEVDUCK_TOOLS config and load specified tools.
389
+
390
+ Format: package:tool1,tool2:package2:tool3
391
+ Example: strands_tools:shell,editor:strands_fun_tools:clipboard,cursor
392
+ """
393
+ loaded_tools = []
394
+ current_package = None
395
+
396
+ for segment in config.split(":"):
397
+ segment = segment.strip()
398
+
399
+ # Check if this segment is a package or tool list
400
+ if "," not in segment and not segment.startswith("strands"):
401
+ # Single tool from current package
402
+ if current_package:
403
+ tool = self._load_single_tool(current_package, segment)
404
+ if tool:
405
+ loaded_tools.append(tool)
406
+ elif "," in segment:
407
+ # Tool list from current package
408
+ if current_package:
409
+ for tool_name in segment.split(","):
410
+ tool_name = tool_name.strip()
411
+ tool = self._load_single_tool(current_package, tool_name)
412
+ if tool:
413
+ loaded_tools.append(tool)
538
414
  else:
539
- logger.info("--mcp mode: skipping vision/audio tools")
415
+ # Package name
416
+ current_package = segment
540
417
 
541
- try:
542
- from strands_tools import (
418
+ logger.info(f"✅ Loaded {len(loaded_tools)} tools from DEVDUCK_TOOLS")
419
+ return loaded_tools
420
+
421
+ def _load_single_tool(self, package, tool_name):
422
+ """Load a single tool from a package"""
423
+ try:
424
+ module = __import__(package, fromlist=[tool_name])
425
+ tool = getattr(module, tool_name)
426
+ logger.debug(f"Loaded {tool_name} from {package}")
427
+ return tool
428
+ except Exception as e:
429
+ logger.warning(f"Failed to load {tool_name} from {package}: {e}")
430
+ return None
431
+
432
+ def _load_default_tools(self):
433
+ """Load default tools when DEVDUCK_TOOLS is not set"""
434
+ tools = []
435
+
436
+ # strands-agents-tools (essential)
437
+ try:
438
+ from strands_tools import (
439
+ shell,
440
+ editor,
441
+ file_read,
442
+ file_write,
443
+ calculator,
444
+ image_reader,
445
+ use_agent,
446
+ load_tool,
447
+ environment,
448
+ mcp_client,
449
+ retrieve,
450
+ )
451
+
452
+ tools.extend(
453
+ [
543
454
  shell,
544
455
  editor,
456
+ file_read,
457
+ file_write,
545
458
  calculator,
546
- # python_repl,
547
459
  image_reader,
548
460
  use_agent,
549
461
  load_tool,
550
462
  environment,
551
463
  mcp_client,
552
464
  retrieve,
553
- )
554
-
555
- core_tools.extend(
556
- [
557
- shell,
558
- editor,
559
- calculator,
560
- # python_repl,
561
- image_reader,
562
- use_agent,
563
- load_tool,
564
- environment,
565
- mcp_client,
566
- retrieve,
567
- ]
568
- )
569
- except ImportError:
570
- logger.info(
571
- "strands-agents-tools not installed - core tools unavailable (install with: pip install devduck[all])"
572
- )
465
+ ]
466
+ )
467
+ logger.info("✅ strands-agents-tools loaded")
468
+ except ImportError:
469
+ logger.warning("strands-agents-tools unavailable")
573
470
 
574
- # Wrap view_logs_tool with @tool decorator
575
- @tool
576
- def view_logs(
577
- action: str = "view",
578
- lines: int = 100,
579
- pattern: str = None,
580
- ) -> Dict[str, Any]:
581
- """View and manage DevDuck logs."""
582
- return view_logs_tool(action, lines, pattern)
583
-
584
- # Add built-in tools to the toolset
585
- core_tools.extend([view_logs])
586
-
587
- # Assign tools
588
- self.tools = core_tools
589
-
590
- logger.info(f"Initialized {len(self.tools)} tools")
591
-
592
- # Check if MODEL_PROVIDER env variable is set
593
- model_provider = os.getenv("MODEL_PROVIDER")
594
-
595
- if model_provider and create_model:
596
- # Use create_model utility for any provider (bedrock, anthropic, etc.)
597
- self.agent_model = create_model(provider=model_provider)
598
- elif OllamaModel:
599
- # Fallback to default Ollama behavior
600
- self.agent_model = OllamaModel(
601
- host=self.ollama_host,
602
- model_id=self.model,
603
- temperature=1,
604
- keep_alive="5m",
605
- )
606
- else:
607
- raise ImportError(
608
- "No model provider available. Install with: pip install devduck[all]"
471
+ # strands-fun-tools (optional, skip in --mcp mode)
472
+ if "--mcp" not in sys.argv:
473
+ try:
474
+ from strands_fun_tools import (
475
+ listen,
476
+ cursor,
477
+ clipboard,
478
+ screen_reader,
479
+ yolo_vision,
609
480
  )
610
481
 
611
- # Create agent with self-healing
612
- self.agent = Agent(
613
- model=self.agent_model,
614
- tools=self.tools,
615
- system_prompt=self._build_system_prompt(),
616
- load_tools_from_directory=True,
617
- )
618
-
619
- # 🚀 AUTO-START SERVERS: TCP, WebSocket, MCP HTTP
620
- if auto_start_servers:
621
- logger.info("Auto-starting servers...")
622
- print("🦆 Auto-starting servers...")
482
+ tools.extend([listen, cursor, clipboard, screen_reader, yolo_vision])
483
+ logger.info("✅ strands-fun-tools loaded")
484
+ except ImportError:
485
+ logger.info("strands-fun-tools unavailable")
623
486
 
624
- if enable_tcp:
625
- try:
626
- # Start TCP server on configurable port
627
- tcp_result = self.agent.tool.tcp(
628
- action="start_server", port=tcp_port
629
- )
630
- if tcp_result.get("status") == "success":
631
- logger.info(f"✓ TCP server started on port {tcp_port}")
632
- print(f"🦆 ✓ TCP server: localhost:{tcp_port}")
633
- else:
634
- logger.warning(f"TCP server start issue: {tcp_result}")
635
- except Exception as e:
636
- logger.error(f"Failed to start TCP server: {e}")
637
- print(f"🦆 ⚠ TCP server failed: {e}")
487
+ return tools
638
488
 
639
- if enable_ws:
640
- try:
641
- # Start WebSocket server on configurable port
642
- ws_result = self.agent.tool.websocket(
643
- action="start_server", port=ws_port
644
- )
645
- if ws_result.get("status") == "success":
646
- logger.info(f"✓ WebSocket server started on port {ws_port}")
647
- print(f"🦆 ✓ WebSocket server: localhost:{ws_port}")
648
- else:
649
- logger.warning(f"WebSocket server start issue: {ws_result}")
650
- except Exception as e:
651
- logger.error(f"Failed to start WebSocket server: {e}")
652
- print(f"🦆 ⚠ WebSocket server failed: {e}")
489
+ def _create_model(self):
490
+ """Create model with smart provider selection"""
491
+ provider = os.getenv("MODEL_PROVIDER")
653
492
 
654
- if enable_mcp:
493
+ if not provider:
494
+ # Auto-detect: Bedrock → MLX → Ollama
495
+ try:
496
+ boto3.client("sts").get_caller_identity()
497
+ provider = "bedrock"
498
+ print("🦆 Using Bedrock")
499
+ except:
500
+ if self.os == "Darwin" and self.arch in ["arm64", "aarch64"]:
655
501
  try:
656
- # Start MCP server with HTTP transport on configurable port
657
- mcp_result = self.agent.tool.mcp_server(
658
- action="start",
659
- transport="http",
660
- port=mcp_port,
661
- expose_agent=True,
662
- agent=self.agent,
663
- )
664
- if mcp_result.get("status") == "success":
665
- logger.info(f"✓ MCP HTTP server started on port {mcp_port}")
666
- print(f"🦆 MCP server: http://localhost:{mcp_port}/mcp")
667
- else:
668
- logger.warning(f"MCP server start issue: {mcp_result}")
669
- except Exception as e:
670
- logger.error(f"Failed to start MCP server: {e}")
671
- print(f"🦆 ⚠ MCP server failed: {e}")
672
-
673
- # Start file watcher for auto hot-reload
674
- self._start_file_watcher()
675
-
676
- logger.info(
677
- f"DevDuck agent initialized successfully with model {self.model}"
678
- )
502
+ from strands_mlx import MLXModel
503
+
504
+ provider = "mlx"
505
+ self.model = "mlx-community/Qwen3-1.7B-4bit"
506
+ print("🦆 Using MLX")
507
+ except ImportError:
508
+ provider = "ollama"
509
+ print("🦆 Using Ollama")
510
+ else:
511
+ provider = "ollama"
512
+ print("🦆 Using Ollama")
513
+
514
+ # Create model
515
+ if provider == "mlx":
516
+ from strands_mlx import MLXModel
517
+
518
+ return MLXModel(model_id=self.model, temperature=1)
519
+ elif provider == "ollama":
520
+ from strands.models.ollama import OllamaModel
521
+
522
+ return OllamaModel(
523
+ host="http://localhost:11434",
524
+ model_id=self.model,
525
+ temperature=1,
526
+ keep_alive="5m",
527
+ )
528
+ else:
529
+ from strands_tools.utils.models.model import create_model
679
530
 
680
- except Exception as e:
681
- logger.error(f"Initialization failed: {e}")
682
- self._self_heal(e)
531
+ return create_model(provider=provider)
683
532
 
684
- def _build_system_prompt(self):
533
+ def _build_prompt(self):
685
534
  """Build adaptive system prompt based on environment
686
535
 
687
536
  IMPORTANT: The system prompt includes the agent's complete source code.
@@ -691,427 +540,344 @@ class DevDuck:
691
540
 
692
541
  Learning: Always check source code truth over conversation memory!
693
542
  """
694
- session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
695
-
696
- # Get own source code for self-awareness
697
543
  own_code = get_own_source_code()
544
+ recent_context = get_last_messages()
545
+ recent_logs = get_recent_logs()
698
546
 
699
- # print(own_code)
700
-
701
- # Get recent conversation history context (with error handling)
547
+ # Detect if using Bedrock for AgentCore documentation
548
+ provider = os.getenv("MODEL_PROVIDER", "")
549
+ is_bedrock = provider == "bedrock" or "bedrock" in provider.lower()
702
550
  try:
703
- recent_context = get_last_messages()
704
- except Exception as e:
705
- print(f"🦆 Warning: Could not load history context: {e}")
706
- recent_context = ""
551
+ if not is_bedrock:
552
+ boto3.client("sts").get_caller_identity()
553
+ is_bedrock = True
554
+ except:
555
+ pass
707
556
 
708
- # Get recent logs for immediate visibility
709
- try:
710
- recent_logs = get_recent_logs()
711
- except Exception as e:
712
- print(f"🦆 Warning: Could not load recent logs: {e}")
713
- recent_logs = ""
557
+ # Build AgentCore documentation if using Bedrock
558
+ agentcore_docs = ""
559
+ if is_bedrock:
560
+ handler_path = str(Path(__file__).parent / "agentcore_handler.py")
561
+ agentcore_docs = f"""
562
+
563
+ ## 🚀 AgentCore (Bedrock)
564
+
565
+ Handler: `{handler_path}`
566
+
567
+ ### Quick Deploy:
568
+ ```python
569
+ # Configure + launch
570
+ agentcore_config(action="configure", agent_name="devduck",
571
+ tools="strands_tools:shell,editor", auto_launch=True)
572
+
573
+ # Invoke
574
+ agentcore_invoke(prompt="test", agent_name="devduck")
575
+
576
+ # Monitor
577
+ agentcore_logs(agent_name="devduck")
578
+ agentcore_agents(action="list")
579
+ ```
580
+
581
+ ### Key Params:
582
+ - tools: "package:tool1,tool2:package2:tool3"
583
+ - idle_timeout: 900s (default)
584
+ - model_id: us.anthropic.claude-sonnet-4-5-20250929-v1:0
585
+ """
714
586
 
715
- return f"""🦆 You are DevDuck - an extreme minimalist, self-adapting agent.
587
+ return f"""🦆 DevDuck - self-adapting agent
716
588
 
717
- Environment: {self.env_info['os']} {self.env_info['arch']}
718
- Python: {self.env_info['python']}
589
+ Environment: {self.os} {self.arch}
719
590
  Model: {self.model}
720
- Hostname: {self.env_info['hostname']}
721
- Session ID: {session_id}
591
+ CWD: {Path.cwd()}
722
592
 
723
593
  You are:
724
594
  - Minimalist: Brief, direct responses
725
- - Self-healing: Adapt when things break
726
595
  - Efficient: Get things done fast
727
596
  - Pragmatic: Use what works
728
597
 
729
- Current working directory: {self.env_info['cwd']}
730
-
731
598
  {recent_context}
732
599
  {recent_logs}
600
+ {agentcore_docs}
733
601
 
734
- ## Your Own Implementation:
735
- You have full access to your own source code for self-awareness and self-modification:
602
+ ## Your Code
736
603
 
604
+ You have full access to your own source code for self-awareness and self-modification:
605
+ ---
737
606
  {own_code}
607
+ ---
738
608
 
739
- ## Hot Reload System Active:
740
- - **Instant Tool Creation** - Save any .py file in `./tools/` and it becomes immediately available
741
- - **No Restart Needed** - Tools are auto-loaded and ready to use instantly
742
- - **Live Development** - Modify existing tools while running and test immediately
743
- - **Full Python Access** - Create any Python functionality as a tool
609
+ ## Hot Reload Active:
610
+ - Save .py files in ./tools/ for instant tool creation
611
+ - Use install_tools() to load from packages
612
+ - No restart needed
744
613
 
745
- ## Dynamic Tool Loading:
746
- - **Install Tools** - Use install_tools() to load tools from any Python package
747
- - Example: install_tools(action="install_and_load", package="strands-fun-tools", module="strands_fun_tools")
748
- - Expands capabilities without restart
749
- - Access to entire Python ecosystem
750
-
751
- ## MCP Server:
752
- - **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
753
- - Example: mcp_server(action="start", port=8000)
754
- - Connect from Claude Desktop, other agents, or custom clients
755
- - Full bidirectional communication
614
+ ## Tool Configuration:
615
+ Set DEVDUCK_TOOLS for custom tools:
616
+ - Format: package:tool1,tool2:package2:tool3
617
+ - Example: strands_tools:shell,editor:strands_fun_tools:clipboard
618
+ - Static tools always loaded: tcp, websocket, ipc, mcp_server, agentcore_*
756
619
 
757
620
  ## Knowledge Base Integration:
758
- - **Automatic RAG** - Set STRANDS_KNOWLEDGE_BASE_ID to enable automatic retrieval/storage
621
+ - **Automatic RAG** - Set DEVDUCK_KNOWLEDGE_BASE_ID to enable automatic retrieval/storage
759
622
  - Before each query: Retrieves relevant context from knowledge base
760
623
  - After each response: Stores conversation for future reference
761
624
  - Seamless memory across sessions without manual tool calls
762
625
 
763
- ## Tool Creation Patterns:
626
+ ## System Prompt Management:
627
+ - system_prompt(action='view') - View current
628
+ - system_prompt(action='update', prompt='...') - Update
629
+ - system_prompt(action='update', repository='owner/repo') - Sync to GitHub
630
+
631
+ ## Shell Commands:
632
+ - Prefix with ! to run shell commands
633
+ - Example: ! ls -la
634
+
635
+ Response: MINIMAL WORDS, MAX PARALLELISM
764
636
 
765
- ### **1. @tool Decorator:**
637
+ ## Tool Building Guide:
638
+
639
+ ### **@tool Decorator (Recommended):**
766
640
  ```python
767
- # ./tools/calculate_tip.py
641
+ # ./tools/my_tool.py
768
642
  from strands import tool
769
643
 
770
644
  @tool
771
- def calculate_tip(amount: float, percentage: float = 15.0) -> str:
772
- \"\"\"Calculate tip and total for a bill.
645
+ def my_tool(param1: str, param2: int = 10) -> str:
646
+ \"\"\"Tool description.
773
647
 
774
648
  Args:
775
- amount: Bill amount in dollars
776
- percentage: Tip percentage (default: 15.0)
649
+ param1: Description of param1
650
+ param2: Description of param2 (default: 10)
777
651
 
778
652
  Returns:
779
- str: Formatted tip calculation result
653
+ str: Description of return value
780
654
  \"\"\"
781
- tip = amount * (percentage / 100)
782
- total = amount + tip
783
- return f"Tip: {{tip:.2f}}, Total: {{total:.2f}}"
655
+ # Implementation
656
+ return f"Result: {{param1}} - {{param2}}"
784
657
  ```
785
658
 
786
- ### **2. Action-Based Pattern:**
659
+ ### **Action-Based Pattern:**
787
660
  ```python
788
- # ./tools/weather.py
789
661
  from typing import Dict, Any
790
662
  from strands import tool
791
663
 
792
664
  @tool
793
- def weather(action: str, location: str = None) -> Dict[str, Any]:
794
- \"\"\"Comprehensive weather information tool.
665
+ def my_tool(action: str, data: str = None) -> Dict[str, Any]:
666
+ \"\"\"Multi-action tool.
795
667
 
796
668
  Args:
797
- action: Action to perform (current, forecast, alerts)
798
- location: City name (required)
669
+ action: Action to perform (get, set, delete)
670
+ data: Optional data for action
799
671
 
800
672
  Returns:
801
- Dict containing status and response content
673
+ Dict with status and content
802
674
  \"\"\"
803
- if action == "current":
804
- return {{"status": "success", "content": [{{"text": f"Weather for {{location}}"}}]}}
805
- elif action == "forecast":
806
- return {{"status": "success", "content": [{{"text": f"Forecast for {{location}}"}}]}}
675
+ if action == "get":
676
+ return {{"status": "success", "content": [{{"text": f"Got: {{data}}"}}]}}
677
+ elif action == "set":
678
+ return {{"status": "success", "content": [{{"text": f"Set: {{data}}"}}]}}
807
679
  else:
808
680
  return {{"status": "error", "content": [{{"text": f"Unknown action: {{action}}"}}]}}
809
681
  ```
810
682
 
811
- ## System Prompt Management:
812
- - **View**: system_prompt(action='view') - See current prompt
813
- - **Update Local**: system_prompt(action='update', prompt='new text') - Updates env var + .prompt file
814
- - **Update GitHub**: system_prompt(action='update', prompt='text', repository='cagataycali/devduck') - Syncs to repo variables
815
- - **Variable Name**: system_prompt(action='update', prompt='text', variable_name='CUSTOM_PROMPT') - Use custom var
816
- - **Add Context**: system_prompt(action='add_context', context='new learning') - Append without replacing
817
-
818
- ### 🧠 Self-Improvement Pattern:
819
- When you learn something valuable during conversations:
820
- 1. Identify the new insight or pattern
821
- 2. Use system_prompt(action='add_context', context='...') to append it
822
- 3. Sync to GitHub: system_prompt(action='update', prompt=new_full_prompt, repository='owner/repo')
823
- 4. New learnings persist across sessions via SYSTEM_PROMPT env var
824
-
825
- **Repository Integration**: Set repository='cagataycali/devduck' to sync prompts across deployments
826
-
827
- ## Shell Commands:
828
- - Prefix with ! to execute shell commands directly
829
- - Example: ! ls -la (lists files)
830
- - Example: ! pwd (shows current directory)
831
-
832
- **Response Format:**
833
- - Tool calls: **MAXIMUM PARALLELISM - ALWAYS**
834
- - Communication: **MINIMAL WORDS**
835
- - Efficiency: **Speed is paramount**
683
+ ### **Tool Best Practices:**
684
+ 1. Use type hints for all parameters
685
+ 2. Provide clear docstrings
686
+ 3. Return consistent formats (str or Dict[str, Any])
687
+ 4. Use action-based pattern for complex tools
688
+ 5. Handle errors gracefully
689
+ 6. Log important operations
836
690
 
837
691
  {os.getenv('SYSTEM_PROMPT', '')}"""
838
692
 
839
- def _self_heal(self, error):
840
- """Attempt self-healing when errors occur"""
841
- logger.error(f"Self-healing triggered by error: {error}")
842
- print(f"🦆 Self-healing from: {error}")
693
+ def _view_logs_impl(self, action, lines, pattern):
694
+ """Implementation of view_logs tool"""
695
+ try:
696
+ if action == "view":
697
+ if not LOG_FILE.exists():
698
+ return {"status": "success", "content": [{"text": "No logs yet"}]}
699
+ with open(LOG_FILE, "r") as f:
700
+ all_lines = f.readlines()
701
+ recent = all_lines[-lines:] if len(all_lines) > lines else all_lines
702
+ return {
703
+ "status": "success",
704
+ "content": [
705
+ {"text": f"Last {len(recent)} lines:\n\n{''.join(recent)}"}
706
+ ],
707
+ }
843
708
 
844
- # Prevent infinite recursion by tracking heal attempts
845
- if not hasattr(self, "_heal_count"):
846
- self._heal_count = 0
709
+ elif action == "search":
710
+ if not pattern:
711
+ return {
712
+ "status": "error",
713
+ "content": [{"text": "pattern required"}],
714
+ }
715
+ if not LOG_FILE.exists():
716
+ return {"status": "success", "content": [{"text": "No logs yet"}]}
717
+ with open(LOG_FILE, "r") as f:
718
+ matches = [line for line in f if pattern.lower() in line.lower()]
719
+ if not matches:
720
+ return {
721
+ "status": "success",
722
+ "content": [{"text": f"No matches for: {pattern}"}],
723
+ }
724
+ return {
725
+ "status": "success",
726
+ "content": [
727
+ {
728
+ "text": f"Found {len(matches)} matches:\n\n{''.join(matches[-100:])}"
729
+ }
730
+ ],
731
+ }
847
732
 
848
- self._heal_count += 1
733
+ elif action == "clear":
734
+ if LOG_FILE.exists():
735
+ LOG_FILE.unlink()
736
+ return {"status": "success", "content": [{"text": "Logs cleared"}]}
849
737
 
850
- # Limit recursion - if we've tried more than 3 times, give up
851
- if self._heal_count > 3:
852
- print(f"🦆 Self-healing failed after {self._heal_count} attempts")
853
- print("🦆 Please fix the issue manually and restart")
854
- sys.exit(1)
738
+ else:
739
+ return {
740
+ "status": "error",
741
+ "content": [{"text": f"Unknown action: {action}"}],
742
+ }
743
+ except Exception as e:
744
+ return {"status": "error", "content": [{"text": f"Error: {e}"}]}
855
745
 
856
- # Common healing strategies
857
- if "not found" in str(error).lower() and "model" in str(error).lower():
858
- print("🦆 Model not found - trying to pull model...")
746
+ def _start_servers(self):
747
+ """Auto-start servers"""
748
+ if self.enable_tcp:
859
749
  try:
860
- # Try to pull the model
861
- result = subprocess.run(
862
- ["ollama", "pull", self.model], capture_output=True, timeout=60
863
- )
864
- if result.returncode == 0:
865
- print(f"🦆 Successfully pulled {self.model}")
866
- else:
867
- print(f"🦆 Failed to pull {self.model}, trying fallback...")
868
- # Fallback to basic models
869
- fallback_models = ["llama3.2:1b", "qwen2.5:0.5b", "gemma2:2b"]
870
- for fallback in fallback_models:
871
- try:
872
- subprocess.run(
873
- ["ollama", "pull", fallback],
874
- capture_output=True,
875
- timeout=30,
876
- )
877
- self.model = fallback
878
- print(f"🦆 Using fallback model: {fallback}")
879
- break
880
- except:
881
- continue
882
- except Exception as pull_error:
883
- print(f"🦆 Model pull failed: {pull_error}")
884
- # Ultra-minimal fallback
885
- self.model = "llama3.2:1b"
750
+ self.agent.tool.tcp(action="start_server", port=self.tcp_port)
751
+ print(f"🦆 TCP: localhost:{self.tcp_port}")
752
+ except Exception as e:
753
+ logger.warning(f"TCP server failed: {e}")
886
754
 
887
- elif "ollama" in str(error).lower():
888
- print("🦆 Ollama issue - checking service...")
755
+ if self.enable_ws:
889
756
  try:
890
- # Check if ollama is running
891
- result = subprocess.run(
892
- ["ollama", "list"], capture_output=True, timeout=5
893
- )
894
- if result.returncode != 0:
895
- print("🦆 Starting ollama service...")
896
- subprocess.Popen(["ollama", "serve"])
897
- import time
898
-
899
- time.sleep(3) # Wait for service to start
900
- except Exception as ollama_error:
901
- print(f"🦆 Ollama service issue: {ollama_error}")
902
-
903
- elif "import" in str(error).lower():
904
- print("🦆 Import issue - reinstalling dependencies...")
905
- ensure_deps()
757
+ self.agent.tool.websocket(action="start_server", port=self.ws_port)
758
+ print(f"🦆 WebSocket: localhost:{self.ws_port}")
759
+ except Exception as e:
760
+ logger.warning(f"WebSocket server failed: {e}")
906
761
 
907
- elif "connection" in str(error).lower():
908
- print("🦆 Connection issue - checking ollama service...")
762
+ if self.enable_mcp:
909
763
  try:
910
- subprocess.run(["ollama", "serve"], check=False, timeout=2)
911
- except:
912
- pass
913
-
914
- # Retry initialization
915
- try:
916
- self.__init__()
917
- except Exception as e2:
918
- print(f"🦆 Self-heal failed: {e2}")
919
- print("🦆 Running in minimal mode...")
920
- self.agent = None
921
-
922
- def __call__(self, query):
923
- """Make the agent callable with automatic knowledge base integration"""
924
- if not self.agent:
925
- logger.warning("Agent unavailable - attempted to call with query")
926
- return "🦆 Agent unavailable - try: devduck.restart()"
927
-
928
- try:
929
- logger.info(f"Agent call started: {query[:100]}...")
930
- # Mark agent as executing to prevent hot-reload interruption
931
- self._agent_executing = True
932
-
933
- # 📚 Knowledge Base Retrieval (BEFORE agent runs)
934
- knowledge_base_id = os.getenv("STRANDS_KNOWLEDGE_BASE_ID")
935
- if knowledge_base_id and hasattr(self.agent, "tool"):
936
- try:
937
- if "retrieve" in self.agent.tool_names:
938
- logger.info(f"Retrieving context from KB: {knowledge_base_id}")
939
- self.agent.tool.retrieve(
940
- text=query, knowledgeBaseId=knowledge_base_id
941
- )
942
- except Exception as e:
943
- logger.warning(f"KB retrieval failed: {e}")
944
-
945
- # Run the agent
946
- result = self.agent(query)
947
-
948
- # 💾 Knowledge Base Storage (AFTER agent runs)
949
- if knowledge_base_id and hasattr(self.agent, "tool"):
950
- try:
951
- if "store_in_kb" in self.agent.tool_names:
952
-
953
- conversation_content = f"Input: {query}, Result: {result!s}"
954
- conversation_title = f"DevDuck: {datetime.now().strftime('%Y-%m-%d')} | {query[:500]}"
955
- self.agent.tool.store_in_kb(
956
- content=conversation_content,
957
- title=conversation_title,
958
- knowledge_base_id=knowledge_base_id,
959
- )
960
- logger.info(f"Stored conversation in KB: {knowledge_base_id}")
961
- except Exception as e:
962
- logger.warning(f"KB storage failed: {e}")
963
-
964
- # Agent finished - check if reload was pending
965
- self._agent_executing = False
966
- logger.info("Agent call completed successfully")
967
- if self._reload_pending:
968
- logger.info("Triggering pending hot-reload after agent completion")
969
- print("🦆 Agent finished - triggering pending hot-reload...")
970
- self.hot_reload()
971
-
972
- return result
973
- except Exception as e:
974
- self._agent_executing = False # Reset flag on error
975
- logger.error(f"Agent call failed with error: {e}")
976
- self._self_heal(e)
977
- if self.agent:
978
- return self.agent(query)
979
- else:
980
- return f"🦆 Error: {e}"
764
+ self.agent.tool.mcp_server(
765
+ action="start",
766
+ transport="http",
767
+ port=self.mcp_port,
768
+ expose_agent=True,
769
+ agent=self.agent,
770
+ )
771
+ print(f"🦆 MCP: http://localhost:{self.mcp_port}/mcp")
772
+ except Exception as e:
773
+ logger.warning(f"MCP server failed: {e}")
981
774
 
982
- def restart(self):
983
- """Restart the agent"""
984
- print("🦆 Restarting...")
985
- self.__init__()
775
+ if self.enable_ipc:
776
+ try:
777
+ self.agent.tool.ipc(action="start_server", socket_path=self.ipc_socket)
778
+ print(f"🦆 ✓ IPC: {self.ipc_socket}")
779
+ except Exception as e:
780
+ logger.warning(f"IPC server failed: {e}")
986
781
 
987
- def _start_file_watcher(self):
988
- """Start background file watcher for auto hot-reload"""
989
- import threading
782
+ def _start_hot_reload(self):
783
+ """Start hot-reload file watcher"""
990
784
 
991
- logger.info("Starting file watcher for hot-reload")
992
- # Get the path to this file
993
785
  self._watch_file = Path(__file__).resolve()
994
786
  self._last_modified = (
995
787
  self._watch_file.stat().st_mtime if self._watch_file.exists() else None
996
788
  )
997
789
  self._watcher_running = True
998
790
 
999
- # Start watcher thread
1000
- self._watcher_thread = threading.Thread(
1001
- target=self._file_watcher_thread, daemon=True
1002
- )
1003
- self._watcher_thread.start()
1004
- logger.info(f"File watcher started, monitoring {self._watch_file}")
1005
-
1006
- def _file_watcher_thread(self):
1007
- """Background thread that watches for file changes"""
1008
- import time
791
+ def watcher_thread():
792
+ import time
1009
793
 
1010
- last_reload_time = 0
1011
- debounce_seconds = 3 # 3 second debounce
794
+ last_reload = 0
795
+ debounce = 3 # seconds
1012
796
 
1013
- while self._watcher_running:
1014
- try:
1015
- # Skip if currently reloading to prevent triggering during exec()
1016
- if getattr(self, "_is_reloading", False):
1017
- time.sleep(1)
1018
- continue
1019
-
1020
- if self._watch_file.exists():
1021
- current_mtime = self._watch_file.stat().st_mtime
1022
- current_time = time.time()
1023
-
1024
- # Check if file was modified AND debounce period has passed
1025
- if (
1026
- self._last_modified
1027
- and current_mtime > self._last_modified
1028
- and current_time - last_reload_time > debounce_seconds
1029
- ):
1030
-
1031
- print(f"🦆 Detected changes in {self._watch_file.name}!")
1032
- self._last_modified = current_mtime
1033
- last_reload_time = current_time
1034
-
1035
- # Check if agent is currently executing
1036
- if getattr(self, "_agent_executing", False):
1037
- logger.info(
1038
- "Code change detected but agent is executing - reload pending"
1039
- )
1040
- print(
1041
- "🦆 Agent is currently executing - reload will trigger after completion"
1042
- )
1043
- self._reload_pending = True
797
+ while self._watcher_running:
798
+ try:
799
+ if self._watch_file.exists():
800
+ mtime = self._watch_file.stat().st_mtime
801
+ current_time = time.time()
802
+
803
+ if (
804
+ self._last_modified
805
+ and mtime > self._last_modified
806
+ and current_time - last_reload > debounce
807
+ ):
808
+
809
+ print(f"🦆 Code changed - hot-reload triggered")
810
+ self._last_modified = mtime
811
+ last_reload = current_time
812
+
813
+ if self._agent_executing:
814
+ print("🦆 Reload pending (agent executing)")
815
+ self._reload_pending = True
816
+ else:
817
+ self._hot_reload()
1044
818
  else:
1045
- # Safe to reload immediately
1046
- logger.info(
1047
- f"Code change detected in {self._watch_file.name} - triggering hot-reload"
1048
- )
1049
- time.sleep(
1050
- 0.5
1051
- ) # Small delay to ensure file write is complete
1052
- self.hot_reload()
1053
- else:
1054
- self._last_modified = current_mtime
819
+ self._last_modified = mtime
820
+ except Exception as e:
821
+ logger.error(f"File watcher error: {e}")
1055
822
 
1056
- except Exception as e:
1057
- print(f"🦆 File watcher error: {e}")
823
+ time.sleep(1)
1058
824
 
1059
- # Check every 1 second
1060
- time.sleep(1)
825
+ thread = threading.Thread(target=watcher_thread, daemon=True)
826
+ thread.start()
827
+ logger.info(f"Hot-reload watching: {self._watch_file}")
1061
828
 
1062
- def _stop_file_watcher(self):
1063
- """Stop the file watcher"""
829
+ def _hot_reload(self):
830
+ """Hot-reload by restarting process"""
831
+ logger.info("Hot-reload: restarting process")
1064
832
  self._watcher_running = False
1065
- print("🦆 File watcher stopped")
833
+ os.execv(sys.executable, [sys.executable] + sys.argv)
1066
834
 
1067
- def hot_reload(self):
1068
- """Hot-reload by restarting the entire Python process with fresh code"""
1069
- logger.info("Hot-reload initiated")
1070
- print("🦆 Hot-reloading via process restart...")
835
+ def __call__(self, query):
836
+ """Call agent with KB integration"""
837
+ if not self.agent:
838
+ return "🦆 Agent unavailable"
1071
839
 
1072
840
  try:
1073
- # Set reload flag to prevent recursive reloads during shutdown
1074
- if hasattr(self, "_is_reloading") and self._is_reloading:
1075
- print("🦆 Reload already in progress, skipping")
1076
- return
841
+ self._agent_executing = True
1077
842
 
1078
- self._is_reloading = True
843
+ # KB retrieval
844
+ kb_id = os.getenv("DEVDUCK_KNOWLEDGE_BASE_ID")
845
+ if kb_id:
846
+ try:
847
+ self.agent.tool.retrieve(text=query, knowledgeBaseId=kb_id)
848
+ except:
849
+ pass
1079
850
 
1080
- # Stop the file watcher
1081
- if hasattr(self, "_watcher_running"):
1082
- self._watcher_running = False
851
+ # Run agent
852
+ result = self.agent(query)
1083
853
 
1084
- print("🦆 Restarting process with fresh code...")
854
+ # KB storage
855
+ if kb_id:
856
+ try:
857
+ self.agent.tool.store_in_kb(
858
+ content=f"Input: {query}\nResult: {str(result)}",
859
+ title=f"DevDuck: {datetime.now().strftime('%Y-%m-%d')} | {query[:500]}",
860
+ knowledge_base_id=kb_id,
861
+ )
862
+ except:
863
+ pass
1085
864
 
1086
- # Restart the entire Python process
1087
- # This ensures all code is freshly loaded
1088
- os.execv(sys.executable, [sys.executable] + sys.argv)
865
+ self._agent_executing = False
1089
866
 
867
+ # Check for pending reload
868
+ if self._reload_pending:
869
+ print("🦆 Agent finished - triggering pending reload")
870
+ self._hot_reload()
871
+
872
+ return result
1090
873
  except Exception as e:
1091
- print(f"🦆 Hot-reload failed: {e}")
1092
- print("🦆 Falling back to manual restart")
1093
- self._is_reloading = False
1094
-
1095
- def status(self):
1096
- """Show current status"""
1097
- return {
1098
- "model": self.model,
1099
- "host": self.ollama_host,
1100
- "env": self.env_info,
1101
- "agent_ready": self.agent is not None,
1102
- "tools": len(self.tools) if hasattr(self, "tools") else 0,
1103
- "file_watcher": {
1104
- "enabled": hasattr(self, "_watcher_running") and self._watcher_running,
1105
- "watching": (
1106
- str(self._watch_file) if hasattr(self, "_watch_file") else None
1107
- ),
1108
- },
1109
- }
1110
-
1111
-
1112
- # 🦆 Auto-initialize when imported
874
+ self._agent_executing = False
875
+ logger.error(f"Agent call failed: {e}")
876
+ return f"🦆 Error: {e}"
877
+
878
+
879
+ # Initialize
1113
880
  # Check environment variables to control server configuration
1114
- # Also check if --mcp flag is present to skip auto-starting servers
1115
881
  _auto_start = os.getenv("DEVDUCK_AUTO_START_SERVERS", "true").lower() == "true"
1116
882
 
1117
883
  # Disable auto-start if --mcp flag is present (stdio mode)
@@ -1121,42 +887,30 @@ if "--mcp" in sys.argv:
1121
887
  _tcp_port = int(os.getenv("DEVDUCK_TCP_PORT", "9999"))
1122
888
  _ws_port = int(os.getenv("DEVDUCK_WS_PORT", "8080"))
1123
889
  _mcp_port = int(os.getenv("DEVDUCK_MCP_PORT", "8000"))
890
+ _ipc_socket = os.getenv("DEVDUCK_IPC_SOCKET", None)
1124
891
  _enable_tcp = os.getenv("DEVDUCK_ENABLE_TCP", "true").lower() == "true"
1125
892
  _enable_ws = os.getenv("DEVDUCK_ENABLE_WS", "true").lower() == "true"
1126
893
  _enable_mcp = os.getenv("DEVDUCK_ENABLE_MCP", "true").lower() == "true"
894
+ _enable_ipc = os.getenv("DEVDUCK_ENABLE_IPC", "true").lower() == "true"
1127
895
 
1128
896
  devduck = DevDuck(
1129
897
  auto_start_servers=_auto_start,
1130
898
  tcp_port=_tcp_port,
1131
899
  ws_port=_ws_port,
1132
900
  mcp_port=_mcp_port,
901
+ ipc_socket=_ipc_socket,
1133
902
  enable_tcp=_enable_tcp,
1134
903
  enable_ws=_enable_ws,
1135
904
  enable_mcp=_enable_mcp,
905
+ enable_ipc=_enable_ipc,
1136
906
  )
1137
907
 
1138
908
 
1139
- # 🚀 Convenience functions
1140
909
  def ask(query):
1141
- """Quick query interface"""
910
+ """Quick query"""
1142
911
  return devduck(query)
1143
912
 
1144
913
 
1145
- def status():
1146
- """Quick status check"""
1147
- return devduck.status()
1148
-
1149
-
1150
- def restart():
1151
- """Quick restart"""
1152
- devduck.restart()
1153
-
1154
-
1155
- def hot_reload():
1156
- """Quick hot-reload without restart"""
1157
- devduck.hot_reload()
1158
-
1159
-
1160
914
  def extract_commands_from_history():
1161
915
  """Extract commonly used commands from shell history for auto-completion."""
1162
916
  commands = set()
@@ -1230,7 +984,8 @@ def extract_commands_from_history():
1230
984
 
1231
985
 
1232
986
  def interactive():
1233
- """Interactive REPL mode for devduck"""
987
+ """Interactive REPL with history"""
988
+ import time
1234
989
  from prompt_toolkit import prompt
1235
990
  from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
1236
991
  from prompt_toolkit.completion import WordCompleter
@@ -1238,14 +993,10 @@ def interactive():
1238
993
 
1239
994
  print("🦆 DevDuck")
1240
995
  print(f"📝 Logs: {LOG_DIR}")
1241
- print("Type 'exit', 'quit', or 'q' to quit.")
1242
- print("Prefix with ! to run shell commands (e.g., ! ls -la)")
996
+ print("Type 'exit' to quit. Prefix with ! for shell commands.")
1243
997
  print("-" * 50)
1244
- logger.info("Interactive mode started")
1245
998
 
1246
- # Set up prompt_toolkit with history
1247
- history_file = get_shell_history_file()
1248
- history = FileHistory(history_file)
999
+ history = FileHistory(get_shell_history_file())
1249
1000
 
1250
1001
  # Create completions from common commands and shell history
1251
1002
  base_commands = ["exit", "quit", "q", "help", "clear", "status", "reload"]
@@ -1255,164 +1006,72 @@ def interactive():
1255
1006
  all_commands = list(set(base_commands + history_commands))
1256
1007
  completer = WordCompleter(all_commands, ignore_case=True)
1257
1008
 
1009
+ # Track consecutive interrupts for double Ctrl+C to exit
1010
+ interrupt_count = 0
1011
+ last_interrupt = 0
1012
+
1258
1013
  while True:
1259
1014
  try:
1260
- # Use prompt_toolkit for enhanced input with arrow key support
1261
1015
  q = prompt(
1262
1016
  "\n🦆 ",
1263
1017
  history=history,
1264
1018
  auto_suggest=AutoSuggestFromHistory(),
1265
1019
  completer=completer,
1266
1020
  complete_while_typing=True,
1267
- mouse_support=False, # breaks scrolling when enabled
1268
- )
1021
+ mouse_support=False,
1022
+ ).strip()
1023
+
1024
+ # Reset interrupt count on successful prompt
1025
+ interrupt_count = 0
1269
1026
 
1270
- # Check for exit command
1271
1027
  if q.lower() in ["exit", "quit", "q"]:
1272
- print("\n🦆 Goodbye!")
1273
1028
  break
1274
1029
 
1275
- # Skip empty inputs
1276
- if q.strip() == "":
1030
+ if not q:
1277
1031
  continue
1278
1032
 
1279
- # Handle shell commands with ! prefix
1033
+ # Shell commands
1280
1034
  if q.startswith("!"):
1281
- shell_command = q[1:].strip()
1282
- try:
1283
- if devduck.agent:
1284
- devduck._agent_executing = (
1285
- True # Prevent hot-reload during shell execution
1286
- )
1287
- result = devduck.agent.tool.shell(
1288
- command=shell_command, timeout=9000
1289
- )
1290
- devduck._agent_executing = False
1291
-
1292
- # Append shell command to history
1293
- append_to_shell_history(q, result["content"][0]["text"])
1294
-
1295
- # Check if reload was pending
1296
- if devduck._reload_pending:
1297
- print(
1298
- "🦆 Shell command finished - triggering pending hot-reload..."
1299
- )
1300
- devduck.hot_reload()
1301
- else:
1302
- print("🦆 Agent unavailable")
1303
- except Exception as e:
1304
- devduck._agent_executing = False # Reset on error
1305
- print(f"🦆 Shell command error: {e}")
1306
- continue
1307
-
1308
- # Get recent conversation context
1309
- recent_context = get_last_messages()
1310
-
1311
- # Get recent logs
1312
- recent_logs = get_recent_logs()
1313
-
1314
- # Update system prompt before each call with history context
1315
- if devduck.agent:
1316
- # Rebuild system prompt with history
1317
- own_code = get_own_source_code()
1318
- session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
1319
-
1320
- devduck.agent.system_prompt = f"""🦆 You are DevDuck - an extreme minimalist, self-adapting agent.
1321
-
1322
- Environment: {devduck.env_info['os']} {devduck.env_info['arch']}
1323
- Python: {devduck.env_info['python']}
1324
- Model: {devduck.model}
1325
- Hostname: {devduck.env_info['hostname']}
1326
- Session ID: {session_id}
1327
-
1328
- You are:
1329
- - Minimalist: Brief, direct responses
1330
- - Self-healing: Adapt when things break
1331
- - Efficient: Get things done fast
1332
- - Pragmatic: Use what works
1333
-
1334
- Current working directory: {devduck.env_info['cwd']}
1335
-
1336
- {recent_context}
1337
- {recent_logs}
1338
-
1339
- ## Your Own Implementation:
1340
- You have full access to your own source code for self-awareness and self-modification:
1341
-
1342
- {own_code}
1343
-
1344
- ## Hot Reload System Active:
1345
- - **Instant Tool Creation** - Save any .py file in `./tools/` and it becomes immediately available
1346
- - **No Restart Needed** - Tools are auto-loaded and ready to use instantly
1347
- - **Live Development** - Modify existing tools while running and test immediately
1348
- - **Full Python Access** - Create any Python functionality as a tool
1349
-
1350
- ## Dynamic Tool Loading:
1351
- - **Install Tools** - Use install_tools() to load tools from any Python package
1352
- - Example: install_tools(action="install_and_load", package="strands-fun-tools", module="strands_fun_tools")
1353
- - Expands capabilities without restart
1354
- - Access to entire Python ecosystem
1355
-
1356
- ## MCP Server:
1357
- - **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
1358
- - Example: mcp_server(action="start", port=8000)
1359
- - Connect from Claude Desktop, other agents, or custom clients
1360
- - Full bidirectional communication
1361
-
1362
- ## System Prompt Management:
1363
- - **View**: system_prompt(action='view') - See current prompt
1364
- - **Update Local**: system_prompt(action='update', prompt='new text') - Updates env var + .prompt file
1365
- - **Update GitHub**: system_prompt(action='update', prompt='text', repository='cagataycali/devduck') - Syncs to repo variables
1366
- - **Variable Name**: system_prompt(action='update', prompt='text', variable_name='CUSTOM_PROMPT') - Use custom var
1367
- - **Add Context**: system_prompt(action='add_context', context='new learning') - Append without replacing
1368
-
1369
- ### 🧠 Self-Improvement Pattern:
1370
- When you learn something valuable during conversations:
1371
- 1. Identify the new insight or pattern
1372
- 2. Use system_prompt(action='add_context', context='...') to append it
1373
- 3. Optionally sync to GitHub: system_prompt(action='update', prompt=new_full_prompt, repository='owner/repo')
1374
- 4. New learnings persist across sessions via SYSTEM_PROMPT env var
1375
-
1376
- **Repository Integration**: Set repository='cagataycali/devduck' to sync prompts across deployments
1377
-
1378
- ## Shell Commands:
1379
- - Prefix with ! to execute shell commands directly
1380
- - Example: ! ls -la (lists files)
1381
- - Example: ! pwd (shows current directory)
1382
-
1383
- **Response Format:**
1384
- - Tool calls: **MAXIMUM PARALLELISM - ALWAYS**
1385
- - Communication: **MINIMAL WORDS**
1386
- - Efficiency: **Speed is paramount**
1387
-
1388
- {os.getenv('SYSTEM_PROMPT', '')}"""
1389
-
1390
- # Update model if MODEL_PROVIDER changed
1391
- model_provider = os.getenv("MODEL_PROVIDER")
1392
- if model_provider:
1393
- try:
1394
- from strands_tools.utils.models.model import create_model
1035
+ if devduck.agent:
1036
+ devduck._agent_executing = True
1037
+ result = devduck.agent.tool.shell(
1038
+ command=q[1:].strip(), timeout=9000
1039
+ )
1040
+ devduck._agent_executing = False
1041
+ append_to_shell_history(q, result["content"][0]["text"])
1395
1042
 
1396
- devduck.agent.model = create_model(provider=model_provider)
1397
- except Exception as e:
1398
- print(f"🦆 Model update error: {e}")
1043
+ if devduck._reload_pending:
1044
+ print("🦆 Shell finished - triggering pending reload")
1045
+ devduck._hot_reload()
1046
+ continue
1399
1047
 
1400
- # Execute the agent with user input
1048
+ # Agent query
1401
1049
  result = ask(q)
1402
-
1403
- # Append to shell history
1050
+ print(result)
1404
1051
  append_to_shell_history(q, str(result))
1405
1052
 
1406
1053
  except KeyboardInterrupt:
1407
- print("\n🦆 Interrupted. Type 'exit' to quit.")
1408
- continue
1054
+ current_time = time.time()
1055
+
1056
+ # Check if this is a consecutive interrupt within 2 seconds
1057
+ if current_time - last_interrupt < 2:
1058
+ interrupt_count += 1
1059
+ if interrupt_count >= 2:
1060
+ print("\n🦆 Exiting...")
1061
+ break
1062
+ else:
1063
+ print("\n🦆 Interrupted. Press Ctrl+C again to exit.")
1064
+ else:
1065
+ interrupt_count = 1
1066
+ print("\n🦆 Interrupted. Press Ctrl+C again to exit.")
1067
+
1068
+ last_interrupt = current_time
1409
1069
  except Exception as e:
1410
1070
  print(f"🦆 Error: {e}")
1411
- continue
1412
1071
 
1413
1072
 
1414
1073
  def cli():
1415
- """CLI entry point for pip-installed devduck command"""
1074
+ """CLI entry point"""
1416
1075
  import argparse
1417
1076
 
1418
1077
  parser = argparse.ArgumentParser(
@@ -1420,14 +1079,16 @@ def cli():
1420
1079
  formatter_class=argparse.RawDescriptionHelpFormatter,
1421
1080
  epilog="""
1422
1081
  Examples:
1423
- devduck # Start interactive mode
1424
- devduck "your query here" # One-shot query
1425
- devduck --mcp # MCP stdio mode (for Claude Desktop)
1082
+ devduck # Interactive mode
1083
+ devduck "query" # One-shot query
1084
+ devduck --mcp # MCP stdio mode
1426
1085
  devduck --tcp-port 9000 # Custom TCP port
1427
1086
  devduck --no-tcp --no-ws # Disable TCP and WebSocket
1428
- devduck --mcp-port 3000 # Custom MCP port
1429
1087
 
1430
- Claude Desktop Config:
1088
+ Tool Configuration:
1089
+ export DEVDUCK_TOOLS="strands_tools:shell,editor:strands_fun_tools:clipboard"
1090
+
1091
+ MCP Config:
1431
1092
  {
1432
1093
  "mcpServers": {
1433
1094
  "devduck": {
@@ -1439,15 +1100,8 @@ Claude Desktop Config:
1439
1100
  """,
1440
1101
  )
1441
1102
 
1442
- # Query argument
1443
- parser.add_argument("query", nargs="*", help="Query to send to the agent")
1444
-
1445
- # MCP stdio mode flag
1446
- parser.add_argument(
1447
- "--mcp",
1448
- action="store_true",
1449
- help="Start MCP server in stdio mode (for Claude Desktop integration)",
1450
- )
1103
+ parser.add_argument("query", nargs="*", help="Query")
1104
+ parser.add_argument("--mcp", action="store_true", help="MCP stdio mode")
1451
1105
 
1452
1106
  # Server configuration
1453
1107
  parser.add_argument(
@@ -1465,65 +1119,53 @@ Claude Desktop Config:
1465
1119
  default=8000,
1466
1120
  help="MCP HTTP server port (default: 8000)",
1467
1121
  )
1122
+ parser.add_argument(
1123
+ "--ipc-socket",
1124
+ type=str,
1125
+ default=None,
1126
+ help="IPC socket path (default: /tmp/devduck_main.sock)",
1127
+ )
1468
1128
 
1469
1129
  # Server enable/disable flags
1470
1130
  parser.add_argument("--no-tcp", action="store_true", help="Disable TCP server")
1471
1131
  parser.add_argument("--no-ws", action="store_true", help="Disable WebSocket server")
1472
1132
  parser.add_argument("--no-mcp", action="store_true", help="Disable MCP server")
1133
+ parser.add_argument("--no-ipc", action="store_true", help="Disable IPC server")
1473
1134
  parser.add_argument(
1474
1135
  "--no-servers",
1475
1136
  action="store_true",
1476
- help="Disable all servers (no TCP, WebSocket, or MCP)",
1137
+ help="Disable all servers (no TCP, WebSocket, MCP, or IPC)",
1477
1138
  )
1478
1139
 
1479
1140
  args = parser.parse_args()
1480
1141
 
1481
- logger.info("CLI mode started")
1482
-
1483
- # Handle --mcp flag for stdio mode
1484
1142
  if args.mcp:
1485
- logger.info("Starting MCP server in stdio mode (blocking, foreground)")
1486
1143
  print("🦆 Starting MCP stdio server...", file=sys.stderr)
1487
-
1488
- # Don't auto-start HTTP/TCP/WS servers for stdio mode
1489
- if devduck.agent:
1490
- try:
1491
- # Start MCP server in stdio mode - this BLOCKS until terminated
1492
- devduck.agent.tool.mcp_server(
1493
- action="start",
1494
- transport="stdio",
1495
- expose_agent=True,
1496
- agent=devduck.agent,
1497
- )
1498
- except Exception as e:
1499
- logger.error(f"Failed to start MCP stdio server: {e}")
1500
- print(f"🦆 Error: {e}", file=sys.stderr)
1501
- sys.exit(1)
1502
- else:
1503
- print("🦆 Agent not available", file=sys.stderr)
1144
+ try:
1145
+ devduck.agent.tool.mcp_server(
1146
+ action="start",
1147
+ transport="stdio",
1148
+ expose_agent=True,
1149
+ agent=devduck.agent,
1150
+ )
1151
+ except Exception as e:
1152
+ print(f"🦆 Error: {e}", file=sys.stderr)
1504
1153
  sys.exit(1)
1505
1154
  return
1506
1155
 
1507
1156
  if args.query:
1508
- query = " ".join(args.query)
1509
- logger.info(f"CLI query: {query}")
1510
- result = ask(query)
1157
+ result = ask(" ".join(args.query))
1511
1158
  print(result)
1512
1159
  else:
1513
- # No arguments - start interactive mode
1514
1160
  interactive()
1515
1161
 
1516
1162
 
1517
- # 🦆 Make module directly callable: import devduck; devduck("query")
1163
+ # Make module callable
1518
1164
  class CallableModule(sys.modules[__name__].__class__):
1519
- """Make the module itself callable"""
1520
-
1521
1165
  def __call__(self, query):
1522
- """Allow direct module call: import devduck; devduck("query")"""
1523
1166
  return ask(query)
1524
1167
 
1525
1168
 
1526
- # Replace module in sys.modules with callable version
1527
1169
  sys.modules[__name__].__class__ = CallableModule
1528
1170
 
1529
1171