codepp 0.0.437__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 (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,96 @@
1
+ """
2
+ 🐶 MOTD (Message of the Day) feature for code-puppy! 🐕
3
+ Stores seen versions in XDG_CONFIG_HOME/code_puppy/motd.txt - woof woof! 🐾
4
+ """
5
+
6
+ import os
7
+
8
+ from code_puppy.config import CONFIG_DIR
9
+ from code_puppy.messaging import emit_info
10
+
11
+ MOTD_VERSION = "2026-01-01"
12
+ MOTD_MESSAGE = """
13
+ # 🐶 Happy New Year! January 1st, 2026 🎉
14
+ Reminder that Code Puppy supports three different OAuth subscriptions:
15
+
16
+ ### Claude Code - `/claude-code-auth`
17
+ - Opus / Haiku / Sonnet
18
+
19
+ ### ChatGPT Pro/Plus - `/chatgpt-auth`
20
+ - gpt-5.2 and gpt-5.2 codex
21
+
22
+ ### Google Antigravity - `/antigravity-auth`
23
+ - Gemini 3 Pro, Flash, and Anthropic models including Opus and Sonnet.
24
+ """
25
+ MOTD_TRACK_FILE = os.path.join(CONFIG_DIR, "motd.txt")
26
+
27
+
28
+ def get_motd_content() -> tuple[str, str]:
29
+ """Get MOTD content, checking plugins first.
30
+
31
+ Returns:
32
+ Tuple of (message, version) - either from plugin or built-in.
33
+ """
34
+ # Check if plugins want to override MOTD
35
+ try:
36
+ from code_puppy.callbacks import on_get_motd
37
+
38
+ results = on_get_motd()
39
+ # Use the last non-None result
40
+ for result in reversed(results):
41
+ if result is not None and isinstance(result, tuple) and len(result) == 2:
42
+ return result
43
+ except Exception:
44
+ pass
45
+
46
+ # Fall back to built-in MOTD
47
+ return (MOTD_MESSAGE, MOTD_VERSION)
48
+
49
+
50
+ def has_seen_motd(version: str) -> bool: # 🐕 Check if puppy has seen this MOTD!
51
+ if not os.path.exists(MOTD_TRACK_FILE):
52
+ return False
53
+ with open(MOTD_TRACK_FILE, "r") as f:
54
+ seen_versions = {line.strip() for line in f if line.strip()}
55
+ return version in seen_versions
56
+
57
+
58
+ def mark_motd_seen(version: str): # 🐶 Mark MOTD as seen by this good puppy!
59
+ # Create directory if it doesn't exist 🏠🐕
60
+ os.makedirs(os.path.dirname(MOTD_TRACK_FILE), exist_ok=True)
61
+
62
+ # Check if the version is already in the file 📋🐶
63
+ seen_versions = set()
64
+ if os.path.exists(MOTD_TRACK_FILE):
65
+ with open(MOTD_TRACK_FILE, "r") as f:
66
+ seen_versions = {line.strip() for line in f if line.strip()}
67
+
68
+ # Only add the version if it's not already there 📝🐕‍🦺
69
+ if version not in seen_versions:
70
+ with open(MOTD_TRACK_FILE, "a") as f:
71
+ f.write(f"{version}\n")
72
+
73
+
74
+ def print_motd(
75
+ console=None, force: bool = False
76
+ ) -> bool: # 🐶 Print exciting puppy MOTD!
77
+ """
78
+ 🐕 Print the message of the day to the user - woof woof! 🐕
79
+
80
+ Args:
81
+ console: Optional console object (for backward compatibility) 🖥️🐶
82
+ force: Whether to force printing even if the MOTD has been seen 💪🐕‍🦺
83
+
84
+ Returns:
85
+ True if the MOTD was printed, False otherwise 🐾
86
+ """
87
+ message, version = get_motd_content()
88
+ if force or not has_seen_motd(version):
89
+ # Create a Rich Markdown object for proper rendering 🎨🐶
90
+ from rich.markdown import Markdown
91
+
92
+ markdown_content = Markdown(message)
93
+ emit_info(markdown_content)
94
+ mark_motd_seen(version)
95
+ return True
96
+ return False
@@ -0,0 +1,179 @@
1
+ """Slide content for the onboarding wizard.
2
+
3
+ 🐶 Lean, mean, ADHD-friendly slides. 5 slides max!
4
+ """
5
+
6
+ from typing import List, Tuple
7
+
8
+ # ============================================================================
9
+ # Slide Data Constants
10
+ # ============================================================================
11
+
12
+ # Model subscription options
13
+ MODEL_OPTIONS: List[Tuple[str, str, str]] = [
14
+ ("chatgpt", "ChatGPT Plus/Pro/Max", "OAuth login - no API key needed"),
15
+ ("claude", "Claude Code Pro/Max", "OAuth login - no API key needed"),
16
+ ("api_keys", "API Keys", "OpenAI, Anthropic, Google, etc."),
17
+ ("openrouter", "OpenRouter", "Single key for 100+ models"),
18
+ ("skip", "Skip for now", "Configure later with /set or /add_model"),
19
+ ]
20
+
21
+
22
+ # ============================================================================
23
+ # Navigation Footer (shown on ALL slides)
24
+ # ============================================================================
25
+
26
+
27
+ def get_nav_footer() -> str:
28
+ """Navigation hints shown at bottom of every slide."""
29
+ return (
30
+ "\n[dim]─────────────────────────────────────[/dim]\n"
31
+ "[green]→/l[/green] Next "
32
+ "[green]←/h[/green] Back "
33
+ "[green]↑↓/jk[/green] Options "
34
+ "[green]Enter[/green] Select "
35
+ "[yellow]ESC[/yellow] Skip"
36
+ )
37
+
38
+
39
+ # ============================================================================
40
+ # Gradient Banner
41
+ # ============================================================================
42
+
43
+
44
+ def get_gradient_banner() -> str:
45
+ """Generate the gradient CODE PUPPY banner."""
46
+ try:
47
+ import pyfiglet
48
+
49
+ lines = pyfiglet.figlet_format("CODE PUPPY", font="ansi_shadow").split("\n")
50
+ colors = ["bright_blue", "bright_cyan", "bright_green"]
51
+ result = []
52
+ for i, line in enumerate(lines):
53
+ if line.strip():
54
+ color = colors[min(i // 2, len(colors) - 1)]
55
+ result.append(f"[{color}]{line}[/{color}]")
56
+ return "\n".join(result)
57
+ except ImportError:
58
+ return "[bold bright_cyan]═══ CODE PUPPY 🐶 ═══[/bold bright_cyan]"
59
+
60
+
61
+ # ============================================================================
62
+ # Slide Content (5 slides total)
63
+ # ============================================================================
64
+
65
+
66
+ def slide_welcome() -> str:
67
+ """Slide 1: Welcome - quick intro."""
68
+ content = get_gradient_banner()
69
+ content += "\n\n"
70
+ content += "[bold white]Welcome! 🐶[/bold white]\n\n"
71
+ content += "[cyan]Quick setup:[/cyan]\n"
72
+ content += " 1. Pick your model provider\n"
73
+ content += " 2. Optional: MCP servers\n"
74
+ content += " 3. Learn when to use which agent\n"
75
+ content += " 4. Start coding!\n\n"
76
+ content += "[dim]Takes ~1 minute. Let's go![/dim]"
77
+ content += get_nav_footer()
78
+ return content
79
+
80
+
81
+ def slide_models(selected_option: int, options: List[Tuple[str, str]]) -> str:
82
+ """Slide 2: Model selection."""
83
+ content = "[bold cyan]📦 Pick Your Models[/bold cyan]\n\n"
84
+ content += "[white]How do you want to access LLMs?[/white]\n\n"
85
+
86
+ for i, (_, label) in enumerate(options):
87
+ if i == selected_option:
88
+ content += f"[bold green]▶ {label}[/bold green]\n"
89
+ else:
90
+ content += f"[dim] {label}[/dim]\n"
91
+
92
+ content += "\n"
93
+
94
+ # Context based on selection
95
+ opt = options[selected_option][0] if options else None
96
+ if opt == "chatgpt":
97
+ content += "[yellow]💡 ChatGPT OAuth[/yellow]\n"
98
+ content += " Uses your existing subscription\n"
99
+ content += " GPT-5.2, GPT-5.2-codex\n"
100
+ elif opt == "claude":
101
+ content += "[yellow]💡 Claude OAuth[/yellow]\n"
102
+ content += " Uses your existing subscription\n"
103
+ content += " Opus/Sonnet/Haiku 4.5\n"
104
+ elif opt == "api_keys":
105
+ content += "[yellow]💡 API Keys[/yellow]\n"
106
+ content += " [cyan]/set OPENAI_API_KEY=sk-...[/cyan]\n"
107
+ content += " [cyan]/add_model[/cyan] to browse 1500+ models\n"
108
+ elif opt == "openrouter":
109
+ content += "[yellow]💡 OpenRouter[/yellow]\n"
110
+ content += " One API key, all providers\n"
111
+ content += " [cyan]/set OPENROUTER_API_KEY=...[/cyan]\n"
112
+ else:
113
+ content += "[dim]No worries! Use /set or /add_model later[/dim]\n"
114
+
115
+ content += get_nav_footer()
116
+ return content
117
+
118
+
119
+ def slide_mcp() -> str:
120
+ """Slide 3: MCP servers (optional power-ups)."""
121
+ content = "[bold cyan]🔌 MCP Servers (Optional)[/bold cyan]\n\n"
122
+ content += "[white]Supercharge with external tools![/white]\n\n"
123
+ content += "[green]Commands:[/green]\n"
124
+ content += " [cyan]/mcp install[/cyan] Browse catalog\n"
125
+ content += " [cyan]/mcp list[/cyan] See your servers\n\n"
126
+ content += "[yellow]🌟 Popular picks:[/yellow]\n"
127
+ content += " • GitHub integration\n"
128
+ content += " • Postgres/databases\n"
129
+ content += " • Slack, Linear, etc.\n\n"
130
+ content += "[dim]Skip this if you just want to code![/dim]"
131
+ content += get_nav_footer()
132
+ return content
133
+
134
+
135
+ def slide_use_cases() -> str:
136
+ """Slide 4: When to use which agent - THE IMPORTANT ONE."""
137
+ content = "[bold cyan]🎯 When to Use What[/bold cyan]\n\n"
138
+
139
+ content += "[bold yellow]🐶 Code Puppy (default)[/bold yellow]\n"
140
+ content += " [green]USE FOR:[/green] Direct coding tasks\n"
141
+ content += " • Fix this bug\n"
142
+ content += " • Add a feature to this file\n"
143
+ content += " • Refactor this function\n"
144
+ content += " • Write tests for X\n\n"
145
+
146
+ content += "[bold yellow]📋 Planning Agent[/bold yellow]\n"
147
+ content += " [green]USE FOR:[/green] Complex multi-step projects\n"
148
+ content += " • Build me a REST API with auth\n"
149
+ content += " • Create a CLI tool from scratch\n"
150
+ content += " • Refactor entire codebase\n"
151
+ content += " • Multi-file architectural changes\n\n"
152
+
153
+ content += "[cyan]Switch: /agent planning-agent[/cyan]\n"
154
+ content += "[dim]Planning breaks big tasks into steps,[/dim]\n"
155
+ content += "[dim]then delegates to specialists.[/dim]"
156
+ content += get_nav_footer()
157
+ return content
158
+
159
+
160
+ def slide_done(trigger_oauth: str | None) -> str:
161
+ """Slide 5: You're ready!"""
162
+ content = "[bold green]🎉 Ready to Roll![/bold green]\n\n"
163
+ content += "[bold cyan]Essential commands:[/bold cyan]\n"
164
+ content += " [cyan]/model[/cyan] Switch models\n"
165
+ content += " [cyan]/agent[/cyan] Switch agents\n"
166
+ content += " [cyan]/help[/cyan] All commands\n\n"
167
+
168
+ content += "[bold yellow]Pro tips:[/bold yellow]\n"
169
+ content += " • Be specific in prompts\n"
170
+ content += " • Use Planning Agent for big tasks\n"
171
+ content += " • @ for file path completion\n\n"
172
+
173
+ if trigger_oauth:
174
+ content += f"[bold cyan]→ {trigger_oauth.title()} OAuth next![/bold cyan]\n\n"
175
+
176
+ content += "[dim]Re-run anytime: [/dim][cyan]/tutorial[/cyan]\n"
177
+ content += "\n[bold yellow]Press Enter to start coding! 🐶[/bold yellow]"
178
+ content += get_nav_footer()
179
+ return content
@@ -0,0 +1,342 @@
1
+ """Interactive TUI onboarding wizard for first-time Code Puppy users.
2
+
3
+ 🐶 Quick 5-slide tutorial. ADHD-friendly!
4
+
5
+ Usage:
6
+ from code_puppy.command_line.onboarding_wizard import (
7
+ run_onboarding_wizard,
8
+ reset_onboarding,
9
+ )
10
+
11
+ result = await run_onboarding_wizard()
12
+ # result: "chatgpt", "claude", "completed", "skipped", or None
13
+ """
14
+
15
+ import asyncio
16
+ import io
17
+ import os
18
+ import sys
19
+ from typing import List, Optional, Tuple
20
+
21
+ from prompt_toolkit import Application
22
+ from prompt_toolkit.formatted_text import ANSI
23
+ from prompt_toolkit.key_binding import KeyBindings
24
+ from prompt_toolkit.layout import Layout, Window
25
+ from prompt_toolkit.layout.controls import FormattedTextControl
26
+ from prompt_toolkit.widgets import Frame
27
+ from rich.console import Console
28
+
29
+ from code_puppy.config import CONFIG_DIR
30
+
31
+ from .onboarding_slides import (
32
+ MODEL_OPTIONS,
33
+ slide_done,
34
+ slide_mcp,
35
+ slide_models,
36
+ slide_use_cases,
37
+ slide_welcome,
38
+ )
39
+
40
+ # ============================================================================
41
+ # State Tracking
42
+ # ============================================================================
43
+
44
+ ONBOARDING_COMPLETE_FILE = os.path.join(CONFIG_DIR, "onboarding_complete")
45
+
46
+
47
+ def has_completed_onboarding() -> bool:
48
+ """Check if the user has already completed onboarding."""
49
+ return os.path.exists(ONBOARDING_COMPLETE_FILE)
50
+
51
+
52
+ def mark_onboarding_complete() -> None:
53
+ """Mark onboarding as complete."""
54
+ os.makedirs(os.path.dirname(ONBOARDING_COMPLETE_FILE), exist_ok=True)
55
+ with open(ONBOARDING_COMPLETE_FILE, "w") as f:
56
+ f.write("completed\n")
57
+
58
+
59
+ def should_show_onboarding() -> bool:
60
+ """Determine if the onboarding wizard should be shown.
61
+
62
+ Returns False if:
63
+ - User has already completed onboarding
64
+ - CODE_PUPPY_SKIP_TUTORIAL env var is set to '1' or 'true'
65
+ """
66
+ # Allow skipping tutorial via environment variable (useful for testing)
67
+ skip_env = os.environ.get("CODE_PUPPY_SKIP_TUTORIAL", "").lower()
68
+ if skip_env in ("1", "true", "yes"):
69
+ return False
70
+ return not has_completed_onboarding()
71
+
72
+
73
+ def reset_onboarding() -> None:
74
+ """Reset onboarding state (for re-running with /tutorial)."""
75
+ if os.path.exists(ONBOARDING_COMPLETE_FILE):
76
+ os.remove(ONBOARDING_COMPLETE_FILE)
77
+
78
+
79
+ # ============================================================================
80
+ # Onboarding Wizard Class
81
+ # ============================================================================
82
+
83
+
84
+ class OnboardingWizard:
85
+ """5-slide interactive tutorial.
86
+
87
+ Slides:
88
+ 0: Welcome
89
+ 1: Model selection
90
+ 2: MCP servers
91
+ 3: Use cases (Planning vs Coding)
92
+ 4: Done!
93
+ """
94
+
95
+ TOTAL_SLIDES = 5
96
+
97
+ def __init__(self):
98
+ """Initialize wizard state."""
99
+ self.current_slide = 0
100
+ self.selected_option = 0
101
+ self.trigger_oauth: Optional[str] = None
102
+ self.model_choice: Optional[str] = None
103
+ self.result: Optional[str] = None
104
+ self._should_exit = False
105
+
106
+ def get_progress_indicator(self) -> str:
107
+ """Progress dots: ● ○ ○ ○ ○"""
108
+ return " ".join(
109
+ "●" if i == self.current_slide else "○" for i in range(self.TOTAL_SLIDES)
110
+ )
111
+
112
+ def get_slide_content(self) -> str:
113
+ """Get content for current slide."""
114
+ if self.current_slide == 0:
115
+ return slide_welcome()
116
+ elif self.current_slide == 1:
117
+ options = self.get_options_for_slide()
118
+ return slide_models(self.selected_option, options)
119
+ elif self.current_slide == 2:
120
+ return slide_mcp()
121
+ elif self.current_slide == 3:
122
+ return slide_use_cases()
123
+ else: # slide 4
124
+ return slide_done(self.trigger_oauth)
125
+
126
+ def get_options_for_slide(self) -> List[Tuple[str, str]]:
127
+ """Get selectable options for current slide."""
128
+ if self.current_slide == 1: # Model selection
129
+ return [(opt[0], opt[1]) for opt in MODEL_OPTIONS]
130
+ return []
131
+
132
+ def handle_option_select(self) -> None:
133
+ """Handle option selection."""
134
+ if self.current_slide == 1: # Model selection
135
+ options = self.get_options_for_slide()
136
+ if 0 <= self.selected_option < len(options):
137
+ choice_id = options[self.selected_option][0]
138
+ self.model_choice = choice_id
139
+ if choice_id == "chatgpt":
140
+ self.trigger_oauth = "chatgpt"
141
+ elif choice_id == "claude":
142
+ self.trigger_oauth = "claude"
143
+
144
+ def next_slide(self) -> bool:
145
+ """Move to next slide."""
146
+ if self.current_slide < self.TOTAL_SLIDES - 1:
147
+ self.current_slide += 1
148
+ self.selected_option = 0
149
+ return True
150
+ return False
151
+
152
+ def prev_slide(self) -> bool:
153
+ """Move to previous slide."""
154
+ if self.current_slide > 0:
155
+ self.current_slide -= 1
156
+ self.selected_option = 0
157
+ return True
158
+ return False
159
+
160
+ def next_option(self) -> None:
161
+ """Move to next option."""
162
+ options = self.get_options_for_slide()
163
+ if options:
164
+ self.selected_option = (self.selected_option + 1) % len(options)
165
+
166
+ def prev_option(self) -> None:
167
+ """Move to previous option."""
168
+ options = self.get_options_for_slide()
169
+ if options:
170
+ self.selected_option = (self.selected_option - 1) % len(options)
171
+
172
+
173
+ # ============================================================================
174
+ # TUI Rendering
175
+ # ============================================================================
176
+
177
+
178
+ def _get_slide_panel_content(wizard: OnboardingWizard) -> ANSI:
179
+ """Generate slide content for display."""
180
+ buffer = io.StringIO()
181
+ console = Console(
182
+ file=buffer,
183
+ force_terminal=True,
184
+ width=80,
185
+ legacy_windows=False,
186
+ color_system="truecolor",
187
+ no_color=False,
188
+ force_interactive=True,
189
+ )
190
+
191
+ # Progress indicator
192
+ progress = wizard.get_progress_indicator()
193
+ console.print(f"[dim]{progress}[/dim]")
194
+ console.print(
195
+ f"[dim]Slide {wizard.current_slide + 1} of {wizard.TOTAL_SLIDES}[/dim]\n"
196
+ )
197
+
198
+ # Slide content (includes nav footer)
199
+ console.print(wizard.get_slide_content())
200
+
201
+ return ANSI(buffer.getvalue())
202
+
203
+
204
+ # ============================================================================
205
+ # Main Entry Point
206
+ # ============================================================================
207
+
208
+
209
+ async def run_onboarding_wizard() -> Optional[str]:
210
+ """Run the interactive tutorial.
211
+
212
+ Returns:
213
+ - "chatgpt" if user wants ChatGPT OAuth
214
+ - "claude" if user wants Claude OAuth
215
+ - "completed" if finished normally
216
+ - "skipped" if user pressed ESC
217
+ - None on error
218
+ """
219
+ from code_puppy.tools.command_runner import set_awaiting_user_input
220
+
221
+ wizard = OnboardingWizard()
222
+ set_awaiting_user_input(True)
223
+
224
+ # Enter alternate screen buffer
225
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
226
+ sys.stdout.write("\033[2J\033[H") # Clear and home
227
+ sys.stdout.flush()
228
+ await asyncio.sleep(0.1)
229
+
230
+ try:
231
+ kb = KeyBindings()
232
+
233
+ @kb.add("right")
234
+ @kb.add("l")
235
+ def next_slide(event):
236
+ if wizard.current_slide == wizard.TOTAL_SLIDES - 1:
237
+ wizard.result = "completed"
238
+ wizard._should_exit = True
239
+ event.app.exit()
240
+ else:
241
+ wizard.next_slide()
242
+ event.app.invalidate()
243
+
244
+ @kb.add("left")
245
+ @kb.add("h")
246
+ def prev_slide(event):
247
+ wizard.prev_slide()
248
+ event.app.invalidate()
249
+
250
+ @kb.add("down")
251
+ @kb.add("j")
252
+ @kb.add("c-n") # Ctrl+N = next (Emacs-style)
253
+ def next_option(event):
254
+ wizard.next_option()
255
+ event.app.invalidate()
256
+
257
+ @kb.add("up")
258
+ @kb.add("k")
259
+ @kb.add("c-p") # Ctrl+P = previous (Emacs-style)
260
+ def prev_option(event):
261
+ wizard.prev_option()
262
+ event.app.invalidate()
263
+
264
+ @kb.add("enter")
265
+ def select_or_next(event):
266
+ options = wizard.get_options_for_slide()
267
+ if options:
268
+ wizard.handle_option_select()
269
+
270
+ if wizard.current_slide == wizard.TOTAL_SLIDES - 1:
271
+ wizard.result = "completed"
272
+ wizard._should_exit = True
273
+ event.app.exit()
274
+ else:
275
+ wizard.next_slide()
276
+ event.app.invalidate()
277
+
278
+ @kb.add("escape")
279
+ def skip_wizard(event):
280
+ wizard.result = "skipped"
281
+ wizard._should_exit = True
282
+ event.app.exit()
283
+
284
+ @kb.add("c-c")
285
+ def cancel_wizard(event):
286
+ wizard.result = "skipped"
287
+ wizard._should_exit = True
288
+ event.app.exit()
289
+
290
+ slide_panel = Window(
291
+ content=FormattedTextControl(lambda: _get_slide_panel_content(wizard))
292
+ )
293
+
294
+ root_container = Frame(slide_panel, title="🐶 Code Puppy Tutorial")
295
+ layout = Layout(root_container)
296
+
297
+ app = Application(
298
+ layout=layout,
299
+ key_bindings=kb,
300
+ full_screen=False,
301
+ mouse_support=False,
302
+ color_depth="DEPTH_24_BIT",
303
+ )
304
+
305
+ sys.stdout.write("\033[2J\033[H")
306
+ sys.stdout.flush()
307
+
308
+ await app.run_async()
309
+
310
+ except KeyboardInterrupt:
311
+ wizard.result = "skipped"
312
+ except Exception:
313
+ wizard.result = None
314
+ finally:
315
+ set_awaiting_user_input(False)
316
+ sys.stdout.write("\033[?1049l")
317
+ sys.stdout.flush()
318
+
319
+ # Clear exit message
320
+ from code_puppy.messaging import emit_info
321
+
322
+ if wizard.result == "skipped":
323
+ emit_info("✓ Tutorial skipped")
324
+ elif wizard.result == "completed":
325
+ emit_info("✓ Tutorial completed! Welcome to Code Puppy! 🐶")
326
+ else:
327
+ emit_info("✓ Exited tutorial")
328
+
329
+ if wizard.result in ("completed", "skipped"):
330
+ mark_onboarding_complete()
331
+
332
+ if wizard.trigger_oauth:
333
+ return wizard.trigger_oauth
334
+
335
+ return wizard.result
336
+
337
+
338
+ async def run_onboarding_if_needed() -> Optional[str]:
339
+ """Run tutorial if user hasn't seen it yet."""
340
+ if should_show_onboarding():
341
+ return await run_onboarding_wizard()
342
+ return None