code-puppy 0.0.353__py3-none-any.whl → 0.0.355__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 (49) hide show
  1. code_puppy/agents/__init__.py +2 -0
  2. code_puppy/agents/agent_manager.py +49 -0
  3. code_puppy/agents/agent_pack_leader.py +383 -0
  4. code_puppy/agents/agent_planning.py +1 -0
  5. code_puppy/agents/event_stream_handler.py +74 -1
  6. code_puppy/agents/pack/__init__.py +34 -0
  7. code_puppy/agents/pack/bloodhound.py +304 -0
  8. code_puppy/agents/pack/husky.py +321 -0
  9. code_puppy/agents/pack/retriever.py +393 -0
  10. code_puppy/agents/pack/shepherd.py +348 -0
  11. code_puppy/agents/pack/terrier.py +287 -0
  12. code_puppy/agents/pack/watchdog.py +367 -0
  13. code_puppy/agents/subagent_stream_handler.py +276 -0
  14. code_puppy/api/__init__.py +13 -0
  15. code_puppy/api/app.py +92 -0
  16. code_puppy/api/main.py +21 -0
  17. code_puppy/api/pty_manager.py +446 -0
  18. code_puppy/api/routers/__init__.py +12 -0
  19. code_puppy/api/routers/agents.py +36 -0
  20. code_puppy/api/routers/commands.py +198 -0
  21. code_puppy/api/routers/config.py +74 -0
  22. code_puppy/api/routers/sessions.py +191 -0
  23. code_puppy/api/templates/terminal.html +361 -0
  24. code_puppy/api/websocket.py +154 -0
  25. code_puppy/callbacks.py +73 -0
  26. code_puppy/command_line/core_commands.py +85 -0
  27. code_puppy/config.py +63 -0
  28. code_puppy/messaging/__init__.py +15 -0
  29. code_puppy/messaging/messages.py +27 -0
  30. code_puppy/messaging/queue_console.py +1 -1
  31. code_puppy/messaging/rich_renderer.py +34 -0
  32. code_puppy/messaging/spinner/__init__.py +20 -2
  33. code_puppy/messaging/subagent_console.py +461 -0
  34. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  35. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  36. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  37. code_puppy/status_display.py +6 -2
  38. code_puppy/tools/agent_tools.py +56 -50
  39. code_puppy/tools/browser/vqa_agent.py +1 -1
  40. code_puppy/tools/common.py +176 -1
  41. code_puppy/tools/display.py +6 -1
  42. code_puppy/tools/subagent_context.py +158 -0
  43. {code_puppy-0.0.353.dist-info → code_puppy-0.0.355.dist-info}/METADATA +4 -3
  44. {code_puppy-0.0.353.dist-info → code_puppy-0.0.355.dist-info}/RECORD +49 -24
  45. {code_puppy-0.0.353.data → code_puppy-0.0.355.data}/data/code_puppy/models.json +0 -0
  46. {code_puppy-0.0.353.data → code_puppy-0.0.355.data}/data/code_puppy/models_dev_api.json +0 -0
  47. {code_puppy-0.0.353.dist-info → code_puppy-0.0.355.dist-info}/WHEEL +0 -0
  48. {code_puppy-0.0.353.dist-info → code_puppy-0.0.355.dist-info}/entry_points.txt +0 -0
  49. {code_puppy-0.0.353.dist-info → code_puppy-0.0.355.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,198 @@
1
+ """Commands API endpoints for slash command execution and autocomplete.
2
+
3
+ This router provides REST endpoints for:
4
+ - Listing all available slash commands
5
+ - Getting info about specific commands
6
+ - Executing slash commands
7
+ - Autocomplete suggestions for partial commands
8
+ """
9
+
10
+ from typing import Any, List, Optional
11
+
12
+ from fastapi import APIRouter, HTTPException
13
+ from pydantic import BaseModel
14
+
15
+ router = APIRouter()
16
+
17
+
18
+ # =============================================================================
19
+ # Pydantic Models
20
+ # =============================================================================
21
+
22
+
23
+ class CommandInfo(BaseModel):
24
+ """Information about a registered command."""
25
+
26
+ name: str
27
+ description: str
28
+ usage: str
29
+ aliases: List[str] = []
30
+ category: str = "core"
31
+ detailed_help: Optional[str] = None
32
+
33
+
34
+ class CommandExecuteRequest(BaseModel):
35
+ """Request to execute a slash command."""
36
+
37
+ command: str # Full command string, e.g., "/set model=gpt-4o"
38
+
39
+
40
+ class CommandExecuteResponse(BaseModel):
41
+ """Response from executing a slash command."""
42
+
43
+ success: bool
44
+ result: Any = None
45
+ error: Optional[str] = None
46
+
47
+
48
+ class AutocompleteRequest(BaseModel):
49
+ """Request for command autocomplete."""
50
+
51
+ partial: str # Partial command string, e.g., "/se" or "/set mo"
52
+
53
+
54
+ class AutocompleteResponse(BaseModel):
55
+ """Response with autocomplete suggestions."""
56
+
57
+ suggestions: List[str]
58
+
59
+
60
+ # =============================================================================
61
+ # Endpoints
62
+ # =============================================================================
63
+
64
+
65
+ @router.get("/")
66
+ async def list_commands() -> List[CommandInfo]:
67
+ """List all available slash commands.
68
+
69
+ Returns a sorted list of all unique commands (no alias duplicates),
70
+ with their metadata including name, description, usage, aliases,
71
+ category, and detailed help.
72
+
73
+ Returns:
74
+ List[CommandInfo]: Sorted list of command information.
75
+ """
76
+ from code_puppy.command_line.command_registry import get_unique_commands
77
+
78
+ commands = []
79
+ for cmd in get_unique_commands():
80
+ commands.append(
81
+ CommandInfo(
82
+ name=cmd.name,
83
+ description=cmd.description,
84
+ usage=cmd.usage,
85
+ aliases=cmd.aliases,
86
+ category=cmd.category,
87
+ detailed_help=cmd.detailed_help,
88
+ )
89
+ )
90
+ return sorted(commands, key=lambda c: c.name)
91
+
92
+
93
+ @router.get("/{name}")
94
+ async def get_command_info(name: str) -> CommandInfo:
95
+ """Get detailed info about a specific command.
96
+
97
+ Looks up a command by name or alias (case-insensitive).
98
+
99
+ Args:
100
+ name: Command name or alias (without leading /).
101
+
102
+ Returns:
103
+ CommandInfo: Full command information.
104
+
105
+ Raises:
106
+ HTTPException: 404 if command not found.
107
+ """
108
+ from code_puppy.command_line.command_registry import get_command
109
+
110
+ cmd = get_command(name)
111
+ if not cmd:
112
+ raise HTTPException(404, f"Command '/{name}' not found")
113
+
114
+ return CommandInfo(
115
+ name=cmd.name,
116
+ description=cmd.description,
117
+ usage=cmd.usage,
118
+ aliases=cmd.aliases,
119
+ category=cmd.category,
120
+ detailed_help=cmd.detailed_help,
121
+ )
122
+
123
+
124
+ @router.post("/execute")
125
+ async def execute_command(request: CommandExecuteRequest) -> CommandExecuteResponse:
126
+ """Execute a slash command.
127
+
128
+ Takes a command string (with or without leading /) and executes it
129
+ using the command handler.
130
+
131
+ Args:
132
+ request: CommandExecuteRequest with the command to execute.
133
+
134
+ Returns:
135
+ CommandExecuteResponse: Result of command execution.
136
+ """
137
+ from code_puppy.command_line.command_handler import handle_command
138
+
139
+ command = request.command
140
+ if not command.startswith("/"):
141
+ command = "/" + command
142
+
143
+ try:
144
+ result = handle_command(command)
145
+ return CommandExecuteResponse(success=True, result=result)
146
+ except Exception as e:
147
+ return CommandExecuteResponse(success=False, error=str(e))
148
+
149
+
150
+ @router.post("/autocomplete")
151
+ async def autocomplete_command(request: AutocompleteRequest) -> AutocompleteResponse:
152
+ """Get autocomplete suggestions for a partial command.
153
+
154
+ Provides intelligent autocomplete based on partial input:
155
+ - Empty input: returns all command names
156
+ - Partial command name: returns matching commands and aliases
157
+ - Complete command with args: returns usage hint
158
+
159
+ Args:
160
+ request: AutocompleteRequest with partial command string.
161
+
162
+ Returns:
163
+ AutocompleteResponse: List of autocomplete suggestions.
164
+ """
165
+ from code_puppy.command_line.command_registry import (
166
+ get_command,
167
+ get_unique_commands,
168
+ )
169
+
170
+ partial = request.partial.lstrip("/")
171
+
172
+ # If empty, return all command names
173
+ if not partial:
174
+ suggestions = [f"/{cmd.name}" for cmd in get_unique_commands()]
175
+ return AutocompleteResponse(suggestions=sorted(suggestions))
176
+
177
+ # Split into command name and args
178
+ parts = partial.split(maxsplit=1)
179
+ cmd_partial = parts[0].lower()
180
+
181
+ # If just the command name (no space yet), suggest matching commands
182
+ if len(parts) == 1:
183
+ suggestions = []
184
+ for cmd in get_unique_commands():
185
+ if cmd.name.startswith(cmd_partial):
186
+ suggestions.append(f"/{cmd.name}")
187
+ for alias in cmd.aliases:
188
+ if alias.startswith(cmd_partial):
189
+ suggestions.append(f"/{alias}")
190
+ return AutocompleteResponse(suggestions=sorted(set(suggestions)))
191
+
192
+ # Command name complete, suggest based on command type
193
+ # (For now, just return the command usage as a hint)
194
+ cmd = get_command(cmd_partial)
195
+ if cmd:
196
+ return AutocompleteResponse(suggestions=[cmd.usage])
197
+
198
+ return AutocompleteResponse(suggestions=[])
@@ -0,0 +1,74 @@
1
+ """Configuration management API endpoints."""
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+ from pydantic import BaseModel
5
+ from typing import Any, Dict, List
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ class ConfigValue(BaseModel):
11
+ key: str
12
+ value: Any
13
+
14
+
15
+ class ConfigUpdate(BaseModel):
16
+ value: Any
17
+
18
+
19
+ @router.get("/")
20
+ async def list_config() -> Dict[str, Any]:
21
+ """List all configuration keys and their current values."""
22
+ from code_puppy.config import get_config_keys, get_value
23
+
24
+ config = {}
25
+ for key in get_config_keys():
26
+ config[key] = get_value(key)
27
+ return {"config": config}
28
+
29
+
30
+ @router.get("/keys")
31
+ async def get_config_keys_list() -> List[str]:
32
+ """Get list of all valid configuration keys."""
33
+ from code_puppy.config import get_config_keys
34
+
35
+ return get_config_keys()
36
+
37
+
38
+ @router.get("/{key}")
39
+ async def get_config_value(key: str) -> ConfigValue:
40
+ """Get a specific configuration value."""
41
+ from code_puppy.config import get_config_keys, get_value
42
+
43
+ valid_keys = get_config_keys()
44
+ if key not in valid_keys:
45
+ raise HTTPException(
46
+ 404, f"Config key '{key}' not found. Valid keys: {valid_keys}"
47
+ )
48
+
49
+ value = get_value(key)
50
+ return ConfigValue(key=key, value=value)
51
+
52
+
53
+ @router.put("/{key}")
54
+ async def set_config_value(key: str, update: ConfigUpdate) -> ConfigValue:
55
+ """Set a configuration value."""
56
+ from code_puppy.config import get_config_keys, get_value, set_value
57
+
58
+ valid_keys = get_config_keys()
59
+ if key not in valid_keys:
60
+ raise HTTPException(
61
+ 404, f"Config key '{key}' not found. Valid keys: {valid_keys}"
62
+ )
63
+
64
+ set_value(key, str(update.value))
65
+ return ConfigValue(key=key, value=get_value(key))
66
+
67
+
68
+ @router.delete("/{key}")
69
+ async def reset_config_value(key: str) -> Dict[str, str]:
70
+ """Reset a configuration value to default (remove from config file)."""
71
+ from code_puppy.config import reset_value
72
+
73
+ reset_value(key)
74
+ return {"message": f"Config key '{key}' reset to default"}
@@ -0,0 +1,191 @@
1
+ """Sessions API endpoints for retrieving subagent session data."""
2
+
3
+ import json
4
+ import pickle
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from fastapi import APIRouter, HTTPException
9
+ from pydantic import BaseModel
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ class SessionInfo(BaseModel):
15
+ """Session metadata information."""
16
+
17
+ session_id: str
18
+ agent_name: Optional[str] = None
19
+ initial_prompt: Optional[str] = None
20
+ created_at: Optional[str] = None
21
+ last_updated: Optional[str] = None
22
+ message_count: int = 0
23
+
24
+
25
+ class MessageContent(BaseModel):
26
+ """Message content with role and optional timestamp."""
27
+
28
+ role: str
29
+ content: Any
30
+ timestamp: Optional[str] = None
31
+
32
+
33
+ class SessionDetail(SessionInfo):
34
+ """Session info with full message history."""
35
+
36
+ messages: List[Dict[str, Any]] = []
37
+
38
+
39
+ def _get_sessions_dir() -> Path:
40
+ """Get the subagent sessions directory.
41
+
42
+ Returns:
43
+ Path to the subagent sessions directory
44
+ """
45
+ from code_puppy.config import DATA_DIR
46
+
47
+ return Path(DATA_DIR) / "subagent_sessions"
48
+
49
+
50
+ def _serialize_message(msg: Any) -> Dict[str, Any]:
51
+ """Serialize a pydantic-ai message to a JSON-safe dict.
52
+
53
+ Handles various pydantic-ai message types that may be stored
54
+ in the pickle files.
55
+
56
+ Args:
57
+ msg: A pydantic-ai message object
58
+
59
+ Returns:
60
+ JSON-serializable dictionary representation of the message
61
+ """
62
+ # Handle pydantic v2 models with model_dump
63
+ if hasattr(msg, "model_dump"):
64
+ return msg.model_dump(mode="json")
65
+ # Handle objects with __dict__ (convert values to strings for safety)
66
+ elif hasattr(msg, "__dict__"):
67
+ return {k: str(v) for k, v in msg.__dict__.items()}
68
+ # Fallback: wrap in a content dict
69
+ else:
70
+ return {"content": str(msg)}
71
+
72
+
73
+ @router.get("/")
74
+ async def list_sessions() -> List[SessionInfo]:
75
+ """List all available sessions.
76
+
77
+ Returns:
78
+ List of SessionInfo objects for each session found
79
+ """
80
+ sessions_dir = _get_sessions_dir()
81
+ if not sessions_dir.exists():
82
+ return []
83
+
84
+ sessions = []
85
+ for txt_file in sessions_dir.glob("*.txt"):
86
+ session_id = txt_file.stem
87
+ try:
88
+ with open(txt_file, "r") as f:
89
+ metadata = json.load(f)
90
+ sessions.append(
91
+ SessionInfo(
92
+ session_id=session_id,
93
+ agent_name=metadata.get("agent_name"),
94
+ initial_prompt=metadata.get("initial_prompt"),
95
+ created_at=metadata.get("created_at"),
96
+ last_updated=metadata.get("last_updated"),
97
+ message_count=metadata.get("message_count", 0),
98
+ )
99
+ )
100
+ except Exception:
101
+ # If we can't parse metadata, still include basic session info
102
+ sessions.append(SessionInfo(session_id=session_id))
103
+
104
+ return sessions
105
+
106
+
107
+ @router.get("/{session_id}")
108
+ async def get_session(session_id: str) -> SessionInfo:
109
+ """Get session metadata.
110
+
111
+ Args:
112
+ session_id: The session identifier
113
+
114
+ Returns:
115
+ SessionInfo with metadata for the specified session
116
+
117
+ Raises:
118
+ HTTPException: 404 if session not found
119
+ """
120
+ sessions_dir = _get_sessions_dir()
121
+ txt_file = sessions_dir / f"{session_id}.txt"
122
+
123
+ if not txt_file.exists():
124
+ raise HTTPException(404, f"Session '{session_id}' not found")
125
+
126
+ with open(txt_file, "r") as f:
127
+ metadata = json.load(f)
128
+
129
+ return SessionInfo(
130
+ session_id=session_id,
131
+ agent_name=metadata.get("agent_name"),
132
+ initial_prompt=metadata.get("initial_prompt"),
133
+ created_at=metadata.get("created_at"),
134
+ last_updated=metadata.get("last_updated"),
135
+ message_count=metadata.get("message_count", 0),
136
+ )
137
+
138
+
139
+ @router.get("/{session_id}/messages")
140
+ async def get_session_messages(session_id: str) -> List[Dict[str, Any]]:
141
+ """Get the full message history for a session.
142
+
143
+ Args:
144
+ session_id: The session identifier
145
+
146
+ Returns:
147
+ List of serialized message dictionaries
148
+
149
+ Raises:
150
+ HTTPException: 404 if session messages not found, 500 on load error
151
+ """
152
+ sessions_dir = _get_sessions_dir()
153
+ pkl_file = sessions_dir / f"{session_id}.pkl"
154
+
155
+ if not pkl_file.exists():
156
+ raise HTTPException(404, f"Session '{session_id}' messages not found")
157
+
158
+ try:
159
+ with open(pkl_file, "rb") as f:
160
+ messages = pickle.load(f)
161
+ return [_serialize_message(msg) for msg in messages]
162
+ except Exception as e:
163
+ raise HTTPException(500, f"Error loading session messages: {e}")
164
+
165
+
166
+ @router.delete("/{session_id}")
167
+ async def delete_session(session_id: str) -> Dict[str, str]:
168
+ """Delete a session and its data.
169
+
170
+ Args:
171
+ session_id: The session identifier
172
+
173
+ Returns:
174
+ Success message dict
175
+
176
+ Raises:
177
+ HTTPException: 404 if session not found
178
+ """
179
+ sessions_dir = _get_sessions_dir()
180
+ txt_file = sessions_dir / f"{session_id}.txt"
181
+ pkl_file = sessions_dir / f"{session_id}.pkl"
182
+
183
+ if not txt_file.exists() and not pkl_file.exists():
184
+ raise HTTPException(404, f"Session '{session_id}' not found")
185
+
186
+ if txt_file.exists():
187
+ txt_file.unlink()
188
+ if pkl_file.exists():
189
+ pkl_file.unlink()
190
+
191
+ return {"message": f"Session '{session_id}' deleted"}