code-puppy 0.0.214__py3-none-any.whl → 0.0.366__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.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
|
@@ -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"}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>🐶 Code Puppy Terminal</title>
|
|
7
|
+
|
|
8
|
+
<!-- Tailwind CSS -->
|
|
9
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
10
|
+
|
|
11
|
+
<!-- xterm.js CSS -->
|
|
12
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
|
13
|
+
|
|
14
|
+
<!-- xterm.js -->
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
|
17
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
/* Custom terminal styling */
|
|
21
|
+
.xterm {
|
|
22
|
+
padding: 8px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.xterm-viewport::-webkit-scrollbar {
|
|
26
|
+
width: 8px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.xterm-viewport::-webkit-scrollbar-track {
|
|
30
|
+
background: #1e1e1e;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.xterm-viewport::-webkit-scrollbar-thumb {
|
|
34
|
+
background: #4a4a4a;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.xterm-viewport::-webkit-scrollbar-thumb:hover {
|
|
39
|
+
background: #5a5a5a;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.status-indicator {
|
|
43
|
+
width: 10px;
|
|
44
|
+
height: 10px;
|
|
45
|
+
border-radius: 50%;
|
|
46
|
+
animation: pulse 2s infinite;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.status-connected {
|
|
50
|
+
background-color: #22c55e;
|
|
51
|
+
box-shadow: 0 0 8px #22c55e;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.status-disconnected {
|
|
55
|
+
background-color: #ef4444;
|
|
56
|
+
box-shadow: 0 0 8px #ef4444;
|
|
57
|
+
animation: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.status-connecting {
|
|
61
|
+
background-color: #f59e0b;
|
|
62
|
+
box-shadow: 0 0 8px #f59e0b;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@keyframes pulse {
|
|
66
|
+
0%, 100% { opacity: 1; }
|
|
67
|
+
50% { opacity: 0.5; }
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
70
|
+
</head>
|
|
71
|
+
<body class="h-full bg-gray-900 text-white overflow-hidden">
|
|
72
|
+
<div class="h-full flex flex-col">
|
|
73
|
+
<!-- Header -->
|
|
74
|
+
<header class="bg-gray-800 border-b border-gray-700 px-4 py-2 flex items-center justify-between">
|
|
75
|
+
<div class="flex items-center space-x-3">
|
|
76
|
+
<span class="text-2xl">🐶</span>
|
|
77
|
+
<h1 class="text-lg font-semibold text-gray-100">Code Puppy Terminal</h1>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="flex items-center space-x-4">
|
|
81
|
+
<!-- Connection Status -->
|
|
82
|
+
<div class="flex items-center space-x-2">
|
|
83
|
+
<div id="status-indicator" class="status-indicator status-disconnected"></div>
|
|
84
|
+
<span id="status-text" class="text-sm text-gray-400">Disconnected</span>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Session ID -->
|
|
88
|
+
<div class="text-sm text-gray-500">
|
|
89
|
+
Session: <span id="session-id" class="font-mono text-gray-400">-</span>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Controls -->
|
|
93
|
+
<div class="flex items-center space-x-2">
|
|
94
|
+
<button id="btn-reconnect"
|
|
95
|
+
class="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
96
|
+
disabled>
|
|
97
|
+
Reconnect
|
|
98
|
+
</button>
|
|
99
|
+
<button id="btn-clear"
|
|
100
|
+
class="px-3 py-1 text-sm bg-gray-600 hover:bg-gray-700 rounded transition-colors">
|
|
101
|
+
Clear
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</header>
|
|
106
|
+
|
|
107
|
+
<!-- Terminal Container -->
|
|
108
|
+
<main class="flex-1 bg-black p-2">
|
|
109
|
+
<div id="terminal" class="h-full w-full"></div>
|
|
110
|
+
</main>
|
|
111
|
+
|
|
112
|
+
<!-- Footer -->
|
|
113
|
+
<footer class="bg-gray-800 border-t border-gray-700 px-4 py-1 flex items-center justify-between text-xs text-gray-500">
|
|
114
|
+
<div>
|
|
115
|
+
<span id="terminal-size">80x24</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="flex items-center space-x-4">
|
|
118
|
+
<span>Ctrl+C to interrupt</span>
|
|
119
|
+
<span>•</span>
|
|
120
|
+
<span>Ctrl+D to exit</span>
|
|
121
|
+
</div>
|
|
122
|
+
</footer>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<script>
|
|
126
|
+
// Terminal configuration
|
|
127
|
+
const CONFIG = {
|
|
128
|
+
wsUrl: `ws://${window.location.host}/ws/terminal`,
|
|
129
|
+
sessionId: new URLSearchParams(window.location.search).get('session') || 'default',
|
|
130
|
+
theme: {
|
|
131
|
+
background: '#1e1e1e',
|
|
132
|
+
foreground: '#d4d4d4',
|
|
133
|
+
cursor: '#aeafad',
|
|
134
|
+
cursorAccent: '#1e1e1e',
|
|
135
|
+
selection: 'rgba(255, 255, 255, 0.3)',
|
|
136
|
+
black: '#000000',
|
|
137
|
+
red: '#cd3131',
|
|
138
|
+
green: '#0dbc79',
|
|
139
|
+
yellow: '#e5e510',
|
|
140
|
+
blue: '#2472c8',
|
|
141
|
+
magenta: '#bc3fbc',
|
|
142
|
+
cyan: '#11a8cd',
|
|
143
|
+
white: '#e5e5e5',
|
|
144
|
+
brightBlack: '#666666',
|
|
145
|
+
brightRed: '#f14c4c',
|
|
146
|
+
brightGreen: '#23d18b',
|
|
147
|
+
brightYellow: '#f5f543',
|
|
148
|
+
brightBlue: '#3b8eea',
|
|
149
|
+
brightMagenta: '#d670d6',
|
|
150
|
+
brightCyan: '#29b8db',
|
|
151
|
+
brightWhite: '#ffffff'
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// State
|
|
156
|
+
let terminal = null;
|
|
157
|
+
let fitAddon = null;
|
|
158
|
+
let socket = null;
|
|
159
|
+
let reconnectAttempts = 0;
|
|
160
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
161
|
+
|
|
162
|
+
// DOM Elements
|
|
163
|
+
const statusIndicator = document.getElementById('status-indicator');
|
|
164
|
+
const statusText = document.getElementById('status-text');
|
|
165
|
+
const sessionIdEl = document.getElementById('session-id');
|
|
166
|
+
const terminalSizeEl = document.getElementById('terminal-size');
|
|
167
|
+
const btnReconnect = document.getElementById('btn-reconnect');
|
|
168
|
+
const btnClear = document.getElementById('btn-clear');
|
|
169
|
+
|
|
170
|
+
// Initialize terminal
|
|
171
|
+
function initTerminal() {
|
|
172
|
+
terminal = new Terminal({
|
|
173
|
+
theme: CONFIG.theme,
|
|
174
|
+
fontFamily: '"Cascadia Code", "Fira Code", "JetBrains Mono", Menlo, Monaco, monospace',
|
|
175
|
+
fontSize: 14,
|
|
176
|
+
lineHeight: 1.2,
|
|
177
|
+
cursorBlink: true,
|
|
178
|
+
cursorStyle: 'block',
|
|
179
|
+
scrollback: 10000,
|
|
180
|
+
convertEol: true,
|
|
181
|
+
allowTransparency: true
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Load addons
|
|
185
|
+
fitAddon = new FitAddon.FitAddon();
|
|
186
|
+
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
|
|
187
|
+
|
|
188
|
+
terminal.loadAddon(fitAddon);
|
|
189
|
+
terminal.loadAddon(webLinksAddon);
|
|
190
|
+
|
|
191
|
+
// Open terminal in container
|
|
192
|
+
const container = document.getElementById('terminal');
|
|
193
|
+
terminal.open(container);
|
|
194
|
+
|
|
195
|
+
// Fit to container
|
|
196
|
+
fitAddon.fit();
|
|
197
|
+
updateTerminalSize();
|
|
198
|
+
|
|
199
|
+
// Handle user input
|
|
200
|
+
terminal.onData(data => {
|
|
201
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
202
|
+
socket.send(JSON.stringify({
|
|
203
|
+
type: 'input',
|
|
204
|
+
data: data
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Handle resize
|
|
210
|
+
terminal.onResize(({ cols, rows }) => {
|
|
211
|
+
updateTerminalSize();
|
|
212
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
213
|
+
socket.send(JSON.stringify({
|
|
214
|
+
type: 'resize',
|
|
215
|
+
cols: cols,
|
|
216
|
+
rows: rows
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Welcome message
|
|
222
|
+
terminal.writeln('\x1b[1;36m🐶 Welcome to Code Puppy Terminal!\x1b[0m');
|
|
223
|
+
terminal.writeln('\x1b[90mConnecting to server...\x1b[0m');
|
|
224
|
+
terminal.writeln('');
|
|
225
|
+
|
|
226
|
+
// Connect to WebSocket
|
|
227
|
+
connect();
|
|
228
|
+
|
|
229
|
+
// Update session ID display
|
|
230
|
+
sessionIdEl.textContent = CONFIG.sessionId;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Update terminal size display
|
|
234
|
+
function updateTerminalSize() {
|
|
235
|
+
if (terminal) {
|
|
236
|
+
terminalSizeEl.textContent = `${terminal.cols}x${terminal.rows}`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Update connection status
|
|
241
|
+
function setStatus(status) {
|
|
242
|
+
statusIndicator.className = 'status-indicator';
|
|
243
|
+
|
|
244
|
+
switch (status) {
|
|
245
|
+
case 'connected':
|
|
246
|
+
statusIndicator.classList.add('status-connected');
|
|
247
|
+
statusText.textContent = 'Connected';
|
|
248
|
+
btnReconnect.disabled = true;
|
|
249
|
+
break;
|
|
250
|
+
case 'disconnected':
|
|
251
|
+
statusIndicator.classList.add('status-disconnected');
|
|
252
|
+
statusText.textContent = 'Disconnected';
|
|
253
|
+
btnReconnect.disabled = false;
|
|
254
|
+
break;
|
|
255
|
+
case 'connecting':
|
|
256
|
+
statusIndicator.classList.add('status-connecting');
|
|
257
|
+
statusText.textContent = 'Connecting...';
|
|
258
|
+
btnReconnect.disabled = true;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Connect to WebSocket
|
|
264
|
+
function connect() {
|
|
265
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
setStatus('connecting');
|
|
270
|
+
|
|
271
|
+
const url = CONFIG.wsUrl;
|
|
272
|
+
socket = new WebSocket(url);
|
|
273
|
+
|
|
274
|
+
socket.onopen = () => {
|
|
275
|
+
setStatus('connected');
|
|
276
|
+
reconnectAttempts = 0;
|
|
277
|
+
terminal.writeln('\x1b[1;32m✓ Connected to server\x1b[0m');
|
|
278
|
+
terminal.writeln('');
|
|
279
|
+
|
|
280
|
+
// Send initial resize
|
|
281
|
+
socket.send(JSON.stringify({
|
|
282
|
+
type: 'resize',
|
|
283
|
+
cols: terminal.cols,
|
|
284
|
+
rows: terminal.rows
|
|
285
|
+
}));
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
socket.onmessage = (event) => {
|
|
289
|
+
try {
|
|
290
|
+
const message = JSON.parse(event.data);
|
|
291
|
+
|
|
292
|
+
switch (message.type) {
|
|
293
|
+
case 'output':
|
|
294
|
+
terminal.write(new TextDecoder().decode(Uint8Array.from(atob(message.data), c => c.charCodeAt(0))));
|
|
295
|
+
break;
|
|
296
|
+
case 'error':
|
|
297
|
+
terminal.writeln(`\x1b[1;31mError: ${message.message}\x1b[0m`);
|
|
298
|
+
break;
|
|
299
|
+
case 'session_started':
|
|
300
|
+
terminal.writeln(`\x1b[90mSession started: ${message.session_id}\x1b[0m`);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
} catch (e) {
|
|
304
|
+
// Plain text output
|
|
305
|
+
terminal.write(event.data);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
socket.onclose = (event) => {
|
|
310
|
+
setStatus('disconnected');
|
|
311
|
+
|
|
312
|
+
if (!event.wasClean) {
|
|
313
|
+
terminal.writeln('');
|
|
314
|
+
terminal.writeln('\x1b[1;31m✗ Connection lost\x1b[0m');
|
|
315
|
+
|
|
316
|
+
// Auto-reconnect with backoff
|
|
317
|
+
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
318
|
+
reconnectAttempts++;
|
|
319
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
320
|
+
terminal.writeln(`\x1b[90mReconnecting in ${delay/1000}s (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...\x1b[0m`);
|
|
321
|
+
setTimeout(connect, delay);
|
|
322
|
+
} else {
|
|
323
|
+
terminal.writeln('\x1b[90mMax reconnection attempts reached. Click "Reconnect" to try again.\x1b[0m');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
socket.onerror = (error) => {
|
|
329
|
+
console.error('WebSocket error:', error);
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Event listeners
|
|
334
|
+
btnReconnect.addEventListener('click', () => {
|
|
335
|
+
reconnectAttempts = 0;
|
|
336
|
+
connect();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
btnClear.addEventListener('click', () => {
|
|
340
|
+
terminal.clear();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Handle window resize
|
|
344
|
+
window.addEventListener('resize', () => {
|
|
345
|
+
if (fitAddon) {
|
|
346
|
+
fitAddon.fit();
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Handle page unload
|
|
351
|
+
window.addEventListener('beforeunload', () => {
|
|
352
|
+
if (socket) {
|
|
353
|
+
socket.close();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Initialize on DOM ready
|
|
358
|
+
document.addEventListener('DOMContentLoaded', initTerminal);
|
|
359
|
+
</script>
|
|
360
|
+
</body>
|
|
361
|
+
</html>
|