code-puppy 0.0.348__py3-none-any.whl → 0.0.372__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.
Files changed (87) hide show
  1. code_puppy/agents/__init__.py +8 -0
  2. code_puppy/agents/agent_manager.py +272 -1
  3. code_puppy/agents/agent_pack_leader.py +383 -0
  4. code_puppy/agents/agent_qa_kitten.py +12 -7
  5. code_puppy/agents/agent_terminal_qa.py +323 -0
  6. code_puppy/agents/base_agent.py +11 -8
  7. code_puppy/agents/event_stream_handler.py +101 -8
  8. code_puppy/agents/pack/__init__.py +34 -0
  9. code_puppy/agents/pack/bloodhound.py +304 -0
  10. code_puppy/agents/pack/husky.py +321 -0
  11. code_puppy/agents/pack/retriever.py +393 -0
  12. code_puppy/agents/pack/shepherd.py +348 -0
  13. code_puppy/agents/pack/terrier.py +287 -0
  14. code_puppy/agents/pack/watchdog.py +367 -0
  15. code_puppy/agents/subagent_stream_handler.py +276 -0
  16. code_puppy/api/__init__.py +13 -0
  17. code_puppy/api/app.py +169 -0
  18. code_puppy/api/main.py +21 -0
  19. code_puppy/api/pty_manager.py +446 -0
  20. code_puppy/api/routers/__init__.py +12 -0
  21. code_puppy/api/routers/agents.py +36 -0
  22. code_puppy/api/routers/commands.py +217 -0
  23. code_puppy/api/routers/config.py +74 -0
  24. code_puppy/api/routers/sessions.py +232 -0
  25. code_puppy/api/templates/terminal.html +361 -0
  26. code_puppy/api/websocket.py +154 -0
  27. code_puppy/callbacks.py +73 -0
  28. code_puppy/chatgpt_codex_client.py +53 -0
  29. code_puppy/claude_cache_client.py +294 -41
  30. code_puppy/command_line/add_model_menu.py +13 -4
  31. code_puppy/command_line/agent_menu.py +662 -0
  32. code_puppy/command_line/core_commands.py +89 -112
  33. code_puppy/command_line/model_picker_completion.py +3 -20
  34. code_puppy/command_line/model_settings_menu.py +21 -3
  35. code_puppy/config.py +145 -70
  36. code_puppy/gemini_model.py +706 -0
  37. code_puppy/http_utils.py +6 -3
  38. code_puppy/messaging/__init__.py +15 -0
  39. code_puppy/messaging/messages.py +27 -0
  40. code_puppy/messaging/queue_console.py +1 -1
  41. code_puppy/messaging/rich_renderer.py +36 -1
  42. code_puppy/messaging/spinner/__init__.py +20 -2
  43. code_puppy/messaging/subagent_console.py +461 -0
  44. code_puppy/model_factory.py +50 -16
  45. code_puppy/model_switching.py +63 -0
  46. code_puppy/model_utils.py +27 -24
  47. code_puppy/models.json +12 -12
  48. code_puppy/plugins/antigravity_oauth/antigravity_model.py +206 -172
  49. code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
  50. code_puppy/plugins/antigravity_oauth/transport.py +236 -45
  51. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
  52. code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
  53. code_puppy/plugins/claude_code_oauth/utils.py +4 -1
  54. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  55. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  56. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  57. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  58. code_puppy/pydantic_patches.py +52 -0
  59. code_puppy/status_display.py +6 -2
  60. code_puppy/tools/__init__.py +37 -1
  61. code_puppy/tools/agent_tools.py +83 -33
  62. code_puppy/tools/browser/__init__.py +37 -0
  63. code_puppy/tools/browser/browser_control.py +6 -6
  64. code_puppy/tools/browser/browser_interactions.py +21 -20
  65. code_puppy/tools/browser/browser_locators.py +9 -9
  66. code_puppy/tools/browser/browser_manager.py +316 -0
  67. code_puppy/tools/browser/browser_navigation.py +7 -7
  68. code_puppy/tools/browser/browser_screenshot.py +78 -140
  69. code_puppy/tools/browser/browser_scripts.py +15 -13
  70. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  71. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  72. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  73. code_puppy/tools/browser/terminal_tools.py +525 -0
  74. code_puppy/tools/command_runner.py +292 -101
  75. code_puppy/tools/common.py +176 -1
  76. code_puppy/tools/display.py +84 -0
  77. code_puppy/tools/subagent_context.py +158 -0
  78. {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
  79. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/METADATA +17 -16
  80. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/RECORD +84 -51
  81. code_puppy/prompts/codex_system_prompt.md +0 -310
  82. code_puppy/tools/browser/camoufox_manager.py +0 -235
  83. code_puppy/tools/browser/vqa_agent.py +0 -90
  84. {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
  85. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
  86. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
  87. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/licenses/LICENSE +0 -0
@@ -5,19 +5,27 @@ configurations, each with their own system prompts and tool sets.
5
5
  """
6
6
 
7
7
  from .agent_manager import (
8
+ clone_agent,
9
+ delete_clone_agent,
8
10
  get_agent_descriptions,
9
11
  get_available_agents,
10
12
  get_current_agent,
13
+ is_clone_agent_name,
11
14
  load_agent,
12
15
  refresh_agents,
13
16
  set_current_agent,
14
17
  )
18
+ from .subagent_stream_handler import subagent_stream_handler
15
19
 
16
20
  __all__ = [
21
+ "clone_agent",
22
+ "delete_clone_agent",
17
23
  "get_available_agents",
18
24
  "get_current_agent",
25
+ "is_clone_agent_name",
19
26
  "set_current_agent",
20
27
  "load_agent",
21
28
  "get_agent_descriptions",
22
29
  "refresh_agents",
30
+ "subagent_stream_handler",
23
31
  ]
@@ -4,6 +4,7 @@ import importlib
4
4
  import json
5
5
  import os
6
6
  import pkgutil
7
+ import re
7
8
  import uuid
8
9
  from pathlib import Path
9
10
  from typing import Dict, List, Optional, Type, Union
@@ -13,7 +14,7 @@ from pydantic_ai.messages import ModelMessage
13
14
  from code_puppy.agents.base_agent import BaseAgent
14
15
  from code_puppy.agents.json_agent import JSONAgent, discover_json_agents
15
16
  from code_puppy.callbacks import on_agent_reload
16
- from code_puppy.messaging import emit_warning
17
+ from code_puppy.messaging import emit_success, emit_warning
17
18
 
18
19
  # Registry of available agents (Python classes and JSON file paths)
19
20
  _AGENT_REGISTRY: Dict[str, Union[Type[BaseAgent], str]] = {}
@@ -225,6 +226,55 @@ def _discover_agents(message_group_id: Optional[str] = None):
225
226
  )
226
227
  continue
227
228
 
229
+ # 1b. Discover agents in sub-packages (like 'pack')
230
+ for _, subpkg_name, ispkg in pkgutil.iter_modules(agents_package.__path__):
231
+ if not ispkg or subpkg_name.startswith("_"):
232
+ continue
233
+
234
+ try:
235
+ # Import the sub-package
236
+ subpkg = importlib.import_module(f"code_puppy.agents.{subpkg_name}")
237
+
238
+ # Iterate through modules in the sub-package
239
+ if not hasattr(subpkg, "__path__"):
240
+ continue
241
+
242
+ for _, modname, _ in pkgutil.iter_modules(subpkg.__path__):
243
+ if modname.startswith("_"):
244
+ continue
245
+
246
+ try:
247
+ # Import the submodule
248
+ module = importlib.import_module(
249
+ f"code_puppy.agents.{subpkg_name}.{modname}"
250
+ )
251
+
252
+ # Look for BaseAgent subclasses
253
+ for attr_name in dir(module):
254
+ attr = getattr(module, attr_name)
255
+ if (
256
+ isinstance(attr, type)
257
+ and issubclass(attr, BaseAgent)
258
+ and attr not in [BaseAgent, JSONAgent]
259
+ ):
260
+ # Create an instance to get the name
261
+ agent_instance = attr()
262
+ _AGENT_REGISTRY[agent_instance.name] = attr
263
+
264
+ except Exception as e:
265
+ emit_warning(
266
+ f"Warning: Could not load agent {subpkg_name}.{modname}: {e}",
267
+ message_group=message_group_id,
268
+ )
269
+ continue
270
+
271
+ except Exception as e:
272
+ emit_warning(
273
+ f"Warning: Could not load agent sub-package {subpkg_name}: {e}",
274
+ message_group=message_group_id,
275
+ )
276
+ continue
277
+
228
278
  # 2. Discover JSON agents in user directory
229
279
  try:
230
280
  json_agents = discover_json_agents()
@@ -246,12 +296,21 @@ def get_available_agents() -> Dict[str, str]:
246
296
  Returns:
247
297
  Dict mapping agent names to display names.
248
298
  """
299
+ from ..config import PACK_AGENT_NAMES, get_pack_agents_enabled
300
+
249
301
  # Generate a message group ID for this operation
250
302
  message_group_id = str(uuid.uuid4())
251
303
  _discover_agents(message_group_id=message_group_id)
252
304
 
305
+ # Check if pack agents are enabled
306
+ pack_agents_enabled = get_pack_agents_enabled()
307
+
253
308
  agents = {}
254
309
  for name, agent_ref in _AGENT_REGISTRY.items():
310
+ # Filter out pack agents if disabled
311
+ if not pack_agents_enabled and name in PACK_AGENT_NAMES:
312
+ continue
313
+
255
314
  try:
256
315
  if isinstance(agent_ref, str): # JSON agent (file path)
257
316
  agent_instance = JSONAgent(agent_ref)
@@ -374,12 +433,21 @@ def get_agent_descriptions() -> Dict[str, str]:
374
433
  Returns:
375
434
  Dict mapping agent names to their descriptions.
376
435
  """
436
+ from ..config import PACK_AGENT_NAMES, get_pack_agents_enabled
437
+
377
438
  # Generate a message group ID for this operation
378
439
  message_group_id = str(uuid.uuid4())
379
440
  _discover_agents(message_group_id=message_group_id)
380
441
 
442
+ # Check if pack agents are enabled
443
+ pack_agents_enabled = get_pack_agents_enabled()
444
+
381
445
  descriptions = {}
382
446
  for name, agent_ref in _AGENT_REGISTRY.items():
447
+ # Filter out pack agents if disabled
448
+ if not pack_agents_enabled and name in PACK_AGENT_NAMES:
449
+ continue
450
+
383
451
  try:
384
452
  if isinstance(agent_ref, str): # JSON agent (file path)
385
453
  agent_instance = JSONAgent(agent_ref)
@@ -400,3 +468,206 @@ def refresh_agents():
400
468
  # Generate a message group ID for agent refreshing
401
469
  message_group_id = str(uuid.uuid4())
402
470
  _discover_agents(message_group_id=message_group_id)
471
+
472
+
473
+ _CLONE_NAME_PATTERN = re.compile(r"^(?P<base>.+)-clone-(?P<index>\d+)$")
474
+ _CLONE_DISPLAY_PATTERN = re.compile(r"\s*\(Clone\s+\d+\)$", re.IGNORECASE)
475
+
476
+
477
+ def _strip_clone_suffix(agent_name: str) -> str:
478
+ """Strip a trailing -clone-N suffix from a name if present."""
479
+ match = _CLONE_NAME_PATTERN.match(agent_name)
480
+ return match.group("base") if match else agent_name
481
+
482
+
483
+ def _strip_clone_display_suffix(display_name: str) -> str:
484
+ """Remove a trailing "(Clone N)" suffix from display names."""
485
+ cleaned = _CLONE_DISPLAY_PATTERN.sub("", display_name).strip()
486
+ return cleaned or display_name
487
+
488
+
489
+ def is_clone_agent_name(agent_name: str) -> bool:
490
+ """Return True if the agent name looks like a clone."""
491
+ return bool(_CLONE_NAME_PATTERN.match(agent_name))
492
+
493
+
494
+ def _default_display_name(agent_name: str) -> str:
495
+ """Build a default display name from an agent name."""
496
+ title = agent_name.title()
497
+ return f"{title} 🤖"
498
+
499
+
500
+ def _build_clone_display_name(display_name: str, clone_index: int) -> str:
501
+ """Build a clone display name based on the source display name."""
502
+ base_name = _strip_clone_display_suffix(display_name)
503
+ return f"{base_name} (Clone {clone_index})"
504
+
505
+
506
+ def _filter_available_tools(tool_names: List[str]) -> List[str]:
507
+ """Filter a tool list to only available tool names."""
508
+ from code_puppy.tools import get_available_tool_names
509
+
510
+ available_tools = set(get_available_tool_names())
511
+ return [tool for tool in tool_names if tool in available_tools]
512
+
513
+
514
+ def _next_clone_index(
515
+ base_name: str, existing_names: set[str], agents_dir: Path
516
+ ) -> int:
517
+ """Compute the next clone index for a base name."""
518
+ clone_pattern = re.compile(rf"^{re.escape(base_name)}-clone-(\\d+)$")
519
+ indices = []
520
+ for name in existing_names:
521
+ match = clone_pattern.match(name)
522
+ if match:
523
+ indices.append(int(match.group(1)))
524
+
525
+ next_index = max(indices, default=0) + 1
526
+ while True:
527
+ clone_name = f"{base_name}-clone-{next_index}"
528
+ clone_path = agents_dir / f"{clone_name}.json"
529
+ if clone_name not in existing_names and not clone_path.exists():
530
+ return next_index
531
+ next_index += 1
532
+
533
+
534
+ def clone_agent(agent_name: str) -> Optional[str]:
535
+ """Clone an agent definition into the user agents directory.
536
+
537
+ Args:
538
+ agent_name: Source agent name to clone.
539
+
540
+ Returns:
541
+ The cloned agent name, or None if cloning failed.
542
+ """
543
+ # Generate a message group ID for agent cloning
544
+ message_group_id = str(uuid.uuid4())
545
+ _discover_agents(message_group_id=message_group_id)
546
+
547
+ agent_ref = _AGENT_REGISTRY.get(agent_name)
548
+ if agent_ref is None:
549
+ emit_warning(f"Agent '{agent_name}' not found for cloning.")
550
+ return None
551
+
552
+ from ..config import get_agent_pinned_model, get_user_agents_directory
553
+
554
+ agents_dir = Path(get_user_agents_directory())
555
+ base_name = _strip_clone_suffix(agent_name)
556
+ existing_names = set(_AGENT_REGISTRY.keys())
557
+ clone_index = _next_clone_index(base_name, existing_names, agents_dir)
558
+ clone_name = f"{base_name}-clone-{clone_index}"
559
+ clone_path = agents_dir / f"{clone_name}.json"
560
+
561
+ try:
562
+ if isinstance(agent_ref, str):
563
+ with open(agent_ref, "r", encoding="utf-8") as f:
564
+ source_config = json.load(f)
565
+
566
+ source_display_name = source_config.get("display_name")
567
+ if not source_display_name:
568
+ source_display_name = _default_display_name(base_name)
569
+
570
+ clone_config = dict(source_config)
571
+ clone_config["name"] = clone_name
572
+ clone_config["display_name"] = _build_clone_display_name(
573
+ source_display_name, clone_index
574
+ )
575
+
576
+ tools = source_config.get("tools", [])
577
+ clone_config["tools"] = (
578
+ _filter_available_tools(tools) if isinstance(tools, list) else []
579
+ )
580
+
581
+ if not clone_config.get("model"):
582
+ clone_config.pop("model", None)
583
+ else:
584
+ agent_instance = agent_ref()
585
+ clone_config = {
586
+ "name": clone_name,
587
+ "display_name": _build_clone_display_name(
588
+ agent_instance.display_name, clone_index
589
+ ),
590
+ "description": agent_instance.description,
591
+ "system_prompt": agent_instance.get_system_prompt(),
592
+ "tools": _filter_available_tools(agent_instance.get_available_tools()),
593
+ }
594
+
595
+ user_prompt = agent_instance.get_user_prompt()
596
+ if user_prompt is not None:
597
+ clone_config["user_prompt"] = user_prompt
598
+
599
+ tools_config = agent_instance.get_tools_config()
600
+ if tools_config is not None:
601
+ clone_config["tools_config"] = tools_config
602
+
603
+ pinned_model = get_agent_pinned_model(agent_instance.name)
604
+ if pinned_model:
605
+ clone_config["model"] = pinned_model
606
+ except Exception as exc:
607
+ emit_warning(f"Failed to build clone for '{agent_name}': {exc}")
608
+ return None
609
+
610
+ if clone_path.exists():
611
+ emit_warning(f"Clone target '{clone_name}' already exists.")
612
+ return None
613
+
614
+ try:
615
+ with open(clone_path, "w", encoding="utf-8") as f:
616
+ json.dump(clone_config, f, indent=2, ensure_ascii=False)
617
+ emit_success(f"Cloned '{agent_name}' to '{clone_name}'.")
618
+ return clone_name
619
+ except Exception as exc:
620
+ emit_warning(f"Failed to write clone file '{clone_path}': {exc}")
621
+ return None
622
+
623
+
624
+ def delete_clone_agent(agent_name: str) -> bool:
625
+ """Delete a cloned JSON agent definition.
626
+
627
+ Args:
628
+ agent_name: Clone agent name to delete.
629
+
630
+ Returns:
631
+ True if the clone was deleted, False otherwise.
632
+ """
633
+ message_group_id = str(uuid.uuid4())
634
+ _discover_agents(message_group_id=message_group_id)
635
+
636
+ if not is_clone_agent_name(agent_name):
637
+ emit_warning(f"Agent '{agent_name}' is not a clone.")
638
+ return False
639
+
640
+ if get_current_agent_name() == agent_name:
641
+ emit_warning("Cannot delete the active agent. Switch agents first.")
642
+ return False
643
+
644
+ agent_ref = _AGENT_REGISTRY.get(agent_name)
645
+ if agent_ref is None:
646
+ emit_warning(f"Clone '{agent_name}' not found.")
647
+ return False
648
+
649
+ if not isinstance(agent_ref, str):
650
+ emit_warning(f"Clone '{agent_name}' is not a JSON agent.")
651
+ return False
652
+
653
+ clone_path = Path(agent_ref)
654
+ if not clone_path.exists():
655
+ emit_warning(f"Clone file for '{agent_name}' does not exist.")
656
+ return False
657
+
658
+ from ..config import get_user_agents_directory
659
+
660
+ agents_dir = Path(get_user_agents_directory()).resolve()
661
+ if clone_path.resolve().parent != agents_dir:
662
+ emit_warning(f"Refusing to delete non-user clone '{agent_name}'.")
663
+ return False
664
+
665
+ try:
666
+ clone_path.unlink()
667
+ emit_success(f"Deleted clone '{agent_name}'.")
668
+ _AGENT_REGISTRY.pop(agent_name, None)
669
+ _AGENT_HISTORIES.pop(agent_name, None)
670
+ return True
671
+ except Exception as exc:
672
+ emit_warning(f"Failed to delete clone '{agent_name}': {exc}")
673
+ return False