devduck 0.1.0__py3-none-any.whl → 0.2.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 CHANGED
@@ -8,32 +8,78 @@ import subprocess
8
8
  import os
9
9
  import platform
10
10
  import socket
11
+ import logging
12
+ import tempfile
11
13
  from pathlib import Path
12
14
  from datetime import datetime
13
15
  from typing import Dict, Any
16
+ from logging.handlers import RotatingFileHandler
14
17
 
15
18
  os.environ["BYPASS_TOOL_CONSENT"] = "true"
16
19
  os.environ["STRANDS_TOOL_CONSOLE_MODE"] = "enabled"
17
20
 
21
+ # 📝 Setup logging system
22
+ LOG_DIR = Path(tempfile.gettempdir()) / "devduck" / "logs"
23
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
24
+ LOG_FILE = LOG_DIR / "devduck.log"
25
+
26
+ # Configure logger
27
+ logger = logging.getLogger("devduck")
28
+ logger.setLevel(logging.DEBUG)
29
+
30
+ # File handler with rotation (10MB max, keep 3 backups)
31
+ file_handler = RotatingFileHandler(
32
+ LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=3, encoding="utf-8"
33
+ )
34
+ file_handler.setLevel(logging.DEBUG)
35
+ file_formatter = logging.Formatter(
36
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
37
+ )
38
+ file_handler.setFormatter(file_formatter)
39
+
40
+ # Console handler (only warnings and above)
41
+ console_handler = logging.StreamHandler()
42
+ console_handler.setLevel(logging.WARNING)
43
+ console_formatter = logging.Formatter("🦆 %(levelname)s: %(message)s")
44
+ console_handler.setFormatter(console_formatter)
45
+
46
+ logger.addHandler(file_handler)
47
+ logger.addHandler(console_handler)
48
+
49
+ logger.info("DevDuck logging system initialized")
50
+
18
51
 
19
52
  # 🔧 Self-healing dependency installer
20
53
  def ensure_deps():
21
54
  """Install dependencies at runtime if missing"""
22
- deps = ["strands-agents", "strands-agents[ollama]", "strands-agents[openai]", "strands-agents[anthropic]", "strands-agents-tools"]
55
+ import importlib.metadata
56
+
57
+ deps = [
58
+ "strands-agents",
59
+ "strands-agents[ollama]",
60
+ "strands-agents[openai]",
61
+ "strands-agents[anthropic]",
62
+ "strands-agents-tools",
63
+ ]
23
64
 
65
+ # Check each package individually using importlib.metadata
24
66
  for dep in deps:
67
+ pkg_name = dep.split("[")[0] # Get base package name (strip extras)
25
68
  try:
26
- if "strands" in dep:
27
- import strands
28
-
29
- break
30
- except ImportError:
69
+ # Check if package is installed using metadata (checks PyPI package name)
70
+ importlib.metadata.version(pkg_name)
71
+ except importlib.metadata.PackageNotFoundError:
31
72
  print(f"🦆 Installing {dep}...")
32
- subprocess.check_call(
33
- [sys.executable, "-m", "pip", "install", dep],
34
- stdout=subprocess.DEVNULL,
35
- stderr=subprocess.DEVNULL,
36
- )
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}")
37
83
 
38
84
 
39
85
  # 🌍 Environment adaptation
