code-puppy 0.0.348__py3-none-any.whl → 0.0.361__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 (70) 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_qa_kitten.py +12 -7
  5. code_puppy/agents/agent_terminal_qa.py +323 -0
  6. code_puppy/agents/base_agent.py +17 -4
  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/claude_cache_client.py +249 -34
  29. code_puppy/command_line/core_commands.py +85 -0
  30. code_puppy/config.py +66 -62
  31. code_puppy/messaging/__init__.py +15 -0
  32. code_puppy/messaging/messages.py +27 -0
  33. code_puppy/messaging/queue_console.py +1 -1
  34. code_puppy/messaging/rich_renderer.py +36 -1
  35. code_puppy/messaging/spinner/__init__.py +20 -2
  36. code_puppy/messaging/subagent_console.py +461 -0
  37. code_puppy/model_utils.py +54 -0
  38. code_puppy/plugins/antigravity_oauth/antigravity_model.py +90 -19
  39. code_puppy/plugins/antigravity_oauth/transport.py +1 -0
  40. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  41. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  42. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  43. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  44. code_puppy/status_display.py +6 -2
  45. code_puppy/tools/__init__.py +37 -1
  46. code_puppy/tools/agent_tools.py +83 -33
  47. code_puppy/tools/browser/__init__.py +37 -0
  48. code_puppy/tools/browser/browser_control.py +6 -6
  49. code_puppy/tools/browser/browser_interactions.py +21 -20
  50. code_puppy/tools/browser/browser_locators.py +9 -9
  51. code_puppy/tools/browser/browser_navigation.py +7 -7
  52. code_puppy/tools/browser/browser_screenshot.py +78 -140
  53. code_puppy/tools/browser/browser_scripts.py +15 -13
  54. code_puppy/tools/browser/camoufox_manager.py +226 -64
  55. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  56. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  57. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  58. code_puppy/tools/browser/terminal_tools.py +525 -0
  59. code_puppy/tools/command_runner.py +292 -101
  60. code_puppy/tools/common.py +176 -1
  61. code_puppy/tools/display.py +84 -0
  62. code_puppy/tools/subagent_context.py +158 -0
  63. {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/METADATA +13 -11
  64. {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/RECORD +69 -38
  65. code_puppy/tools/browser/vqa_agent.py +0 -90
  66. {code_puppy-0.0.348.data → code_puppy-0.0.361.data}/data/code_puppy/models.json +0 -0
  67. {code_puppy-0.0.348.data → code_puppy-0.0.361.data}/data/code_puppy/models_dev_api.json +0 -0
  68. {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/WHEEL +0 -0
  69. {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/entry_points.txt +0 -0
  70. {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,217 @@
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
+ import asyncio
11
+ from concurrent.futures import ThreadPoolExecutor
12
+ from typing import Any, List, Optional
13
+
14
+ from fastapi import APIRouter, HTTPException
15
+ from pydantic import BaseModel
16
+
17
+ # Thread pool for blocking command execution
18
+ _executor = ThreadPoolExecutor(max_workers=4)
19
+
20
+ # Timeout for command execution (seconds)
21
+ COMMAND_TIMEOUT = 30.0
22
+
23
+ router = APIRouter()
24
+
25
+
26
+ # =============================================================================
27
+ # Pydantic Models
28
+ # =============================================================================
29
+
30
+
31
+ class CommandInfo(BaseModel):
32
+ """Information about a registered command."""
33
+
34
+ name: str
35
+ description: str
36
+ usage: str
37
+ aliases: List[str] = []
38
+ category: str = "core"
39
+ detailed_help: Optional[str] = None
40
+
41
+
42
+ class CommandExecuteRequest(BaseModel):
43
+ """Request to execute a slash command."""
44
+
45
+ command: str # Full command string, e.g., "/set model=gpt-4o"
46
+
47
+
48
+ class CommandExecuteResponse(BaseModel):
49
+ """Response from executing a slash command."""
50
+
51
+ success: bool
52
+ result: Any = None
53
+ error: Optional[str] = None
54
+
55
+
56
+ class AutocompleteRequest(BaseModel):
57
+ """Request for command autocomplete."""
58
+
59
+ partial: str # Partial command string, e.g., "/se" or "/set mo"
60
+
61
+
62
+ class AutocompleteResponse(BaseModel):
63
+ """Response with autocomplete suggestions."""
64
+
65
+ suggestions: List[str]
66
+
67
+
68
+ # =============================================================================
69
+ # Endpoints
70
+ # =============================================================================
71
+
72
+
73
+ @router.get("/")
74
+ async def list_commands() -> List[CommandInfo]:
75
+ """List all available slash commands.
76
+
77
+ Returns a sorted list of all unique commands (no alias duplicates),
78
+ with their metadata including name, description, usage, aliases,
79
+ category, and detailed help.
80
+
81
+ Returns:
82
+ List[CommandInfo]: Sorted list of command information.
83
+ """
84
+ from code_puppy.command_line.command_registry import get_unique_commands
85
+
86
+ commands = []
87
+ for cmd in get_unique_commands():
88
+ commands.append(
89
+ CommandInfo(
90
+ name=cmd.name,
91
+ description=cmd.description,
92
+ usage=cmd.usage,
93
+ aliases=cmd.aliases,
94
+ category=cmd.category,
95
+ detailed_help=cmd.detailed_help,
96
+ )
97
+ )
98
+ return sorted(commands, key=lambda c: c.name)
99
+
100
+
101
+ @router.get("/{name}")
102
+ async def get_command_info(name: str) -> CommandInfo:
103
+ """Get detailed info about a specific command.
104
+
105
+ Looks up a command by name or alias (case-insensitive).
106
+
107
+ Args:
108
+ name: Command name or alias (without leading /).
109
+
110
+ Returns:
111
+ CommandInfo: Full command information.
112
+
113
+ Raises:
114
+ HTTPException: 404 if command not found.
115
+ """
116
+ from code_puppy.command_line.command_registry import get_command
117
+
118
+ cmd = get_command(name)
119
+ if not cmd:
120
+ raise HTTPException(404, f"Command '/{name}' not found")
121
+
122
+ return CommandInfo(
123
+ name=cmd.name,
124
+ description=cmd.description,
125
+ usage=cmd.usage,
126
+ aliases=cmd.aliases,
127
+ category=cmd.category,
128
+ detailed_help=cmd.detailed_help,
129
+ )
130
+
131
+
132
+ @router.post("/execute")
133
+ async def execute_command(request: CommandExecuteRequest) -> CommandExecuteResponse:
134
+ """Execute a slash command.
135
+
136
+ Takes a command string (with or without leading /) and executes it
137
+ using the command handler. Runs in a thread pool to avoid blocking
138
+ the event loop, with a timeout to prevent hangs.
139
+
140
+ Args:
141
+ request: CommandExecuteRequest with the command to execute.
142
+
143
+ Returns:
144
+ CommandExecuteResponse: Result of command execution.
145
+ """
146
+ from code_puppy.command_line.command_handler import handle_command
147
+
148
+ command = request.command
149
+ if not command.startswith("/"):
150
+ command = "/" + command
151
+
152
+ loop = asyncio.get_running_loop()
153
+
154
+ try:
155
+ # Run blocking command in thread pool with timeout
156
+ result = await asyncio.wait_for(
157
+ loop.run_in_executor(_executor, handle_command, command),
158
+ timeout=COMMAND_TIMEOUT,
159
+ )
160
+ return CommandExecuteResponse(success=True, result=result)
161
+ except asyncio.TimeoutError:
162
+ return CommandExecuteResponse(
163
+ success=False, error=f"Command timed out after {COMMAND_TIMEOUT}s"
164
+ )
165
+ except Exception as e:
166
+ return CommandExecuteResponse(success=False, error=str(e))
167
+
168
+
169
+ @router.post("/autocomplete")
170
+ async def autocomplete_command(request: AutocompleteRequest) -> AutocompleteResponse:
171
+ """Get autocomplete suggestions for a partial command.
172
+
173
+ Provides intelligent autocomplete based on partial input:
174
+ - Empty input: returns all command names
175
+ - Partial command name: returns matching commands and aliases
176
+ - Complete command with args: returns usage hint
177
+
178
+ Args:
179
+ request: AutocompleteRequest with partial command string.
180
+
181
+ Returns:
182
+ AutocompleteResponse: List of autocomplete suggestions.
183
+ """
184
+ from code_puppy.command_line.command_registry import (
185
+ get_command,
186
+ get_unique_commands,
187
+ )
188
+
189
+ partial = request.partial.lstrip("/")
190
+
191
+ # If empty, return all command names
192
+ if not partial:
193
+ suggestions = [f"/{cmd.name}" for cmd in get_unique_commands()]
194
+ return AutocompleteResponse(suggestions=sorted(suggestions))
195
+
196
+ # Split into command name and args
197
+ parts = partial.split(maxsplit=1)
198
+ cmd_partial = parts[0].lower()
199
+
200
+ # If just the command name (no space yet), suggest matching commands
201
+ if len(parts) == 1:
202
+ suggestions = []
203
+ for cmd in get_unique_commands():
204
+ if cmd.name.startswith(cmd_partial):
205
+ suggestions.append(f"/{cmd.name}")
206
+ for alias in cmd.aliases:
207
+ if alias.startswith(cmd_partial):
208
+ suggestions.append(f"/{alias}")
209
+ return AutocompleteResponse(suggestions=sorted(set(suggestions)))
210
+
211
+ # Command name complete, suggest based on command type
212
+ # (For now, just return the command usage as a hint)
213
+ cmd = get_command(cmd_partial)
214
+ if cmd:
215
+ return AutocompleteResponse(suggestions=[cmd.usage])
216
+
217
+ 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,232 @@
1
+ """Sessions API endpoints for retrieving subagent session data."""
2
+
3
+ import asyncio
4
+ import json
5
+ import pickle
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from fastapi import APIRouter, HTTPException
11
+ from pydantic import BaseModel
12
+
13
+ # Thread pool for blocking file I/O
14
+ _executor = ThreadPoolExecutor(max_workers=2)
15
+
16
+ # Timeout for file operations (seconds)
17
+ FILE_IO_TIMEOUT = 10.0
18
+
19
+ router = APIRouter()
20
+
21
+
22
+ class SessionInfo(BaseModel):
23
+ """Session metadata information."""
24
+
25
+ session_id: str
26
+ agent_name: Optional[str] = None
27
+ initial_prompt: Optional[str] = None
28
+ created_at: Optional[str] = None
29
+ last_updated: Optional[str] = None
30
+ message_count: int = 0
31
+
32
+
33
+ class MessageContent(BaseModel):
34
+ """Message content with role and optional timestamp."""
35
+
36
+ role: str
37
+ content: Any
38
+ timestamp: Optional[str] = None
39
+
40
+
41
+ class SessionDetail(SessionInfo):
42
+ """Session info with full message history."""
43
+
44
+ messages: List[Dict[str, Any]] = []
45
+
46
+
47
+ def _get_sessions_dir() -> Path:
48
+ """Get the subagent sessions directory.
49
+
50
+ Returns:
51
+ Path to the subagent sessions directory
52
+ """
53
+ from code_puppy.config import DATA_DIR
54
+
55
+ return Path(DATA_DIR) / "subagent_sessions"
56
+
57
+
58
+ def _serialize_message(msg: Any) -> Dict[str, Any]:
59
+ """Serialize a pydantic-ai message to a JSON-safe dict.
60
+
61
+ Handles various pydantic-ai message types that may be stored
62
+ in the pickle files.
63
+
64
+ Args:
65
+ msg: A pydantic-ai message object
66
+
67
+ Returns:
68
+ JSON-serializable dictionary representation of the message
69
+ """
70
+ # Handle pydantic v2 models with model_dump
71
+ if hasattr(msg, "model_dump"):
72
+ return msg.model_dump(mode="json")
73
+ # Handle objects with __dict__ (convert values to strings for safety)
74
+ elif hasattr(msg, "__dict__"):
75
+ return {k: str(v) for k, v in msg.__dict__.items()}
76
+ # Fallback: wrap in a content dict
77
+ else:
78
+ return {"content": str(msg)}
79
+
80
+
81
+ def _load_json_sync(file_path: Path) -> dict:
82
+ """Synchronous JSON file load (for use in executor)."""
83
+ with open(file_path, "r") as f:
84
+ return json.load(f)
85
+
86
+
87
+ def _load_pickle_sync(file_path: Path) -> Any:
88
+ """Synchronous pickle file load (for use in executor)."""
89
+ with open(file_path, "rb") as f:
90
+ return pickle.load(f)
91
+
92
+
93
+ @router.get("/")
94
+ async def list_sessions() -> List[SessionInfo]:
95
+ """List all available sessions.
96
+
97
+ Returns:
98
+ List of SessionInfo objects for each session found
99
+ """
100
+ sessions_dir = _get_sessions_dir()
101
+ if not sessions_dir.exists():
102
+ return []
103
+
104
+ loop = asyncio.get_running_loop()
105
+ sessions = []
106
+
107
+ for txt_file in sessions_dir.glob("*.txt"):
108
+ session_id = txt_file.stem
109
+ try:
110
+ # Run blocking I/O in thread pool with timeout
111
+ metadata = await asyncio.wait_for(
112
+ loop.run_in_executor(_executor, _load_json_sync, txt_file),
113
+ timeout=FILE_IO_TIMEOUT,
114
+ )
115
+ sessions.append(
116
+ SessionInfo(
117
+ session_id=session_id,
118
+ agent_name=metadata.get("agent_name"),
119
+ initial_prompt=metadata.get("initial_prompt"),
120
+ created_at=metadata.get("created_at"),
121
+ last_updated=metadata.get("last_updated"),
122
+ message_count=metadata.get("message_count", 0),
123
+ )
124
+ )
125
+ except asyncio.TimeoutError:
126
+ # Timed out reading file, include basic info
127
+ sessions.append(SessionInfo(session_id=session_id))
128
+ except Exception:
129
+ # If we can't parse metadata, still include basic session info
130
+ sessions.append(SessionInfo(session_id=session_id))
131
+
132
+ return sessions
133
+
134
+
135
+ @router.get("/{session_id}")
136
+ async def get_session(session_id: str) -> SessionInfo:
137
+ """Get session metadata.
138
+
139
+ Args:
140
+ session_id: The session identifier
141
+
142
+ Returns:
143
+ SessionInfo with metadata for the specified session
144
+
145
+ Raises:
146
+ HTTPException: 404 if session not found, 504 on timeout
147
+ """
148
+ sessions_dir = _get_sessions_dir()
149
+ txt_file = sessions_dir / f"{session_id}.txt"
150
+
151
+ if not txt_file.exists():
152
+ raise HTTPException(404, f"Session '{session_id}' not found")
153
+
154
+ loop = asyncio.get_running_loop()
155
+
156
+ try:
157
+ metadata = await asyncio.wait_for(
158
+ loop.run_in_executor(_executor, _load_json_sync, txt_file),
159
+ timeout=FILE_IO_TIMEOUT,
160
+ )
161
+ except asyncio.TimeoutError:
162
+ raise HTTPException(504, f"Timeout reading session '{session_id}'")
163
+
164
+ return SessionInfo(
165
+ session_id=session_id,
166
+ agent_name=metadata.get("agent_name"),
167
+ initial_prompt=metadata.get("initial_prompt"),
168
+ created_at=metadata.get("created_at"),
169
+ last_updated=metadata.get("last_updated"),
170
+ message_count=metadata.get("message_count", 0),
171
+ )
172
+
173
+
174
+ @router.get("/{session_id}/messages")
175
+ async def get_session_messages(session_id: str) -> List[Dict[str, Any]]:
176
+ """Get the full message history for a session.
177
+
178
+ Args:
179
+ session_id: The session identifier
180
+
181
+ Returns:
182
+ List of serialized message dictionaries
183
+
184
+ Raises:
185
+ HTTPException: 404 if session messages not found, 500 on load error, 504 on timeout
186
+ """
187
+ sessions_dir = _get_sessions_dir()
188
+ pkl_file = sessions_dir / f"{session_id}.pkl"
189
+
190
+ if not pkl_file.exists():
191
+ raise HTTPException(404, f"Session '{session_id}' messages not found")
192
+
193
+ loop = asyncio.get_running_loop()
194
+
195
+ try:
196
+ messages = await asyncio.wait_for(
197
+ loop.run_in_executor(_executor, _load_pickle_sync, pkl_file),
198
+ timeout=FILE_IO_TIMEOUT,
199
+ )
200
+ return [_serialize_message(msg) for msg in messages]
201
+ except asyncio.TimeoutError:
202
+ raise HTTPException(504, f"Timeout loading session '{session_id}' messages")
203
+ except Exception as e:
204
+ raise HTTPException(500, f"Error loading session messages: {e}")
205
+
206
+
207
+ @router.delete("/{session_id}")
208
+ async def delete_session(session_id: str) -> Dict[str, str]:
209
+ """Delete a session and its data.
210
+
211
+ Args:
212
+ session_id: The session identifier
213
+
214
+ Returns:
215
+ Success message dict
216
+
217
+ Raises:
218
+ HTTPException: 404 if session not found
219
+ """
220
+ sessions_dir = _get_sessions_dir()
221
+ txt_file = sessions_dir / f"{session_id}.txt"
222
+ pkl_file = sessions_dir / f"{session_id}.pkl"
223
+
224
+ if not txt_file.exists() and not pkl_file.exists():
225
+ raise HTTPException(404, f"Session '{session_id}' not found")
226
+
227
+ if txt_file.exists():
228
+ txt_file.unlink()
229
+ if pkl_file.exists():
230
+ pkl_file.unlink()
231
+
232
+ return {"message": f"Session '{session_id}' deleted"}