@@ -242,6 +288,129 @@ def system_prompt_tool(
242
288
  return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
243
289
 
244
290
 
291
+ def view_logs_tool(
292
+ action: str = "view",
293
+ lines: int = 100,
294
+ pattern: str = None,
295
+ ) -> Dict[str, Any]:
296
+ """
297
+ View and manage DevDuck logs.
298
+
299
+ Args:
300
+ action: Action to perform - "view", "tail", "search", "clear", "stats"
301
+ lines: Number of lines to show (for view/tail)
302
+ pattern: Search pattern (for search action)
303
+
304
+ Returns:
305
+ Dict with status and content
306
+ """
307
+ try:
308
+ if action == "view":
309
+ if not LOG_FILE.exists():
310
+ return {"status": "success", "content": [{"text": "No logs yet"}]}
311
+
312
+ with open(LOG_FILE, "r", encoding="utf-8") as f:
313
+ all_lines = f.readlines()
314
+ recent_lines = (
315
+ all_lines[-lines:] if len(all_lines) > lines else all_lines
316
+ )
317
+ content = "".join(recent_lines)
318
+
319
+ return {
320
+ "status": "success",
321
+ "content": [
322
+ {"text": f"Last {len(recent_lines)} log lines:\n\n{content}"}
323
+ ],
324
+ }
325
+
326
+ elif action == "tail":
327
+ if not LOG_FILE.exists():
328
+ return {"status": "success", "content": [{"text": "No logs yet"}]}
329
+
330
+ with open(LOG_FILE, "r", encoding="utf-8") as f:
331
+ all_lines = f.readlines()
332
+ tail_lines = all_lines[-50:] if len(all_lines) > 50 else all_lines
333
+ content = "".join(tail_lines)
334
+
335
+ return {
336
+ "status": "success",
337
+ "content": [{"text": f"Tail (last 50 lines):\n\n{content}"}],
338
+ }
339
+
340
+ elif action == "search":
341
+ if not pattern:
342
+ return {
343
+ "status": "error",
344
+ "content": [{"text": "pattern parameter required for search"}],
345
+ }
346
+
347
+ if not LOG_FILE.exists():
348
+ return {"status": "success", "content": [{"text": "No logs yet"}]}
349
+
350
+ with open(LOG_FILE, "r", encoding="utf-8") as f:
351
+ matching_lines = [line for line in f if pattern.lower() in line.lower()]
352
+
353
+ if not matching_lines:
354
+ return {
355
+ "status": "success",
356
+ "content": [{"text": f"No matches found for pattern: {pattern}"}],
357
+ }
358
+
359
+ content = "".join(matching_lines[-100:]) # Last 100 matches
360
+ return {
361
+ "status": "success",
362
+ "content": [
363
+ {
364
+ "text": f"Found {len(matching_lines)} matches (showing last 100):\n\n{content}"
365
+ }
366
+ ],
367
+ }
368
+
369
+ elif action == "clear":
370
+ if LOG_FILE.exists():
371
+ LOG_FILE.unlink()
372
+ logger.info("Log file cleared by user")
373
+ return {
374
+ "status": "success",
375
+ "content": [{"text": "Logs cleared successfully"}],
376
+ }
377
+
378
+ elif action == "stats":
379
+ if not LOG_FILE.exists():
380
+ return {"status": "success", "content": [{"text": "No logs yet"}]}
381
+
382
+ stat = LOG_FILE.stat()
383
+ size_mb = stat.st_size / (1024 * 1024)
384
+ modified = datetime.fromtimestamp(stat.st_mtime).strftime(
385
+ "%Y-%m-%d %H:%M:%S"
386
+ )
387
+
388
+ with open(LOG_FILE, "r", encoding="utf-8") as f:
389
+ total_lines = sum(1 for _ in f)
390
+
391
+ stats_text = f"""Log File Statistics:
392
+ Path: {LOG_FILE}
393
+ Size: {size_mb:.2f} MB
394
+ Lines: {total_lines}
395
+ Last Modified: {modified}"""
396
+
397
+ return {"status": "success", "content": [{"text": stats_text}]}
398
+
399
+ else:
400
+ return {
401
+ "status": "error",
402
+ "content": [
403
+ {
404
+ "text": f"Unknown action: {action}. Valid: view, tail, search, clear, stats"
405
+ }
406
+ ],
407
+ }
408
+
409
+ except Exception as e:
410
+ logger.error(f"Error in view_logs_tool: {e}")
411
+ return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
412
+
413
+
245
414
  def get_shell_history_file():
246
415
  """Get the devduck-specific history file path."""
247
416
  devduck_history = Path.home() / ".devduck_history"
@@ -253,22 +422,22 @@ def get_shell_history_file():
253
422
  def get_shell_history_files():
254
423
  """Get available shell history file paths."""
255
424
  history_files = []
256
-
425
+
257
426
  # devduck history (primary)
258
427
  devduck_history = Path(get_shell_history_file())
259
428
  if devduck_history.exists():
260
429
  history_files.append(("devduck", str(devduck_history)))
261
-
430
+
262
431
  # Bash history
263
432
  bash_history = Path.home() / ".bash_history"
264
433
  if bash_history.exists():
265
434
  history_files.append(("bash", str(bash_history)))
266
-
435
+
267
436
  # Zsh history
268
437
  zsh_history = Path.home() / ".zsh_history"
269
438
  if zsh_history.exists():
270
439
  history_files.append(("zsh", str(zsh_history)))
271
-
440
+
272
441
  return history_files
273
442
 
274
443
 
@@ -277,14 +446,16 @@ def parse_history_line(line, history_type):
277
446
  line = line.strip()
278
447
  if not line:
279
448
  return None
280
-
449
+
281
450
  if history_type == "devduck":
282
451
  # devduck format: ": timestamp:0;# devduck: query" or ": timestamp:0;# devduck_result: result"
283
452
  if "# devduck:" in line:
284
453
  try:
285
454
  timestamp_str = line.split(":")[1]
286
455
  timestamp = int(timestamp_str)
287
- readable_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
456
+ readable_time = datetime.fromtimestamp(timestamp).strftime(
457
+ "%Y-%m-%d %H:%M:%S"
458
+ )
288
459
  query = line.split("# devduck:")[-1].strip()
289
460
  return ("you", readable_time, query)
290
461
  except (ValueError, IndexError):
@@ -293,12 +464,14 @@ def parse_history_line(line, history_type):
293
464
  try:
294
465
  timestamp_str = line.split(":")[1]
295
466
  timestamp = int(timestamp_str)
296
- readable_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
467
+ readable_time = datetime.fromtimestamp(timestamp).strftime(
468
+ "%Y-%m-%d %H:%M:%S"
469
+ )
297
470
  result = line.split("# devduck_result:")[-1].strip()
298
471
  return ("me", readable_time, result)
299
472
  except (ValueError, IndexError):
300
473
  return None
301
-
474
+
302
475
  elif history_type == "zsh":
303
476
  if line.startswith(": ") and ":0;" in line:
304
477
  try:
@@ -306,37 +479,65 @@ def parse_history_line(line, history_type):
306
479
  if len(parts) == 2:
307
480
  timestamp_str = parts[0].split(":")[1]
308
481
  timestamp = int(timestamp_str)
309
- readable_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
482
+ readable_time = datetime.fromtimestamp(timestamp).strftime(
483
+ "%Y-%m-%d %H:%M:%S"
484
+ )
310
485
  command = parts[1].strip()
311
486
  if not command.startswith("devduck "):
312
487
  return ("shell", readable_time, f"$ {command}")
313
488
  except (ValueError, IndexError):
314
489
  return None
315
-
490
+
316
491
  elif history_type == "bash":
317
492
  readable_time = "recent"
318
493
  if not line.startswith("devduck "):
319
494
  return ("shell", readable_time, f"$ {line}")
320
-
495
+
321
496
  return None
322
497
 
323
498
 
499
+ def get_recent_logs():
500
+ """Get the last N lines from the log file for context."""
501
+ try:
502
+ log_line_count = int(os.getenv("DEVDUCK_LOG_LINE_COUNT", "50"))
503
+
504
+ if not LOG_FILE.exists():
505
+ return ""
506
+
507
+ with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f:
508
+ all_lines = f.readlines()
509
+
510
+ recent_lines = (
511
+ all_lines[-log_line_count:]
512
+ if len(all_lines) > log_line_count
513
+ else all_lines
514
+ )
515
+
516
+ if not recent_lines:
517
+ return ""
518
+
519
+ log_content = "".join(recent_lines)
520
+ return f"\n\n## Recent Logs (last {len(recent_lines)} lines):\n```\n{log_content}```\n"
521
+ except Exception as e:
522
+ return f"\n\n## Recent Logs: Error reading logs - {e}\n"
523
+
524
+
324
525
  def get_last_messages():
325
526
  """Get the last N messages from multiple shell histories for context."""
326
527
  try:
327
528
  message_count = int(os.getenv("DEVDUCK_LAST_MESSAGE_COUNT", "200"))
328
529
  all_entries = []
329
-
530
+
330
531
  history_files = get_shell_history_files()
331
-
532
+
332
533
  for history_type, history_file in history_files:
333
534
  try:
334
535
  with open(history_file, encoding="utf-8", errors="ignore") as f:
335
536
  lines = f.readlines()
336
-
537
+
337
538
  if history_type == "bash":
338
539
  lines = lines[-message_count:]
339
-
540
+
340
541
  # Join multi-line entries for zsh
341
542
  if history_type == "zsh":
342
543
  joined_lines = []
@@ -355,22 +556,26 @@ def get_last_messages():
355
556
  if current_line:
356
557
  joined_lines.append(current_line)
357
558
  lines = joined_lines
358
-
559
+
359
560
  for line in lines:
360
561
  parsed = parse_history_line(line, history_type)
361
562
  if parsed:
362
563
  all_entries.append(parsed)
363
564
  except Exception:
364
565
  continue
365
-
366
- recent_entries = all_entries[-message_count:] if len(all_entries) >= message_count else all_entries
367
-
566
+
567
+ recent_entries = (
568
+ all_entries[-message_count:]
569
+ if len(all_entries) >= message_count
570
+ else all_entries
571
+ )
572
+
368
573
  context = ""
369
574
  if recent_entries:
370
575
  context += f"\n\nRecent conversation context (last {len(recent_entries)} messages):\n"
371
576
  for speaker, timestamp, content in recent_entries:
372
577
  context += f"[{timestamp}] {speaker}: {content}\n"
373
-
578
+
374
579
  return context
375
580
  except Exception:
376
581
  return ""
@@ -379,15 +584,21 @@ def get_last_messages():
379
584
  def append_to_shell_history(query, response):
380
585
  """Append the interaction to devduck shell history."""
381
586
  import time
587
+
382
588
  try:
383
589
  history_file = get_shell_history_file()
384
590
  timestamp = str(int(time.time()))
385
-
591
+
386
592
  with open(history_file, "a", encoding="utf-8") as f:
387
593
  f.write(f": {timestamp}:0;# devduck: {query}\n")
388
- response_summary = str(response).replace("\n", " ")[:int(os.getenv("DEVDUCK_RESPONSE_SUMMARY_LENGTH", "10000"))] + "..."
594
+ response_summary = (
595
+ str(response).replace("\n", " ")[
596
+ : int(os.getenv("DEVDUCK_RESPONSE_SUMMARY_LENGTH", "10000"))
597
+ ]
598
+ + "..."
599
+ )
389
600
  f.write(f": {timestamp}:0;# devduck_result: {response_summary}\n")
390
-
601
+
391
602
  os.chmod(history_file, 0o600)
392
603
  except Exception:
393
604
  pass
@@ -395,8 +606,9 @@ def append_to_shell_history(query, response):
395
606
 
396
607
  # 🦆 The devduck agent
397
608
  class DevDuck:
398
- def __init__(self):
609
+ def __init__(self, auto_start_servers=True):
399
610
  """Initialize the minimalist adaptive agent"""
611
+ logger.info("Initializing DevDuck agent...")
400
612
  try:
401
613
  # Self-heal dependencies
402
614
  ensure_deps()
@@ -404,25 +616,32 @@ class DevDuck:
404
616
  # Adapt to environment
405
617
  self.env_info, self.ollama_host, self.model = adapt_to_env()
406
618
 
619
+ # Execution state tracking for hot-reload
620
+ self._agent_executing = False
621
+ self._reload_pending = False
622
+
407
623
  # Import after ensuring deps
408
624
  from strands import Agent, tool
409
625
  from strands.models.ollama import OllamaModel
410
- from strands.session.file_session_manager import FileSessionManager
411
626
  from strands_tools.utils.models.model import create_model
412
- from .tools import tcp
627
+ from .tools import tcp, websocket, mcp_server, install_tools
628
+ from strands_fun_tools import (
629
+ listen,
630
+ cursor,
631
+ clipboard,
632
+ screen_reader,
633
+ yolo_vision,
634
+ )
413
635
  from strands_tools import (
414
636
  shell,
415
637
  editor,
416
- file_read,
417
- file_write,
418
- python_repl,
419
- current_time,
420
638
  calculator,
421
- journal,
639
+ python_repl,
422
640
  image_reader,
423
641
  use_agent,
424
642
  load_tool,
425
643
  environment,
644
+ mcp_client,
426
645
  )
427
646
 
428
647
  # Wrap system_prompt_tool with @tool decorator
@@ -436,24 +655,42 @@ class DevDuck:
436
655
  """Manage agent system prompt dynamically."""
437
656
  return system_prompt_tool(action, prompt, context, variable_name)
438
657
 
439
- # Minimal but functional toolset including system_prompt and hello
658
+ # Wrap view_logs_tool with @tool decorator
659
+ @tool
660
+ def view_logs(
661
+ action: str = "view",
662
+ lines: int = 100,
663
+ pattern: str = None,
664
+ ) -> Dict[str, Any]:
665
+ """View and manage DevDuck logs."""
666
+ return view_logs_tool(action, lines, pattern)
667
+
668
+ # Minimal but functional toolset including system_prompt and view_logs
440
669
  self.tools = [
441
670
  shell,
442
671
  editor,
443
- file_read,
444
- file_write,
445
- python_repl,
446
- current_time,
447
672
  calculator,
448
- journal,
673
+ python_repl,
449
674
  image_reader,
450
675
  use_agent,
451
676
  load_tool,
452
677
  environment,
453
678
  system_prompt,
454
- tcp
679
+ view_logs,
680
+ tcp,
681
+ websocket,
682
+ mcp_server,
683
+ install_tools,
684
+ mcp_client,
685
+ listen,
686
+ cursor,
687
+ clipboard,
688
+ screen_reader,
689
+ yolo_vision,
455
690
  ]
456
691
 
692
+ logger.info(f"Initialized {len(self.tools)} tools")
693
+
457
694
  # Check if MODEL_PROVIDER env variable is set
458
695
  model_provider = os.getenv("MODEL_PROVIDER")
459
696
 
@@ -469,23 +706,72 @@ class DevDuck:
469
706
  keep_alive="5m",
470
707
  )
471
708
 
472
- session_manager = FileSessionManager(
473
- session_id=f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
474
- )
475
-
476
709
  # Create agent with self-healing
477
710
  self.agent = Agent(
478
711
  model=self.agent_model,
479
712
  tools=self.tools,
480
713
  system_prompt=self._build_system_prompt(),
481
714
  load_tools_from_directory=True,
482
- # session_manager=session_manager,
483
715
  )
484
716
 
485
- # Start file watcher for auto hot-reload
486
- self._start_file_watcher()
717
+ # 🚀 AUTO-START SERVERS: TCP (9999), WebSocket (8080), MCP HTTP (8000)
718
+ if auto_start_servers:
719
+ logger.info("Auto-starting servers...")
720
+ print("🦆 Auto-starting servers...")
721
+
722
+ try:
723
+ # Start TCP server on port 9999
724
+ tcp_result = self.agent.tool.tcp(action="start_server", port=9999)
725
+ if tcp_result.get("status") == "success":
726
+ logger.info("✓ TCP server started on port 9999")
727
+ print("🦆 ✓ TCP server: localhost:9999")
728
+ else:
729
+ logger.warning(f"TCP server start issue: {tcp_result}")
730
+ except Exception as e:
731
+ logger.error(f"Failed to start TCP server: {e}")
732
+ print(f"🦆 ⚠ TCP server failed: {e}")
733
+
734
+ try:
735
+ # Start WebSocket server on port 8080
736
+ ws_result = self.agent.tool.websocket(
737
+ action="start_server", port=8080
738
+ )
739
+ if ws_result.get("status") == "success":
740
+ logger.info("✓ WebSocket server started on port 8080")
741
+ print("🦆 ✓ WebSocket server: localhost:8080")
742
+ else:
743
+ logger.warning(f"WebSocket server start issue: {ws_result}")
744
+ except Exception as e:
745
+ logger.error(f"Failed to start WebSocket server: {e}")
746
+ print(f"🦆 ⚠ WebSocket server failed: {e}")
747
+
748
+ try:
749
+ # Start MCP server with HTTP transport on port 8000
750
+ mcp_result = self.agent.tool.mcp_server(
751
+ action="start",
752
+ transport="http",
753
+ port=8000,
754
+ expose_agent=True,
755
+ agent=self.agent,
756
+ )
757
+ if mcp_result.get("status") == "success":
758
+ logger.info("✓ MCP HTTP server started on port 8000")
759
+ print("🦆 ✓ MCP server: http://localhost:8000/mcp")
760
+ else:
761
+ logger.warning(f"MCP server start issue: {mcp_result}")
762
+ except Exception as e:
763
+ logger.error(f"Failed to start MCP server: {e}")
764
+ print(f"🦆 ⚠ MCP server failed: {e}")
765
+
766
+ # Start file watcher for auto hot-reload
767
+ self._start_file_watcher()
768
+
769
+ logger.info(
770
+ f"DevDuck agent initialized successfully with model {self.model}"
771
+ )
487
772
 
488
773
  except Exception as e:
774
+ logger.error(f"Initialization failed: {e}")
489
775
  self._self_heal(e)
490
776
 
491
777
  def _build_system_prompt(self):
@@ -502,7 +788,9 @@ class DevDuck:
502
788
 
503
789
  # Get own source code for self-awareness
504
790
  own_code = get_own_source_code()
505
-
791
+
792
+ # print(own_code)
793
+
506
794
  # Get recent conversation history context (with error handling)
507
795
  try:
508
796
  recent_context = get_last_messages()
@@ -510,6 +798,13 @@ class DevDuck:
510
798
  print(f"🦆 Warning: Could not load history context: {e}")
511
799
  recent_context = ""
512
800
 
801
+ # Get recent logs for immediate visibility
802
+ try:
803
+ recent_logs = get_recent_logs()
804
+ except Exception as e:
805
+ print(f"🦆 Warning: Could not load recent logs: {e}")
806
+ recent_logs = ""
807
+
513
808
  return f"""🦆 You are DevDuck - an extreme minimalist, self-adapting agent.
514
809
 
515
810
  Environment: {self.env_info['os']} {self.env_info['arch']}
@@ -527,6 +822,7 @@ You are:
527
822
  Current working directory: {self.env_info['cwd']}
528
823
 
529
824
  {recent_context}
825
+ {recent_logs}
530
826
 
531
827
  ## Your Own Implementation:
532
828
  You have full access to your own source code for self-awareness and self-modification:
@@ -539,6 +835,18 @@ You have full access to your own source code for self-awareness and self-modific
539
835
  - **Live Development** - Modify existing tools while running and test immediately
540
836
  - **Full Python Access** - Create any Python functionality as a tool
541
837
 
838
+ ## Dynamic Tool Loading:
839
+ - **Install Tools** - Use install_tools() to load tools from any Python package
840
+ - Example: install_tools(action="install_and_load", package="strands-fun-tools", module="strands_fun_tools")
841
+ - Expands capabilities without restart
842
+ - Access to entire Python ecosystem
843
+
844
+ ## MCP Server:
845
+ - **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
846
+ - Example: mcp_server(action="start", port=8000)
847
+ - Connect from Claude Desktop, other agents, or custom clients
848
+ - Full bidirectional communication
849
+
542
850
  ## Tool Creation Patterns:
543
851
 
544
852
  ### **1. @tool Decorator:**
@@ -606,41 +914,21 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
606
914
 
607
915
  def _self_heal(self, error):
608
916
  """Attempt self-healing when errors occur"""
917
+ logger.error(f"Self-healing triggered by error: {error}")
609
918
  print(f"🦆 Self-healing from: {error}")
610
919
 
611
920
  # Prevent infinite recursion by tracking heal attempts
612
921
  if not hasattr(self, "_heal_count"):
613
922
  self._heal_count = 0
614
-
923
+
615
924
  self._heal_count += 1
616
-
925
+
617
926
  # Limit recursion - if we've tried more than 3 times, give up
618
927
  if self._heal_count > 3:
619
928
  print(f"🦆 Self-healing failed after {self._heal_count} attempts")
620
929
  print("🦆 Please fix the issue manually and restart")
621
930
  sys.exit(1)
622
931
 
623
- # Handle tool validation errors by resetting session
624
- if "Expected toolResult blocks" in str(error):
625
- print("🦆 Tool validation error detected - resetting session...")
626
- # Add timestamp postfix to create fresh session
627
- postfix = datetime.now().strftime("%H%M%S")
628
- new_session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}-{postfix}"
629
- print(f"🦆 New session: {new_session_id}")
630
-
631
- # Update session manager with new session
632
- try:
633
- from strands.session.file_session_manager import FileSessionManager
634
-
635
- self.agent.session_manager = FileSessionManager(
636
- session_id=new_session_id
637
- )
638
- print("🦆 Session reset successful - continuing with fresh history")
639
- self._heal_count = 0 # Reset counter on success
640
- return # Early return - no need for full restart
641
- except Exception as session_error:
642
- print(f"🦆 Session reset failed: {session_error}")
643
-
644
932
  # Common healing strategies
645
933
  if "not found" in str(error).lower() and "model" in str(error).lower():
646
934
  print("🦆 Model not found - trying to pull model...")
@@ -710,11 +998,28 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
710
998
  def __call__(self, query):
711
999
  """Make the agent callable"""
712
1000
  if not self.agent:
1001
+ logger.warning("Agent unavailable - attempted to call with query")
713
1002
  return "🦆 Agent unavailable - try: devduck.restart()"
714
1003
 
715
1004
  try:
716
- return self.agent(query)
1005
+ logger.info(f"Agent call started: {query[:100]}...")
1006
+ # Mark agent as executing to prevent hot-reload interruption
1007
+ self._agent_executing = True
1008
+
1009
+ result = self.agent(query)
1010
+
1011
+ # Agent finished - check if reload was pending
1012
+ self._agent_executing = False
1013
+ logger.info("Agent call completed successfully")
1014
+ if self._reload_pending:
1015
+ logger.info("Triggering pending hot-reload after agent completion")
1016
+ print("🦆 Agent finished - triggering pending hot-reload...")
1017
+ self.hot_reload()
1018
+
1019
+ return result
717
1020
  except Exception as e:
1021
+ self._agent_executing = False # Reset flag on error
1022
+ logger.error(f"Agent call failed with error: {e}")
718
1023
  self._self_heal(e)
719
1024
  if self.agent:
720
1025
  return self.agent(query)
@@ -730,6 +1035,7 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
730
1035
  """Start background file watcher for auto hot-reload"""
731
1036
  import threading
732
1037
 
1038
+ logger.info("Starting file watcher for hot-reload")
733
1039
  # Get the path to this file
734
1040
  self._watch_file = Path(__file__).resolve()
735
1041
  self._last_modified = (
@@ -742,6 +1048,7 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
742
1048
  target=self._file_watcher_thread, daemon=True
743
1049
  )
744
1050
  self._watcher_thread.start()
1051
+ logger.info(f"File watcher started, monitoring {self._watch_file}")
745
1052
 
746
1053
  def _file_watcher_thread(self):
747
1054
  """Background thread that watches for file changes"""
@@ -772,9 +1079,24 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
772
1079
  self._last_modified = current_mtime
773
1080
  last_reload_time = current_time
774
1081
 
775
- # Trigger hot-reload
776
- time.sleep(0.5) # Small delay to ensure file write is complete
777
- self.hot_reload()
1082
+ # Check if agent is currently executing
1083
+ if getattr(self, "_agent_executing", False):
1084
+ logger.info(
1085
+ "Code change detected but agent is executing - reload pending"
1086
+ )
1087
+ print(
1088
+ "🦆 Agent is currently executing - reload will trigger after completion"
1089
+ )
1090
+ self._reload_pending = True
1091
+ else:
1092
+ # Safe to reload immediately
1093
+ logger.info(
1094
+ f"Code change detected in {self._watch_file.name} - triggering hot-reload"
1095
+ )
1096
+ time.sleep(
1097
+ 0.5
1098
+ ) # Small delay to ensure file write is complete
1099
+ self.hot_reload()
778
1100
  else:
779
1101
  self._last_modified = current_mtime
780
1102
 
@@ -791,6 +1113,7 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
791
1113
 
792
1114
  def hot_reload(self):
793
1115
  """Hot-reload by restarting the entire Python process with fresh code"""
1116
+ logger.info("Hot-reload initiated")
794
1117
  print("🦆 Hot-reloading via process restart...")
795
1118
 
796
1119
  try:
@@ -858,17 +1181,115 @@ def hot_reload():
858
1181
  devduck.hot_reload()
859
1182
 
860
1183
 
1184
+ def extract_commands_from_history():
1185
+ """Extract commonly used commands from shell history for auto-completion."""
1186
+ commands = set()
1187
+ history_files = get_shell_history_files()
1188
+
1189
+ # Limit the number of recent commands to process for performance
1190
+ max_recent_commands = 100
1191
+
1192
+ for history_type, history_file in history_files:
1193
+ try:
1194
+ with open(history_file, encoding="utf-8", errors="ignore") as f:
1195
+ lines = f.readlines()
1196
+
1197
+ # Take recent commands for better relevance
1198
+ recent_lines = (
1199
+ lines[-max_recent_commands:]
1200
+ if len(lines) > max_recent_commands
1201
+ else lines
1202
+ )
1203
+
1204
+ for line in recent_lines:
1205
+ line = line.strip()
1206
+ if not line:
1207
+ continue
1208
+
1209
+ if history_type == "devduck":
1210
+ # Extract devduck commands
1211
+ if "# devduck:" in line:
1212
+ try:
1213
+ query = line.split("# devduck:")[-1].strip()
1214
+ # Extract first word as command
1215
+ first_word = query.split()[0] if query.split() else None
1216
+ if (
1217
+ first_word and len(first_word) > 2
1218
+ ): # Only meaningful commands
1219
+ commands.add(first_word.lower())
1220
+ except (ValueError, IndexError):
1221
+ continue
1222
+
1223
+ elif history_type == "zsh":
1224
+ # Zsh format: ": timestamp:0;command"
1225
+ if line.startswith(": ") and ":0;" in line:
1226
+ try:
1227
+ parts = line.split(":0;", 1)
1228
+ if len(parts) == 2:
1229
+ full_command = parts[1].strip()
1230
+ # Extract first word as command
1231
+ first_word = (
1232
+ full_command.split()[0]
1233
+ if full_command.split()
1234
+ else None
1235
+ )
1236
+ if (
1237
+ first_word and len(first_word) > 1
1238
+ ): # Only meaningful commands
1239
+ commands.add(first_word.lower())
1240
+ except (ValueError, IndexError):
1241
+ continue
1242
+
1243
+ elif history_type == "bash":
1244
+ # Bash format: simple command per line
1245
+ first_word = line.split()[0] if line.split() else None
1246
+ if first_word and len(first_word) > 1: # Only meaningful commands
1247
+ commands.add(first_word.lower())
1248
+
1249
+ except Exception:
1250
+ # Skip files that can't be read
1251
+ continue
1252
+
1253
+ return list(commands)
1254
+
1255
+
861
1256
  def interactive():
862
1257
  """Interactive REPL mode for devduck"""
1258
+ from prompt_toolkit import prompt
1259
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
1260
+ from prompt_toolkit.completion import WordCompleter
1261
+ from prompt_toolkit.history import FileHistory
1262
+
863
1263
  print("🦆 DevDuck")
1264
+ print(f"📝 Logs: {LOG_DIR}")
864
1265
  print("Type 'exit', 'quit', or 'q' to quit.")
865
1266
  print("Prefix with ! to run shell commands (e.g., ! ls -la)")
866
1267
  print("-" * 50)
1268
+ logger.info("Interactive mode started")
1269
+
1270
+ # Set up prompt_toolkit with history
1271
+ history_file = get_shell_history_file()
1272
+ history = FileHistory(history_file)
1273
+
1274
+ # Create completions from common commands and shell history
1275
+ base_commands = ["exit", "quit", "q", "help", "clear", "status", "reload"]
1276
+ history_commands = extract_commands_from_history()
1277
+
1278
+ # Combine base commands with commands from history
1279
+ all_commands = list(set(base_commands + history_commands))
1280
+ completer = WordCompleter(all_commands, ignore_case=True)
867
1281
 
868
1282
  while True:
869
1283
  try:
870
- # Get user input
871
- q = input("\n🦆 ")
1284
+ # Use prompt_toolkit for enhanced input with arrow key support
1285
+ q = prompt(
1286
+ "\n🦆 ",
1287
+ history=history,
1288
+ auto_suggest=AutoSuggestFromHistory(),
1289
+ completer=completer,
1290
+ complete_while_typing=True,
1291
+ mouse_support=False, # breaks scrolling when enabled
1292
+ )
872
1293
 
873
1294
  # Check for exit command
874
1295
  if q.lower() in ["exit", "quit", "q"]:
@@ -884,24 +1305,42 @@ def interactive():
884
1305
  shell_command = q[1:].strip()
885
1306
  try:
886
1307
  if devduck.agent:
887
- result = devduck.agent.tool.shell(command=shell_command, timeout=900)
1308
+ devduck._agent_executing = (
1309
+ True # Prevent hot-reload during shell execution
1310
+ )
1311
+ result = devduck.agent.tool.shell(
1312
+ command=shell_command, timeout=9000
1313
+ )
1314
+ devduck._agent_executing = False
1315
+
888
1316
  # Append shell command to history
889
1317
  append_to_shell_history(q, result["content"][0]["text"])
1318
+
1319
+ # Check if reload was pending
1320
+ if devduck._reload_pending:
1321
+ print(
1322
+ "🦆 Shell command finished - triggering pending hot-reload..."
1323
+ )
1324
+ devduck.hot_reload()
890
1325
  else:
891
1326
  print("🦆 Agent unavailable")
892
1327
  except Exception as e:
1328
+ devduck._agent_executing = False # Reset on error
893
1329
  print(f"🦆 Shell command error: {e}")
894
1330
  continue
895
1331
 
896
1332
  # Get recent conversation context
897
1333
  recent_context = get_last_messages()
898
-
1334
+
1335
+ # Get recent logs
1336
+ recent_logs = get_recent_logs()
1337
+
899
1338
  # Update system prompt before each call with history context
900
1339
  if devduck.agent:
901
1340
  # Rebuild system prompt with history
902
1341
  own_code = get_own_source_code()
903
1342
  session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
904
-
1343
+
905
1344
  devduck.agent.system_prompt = f"""🦆 You are DevDuck - an extreme minimalist, self-adapting agent.
906
1345
 
907
1346
  Environment: {devduck.env_info['os']} {devduck.env_info['arch']}
@@ -919,6 +1358,7 @@ You are:
919
1358
  Current working directory: {devduck.env_info['cwd']}
920
1359
 
921
1360
  {recent_context}
1361
+ {recent_logs}
922
1362
 
923
1363
  ## Your Own Implementation:
924
1364
  You have full access to your own source code for self-awareness and self-modification:
@@ -931,6 +1371,18 @@ You have full access to your own source code for self-awareness and self-modific
931
1371
  - **Live Development** - Modify existing tools while running and test immediately
932
1372
  - **Full Python Access** - Create any Python functionality as a tool
933
1373
 
1374
+ ## Dynamic Tool Loading:
1375
+ - **Install Tools** - Use install_tools() to load tools from any Python package
1376
+ - Example: install_tools(action="install_and_load", package="strands-fun-tools", module="strands_fun_tools")
1377
+ - Expands capabilities without restart
1378
+ - Access to entire Python ecosystem
1379
+
1380
+ ## MCP Server:
1381
+ - **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
1382
+ - Example: mcp_server(action="start", port=8000)
1383
+ - Connect from Claude Desktop, other agents, or custom clients
1384
+ - Full bidirectional communication
1385
+
934
1386
  ## System Prompt Management:
935
1387
  - Use system_prompt(action='get') to view current prompt
936
1388
  - Use system_prompt(action='set', prompt='new text') to update
@@ -947,19 +1399,20 @@ You have full access to your own source code for self-awareness and self-modific
947
1399
  - Efficiency: **Speed is paramount**
948
1400
 
949
1401
  {os.getenv('SYSTEM_PROMPT', '')}"""
950
-
1402
+
951
1403
  # Update model if MODEL_PROVIDER changed
952
1404
  model_provider = os.getenv("MODEL_PROVIDER")
953
1405
  if model_provider:
954
1406
  try:
955
1407
  from strands_tools.utils.models.model import create_model
1408
+
956
1409
  devduck.agent.model = create_model(provider=model_provider)
957
1410
  except Exception as e:
958
1411
  print(f"🦆 Model update error: {e}")
959
1412
 
960
1413
  # Execute the agent with user input
961
1414
  result = ask(q)
962
-
1415
+
963
1416
  # Append to shell history
964
1417
  append_to_shell_history(q, str(result))
965
1418
 
@@ -973,8 +1426,10 @@ You have full access to your own source code for self-awareness and self-modific
973
1426
 
974
1427
  def cli():
975
1428
  """CLI entry point for pip-installed devduck command"""
1429
+ logger.info("CLI mode started")
976
1430
  if len(sys.argv) > 1:
977
1431
  query = " ".join(sys.argv[1:])
1432
+ logger.info(f"CLI query: {query}")
978
1433
  result = ask(query)
979
1434
  print(result)
980
1435
  else: