code-puppy 0.0.373__py3-none-any.whl → 0.0.375__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 (44) hide show
  1. code_puppy/agents/agent_creator_agent.py +49 -1
  2. code_puppy/agents/agent_helios.py +122 -0
  3. code_puppy/agents/agent_manager.py +60 -4
  4. code_puppy/agents/base_agent.py +61 -4
  5. code_puppy/agents/json_agent.py +30 -7
  6. code_puppy/callbacks.py +125 -0
  7. code_puppy/command_line/colors_menu.py +2 -0
  8. code_puppy/command_line/command_handler.py +1 -0
  9. code_puppy/command_line/config_commands.py +3 -1
  10. code_puppy/command_line/uc_menu.py +890 -0
  11. code_puppy/config.py +29 -0
  12. code_puppy/messaging/messages.py +18 -0
  13. code_puppy/messaging/rich_renderer.py +48 -7
  14. code_puppy/messaging/subagent_console.py +0 -1
  15. code_puppy/model_factory.py +63 -258
  16. code_puppy/model_utils.py +33 -1
  17. code_puppy/plugins/antigravity_oauth/register_callbacks.py +106 -1
  18. code_puppy/plugins/antigravity_oauth/utils.py +2 -3
  19. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +85 -3
  20. code_puppy/plugins/claude_code_oauth/register_callbacks.py +88 -0
  21. code_puppy/plugins/ralph/__init__.py +13 -0
  22. code_puppy/plugins/ralph/agents.py +433 -0
  23. code_puppy/plugins/ralph/commands.py +208 -0
  24. code_puppy/plugins/ralph/loop_controller.py +285 -0
  25. code_puppy/plugins/ralph/models.py +125 -0
  26. code_puppy/plugins/ralph/register_callbacks.py +133 -0
  27. code_puppy/plugins/ralph/state_manager.py +322 -0
  28. code_puppy/plugins/ralph/tools.py +451 -0
  29. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  30. code_puppy/plugins/universal_constructor/models.py +138 -0
  31. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  32. code_puppy/plugins/universal_constructor/registry.py +304 -0
  33. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  34. code_puppy/tools/__init__.py +169 -1
  35. code_puppy/tools/agent_tools.py +1 -1
  36. code_puppy/tools/command_runner.py +23 -9
  37. code_puppy/tools/universal_constructor.py +889 -0
  38. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/METADATA +1 -1
  39. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/RECORD +44 -28
  40. {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models.json +0 -0
  41. {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models_dev_api.json +0 -0
  42. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/WHEEL +0 -0
  43. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/entry_points.txt +0 -0
  44. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,8 @@
1
- """Antigravity OAuth Plugin callbacks for Code Puppy CLI."""
1
+ """Antigravity OAuth Plugin callbacks for Code Puppy CLI.
2
+
3
+ Provides OAuth authentication for Antigravity models and registers
4
+ the 'antigravity' model type handler.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -30,6 +34,8 @@ from .oauth import (
30
34
  prepare_oauth_context,
31
35
  )
32
36
  from .storage import clear_accounts
37
+ from .token import is_token_expired, refresh_access_token
38
+ from .transport import create_antigravity_client
33
39
  from .utils import (
34
40
  add_models_to_config,
35
41
  load_antigravity_models,
@@ -408,6 +414,105 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
408
414
  return None
409
415
 
410
416
 
417
+ def _create_antigravity_model(model_name: str, model_config: Dict, config: Dict) -> Any:
418
+ """Create an Antigravity model instance.
419
+
420
+ This handler is registered via the 'register_model_type' callback to handle
421
+ models with type='antigravity'.
422
+ """
423
+ from code_puppy.gemini_model import GeminiModel
424
+ from code_puppy.model_factory import get_custom_config
425
+
426
+ # Try to import custom model for thinking signatures
427
+ try:
428
+ from .antigravity_model import AntigravityModel
429
+ except ImportError:
430
+ AntigravityModel = None # type: ignore
431
+
432
+ url, headers, verify, api_key = get_custom_config(model_config)
433
+ if not api_key:
434
+ emit_warning(
435
+ f"API key is not set for Antigravity endpoint; skipping model '{model_config.get('name')}'."
436
+ )
437
+ return None
438
+
439
+ # Get fresh access token (refresh if needed)
440
+ tokens = load_stored_tokens()
441
+ if not tokens:
442
+ emit_warning("Antigravity tokens not found; run /antigravity-auth first.")
443
+ return None
444
+
445
+ access_token = tokens.get("access_token", "")
446
+ refresh_token = tokens.get("refresh_token", "")
447
+ expires_at = tokens.get("expires_at")
448
+
449
+ # Refresh if expired or about to expire (initial check)
450
+ if is_token_expired(expires_at):
451
+ new_tokens = refresh_access_token(refresh_token)
452
+ if new_tokens:
453
+ access_token = new_tokens.access_token
454
+ refresh_token = new_tokens.refresh_token
455
+ expires_at = new_tokens.expires_at
456
+ tokens["access_token"] = new_tokens.access_token
457
+ tokens["refresh_token"] = new_tokens.refresh_token
458
+ tokens["expires_at"] = new_tokens.expires_at
459
+ save_tokens(tokens)
460
+ else:
461
+ emit_warning(
462
+ "Failed to refresh Antigravity token; run /antigravity-auth again."
463
+ )
464
+ return None
465
+
466
+ # Callback to persist tokens when proactively refreshed during session
467
+ def on_token_refreshed(new_tokens: Any) -> None:
468
+ """Persist new tokens when proactively refreshed."""
469
+ try:
470
+ updated_tokens = load_stored_tokens() or {}
471
+ updated_tokens["access_token"] = new_tokens.access_token
472
+ updated_tokens["refresh_token"] = new_tokens.refresh_token
473
+ updated_tokens["expires_at"] = new_tokens.expires_at
474
+ save_tokens(updated_tokens)
475
+ logger.debug("Persisted proactively refreshed Antigravity tokens")
476
+ except Exception as e:
477
+ logger.warning("Failed to persist refreshed tokens: %s", e)
478
+
479
+ project_id = tokens.get("project_id", model_config.get("project_id", ""))
480
+ client = create_antigravity_client(
481
+ access_token=access_token,
482
+ project_id=project_id,
483
+ model_name=model_config["name"],
484
+ base_url=url,
485
+ headers=headers,
486
+ refresh_token=refresh_token,
487
+ expires_at=expires_at,
488
+ on_token_refreshed=on_token_refreshed,
489
+ )
490
+
491
+ # Use custom model with direct httpx client
492
+ if AntigravityModel:
493
+ model = AntigravityModel(
494
+ model_name=model_config["name"],
495
+ api_key=api_key or "", # Antigravity uses OAuth, key may be empty
496
+ base_url=url,
497
+ http_client=client,
498
+ )
499
+ else:
500
+ model = GeminiModel(
501
+ model_name=model_config["name"],
502
+ api_key=api_key or "",
503
+ base_url=url,
504
+ http_client=client,
505
+ )
506
+
507
+ return model
508
+
509
+
510
+ def _register_model_types() -> List[Dict[str, Any]]:
511
+ """Register the antigravity model type handler."""
512
+ return [{"type": "antigravity", "handler": _create_antigravity_model}]
513
+
514
+
411
515
  # Register callbacks
412
516
  register_callback("custom_command_help", _custom_help)
413
517
  register_callback("custom_command", _handle_custom_command)
518
+ register_callback("register_model_type", _register_model_types)
@@ -77,9 +77,9 @@ def add_models_to_config(access_token: str, project_id: str = "") -> bool:
77
77
  # Build custom headers
78
78
  headers = dict(ANTIGRAVITY_HEADERS)
79
79
 
80
- # Use custom_gemini type with Antigravity transport
80
+ # Use antigravity type - handled by the plugin's register_model_type callback
81
81
  models_config[prefixed_name] = {
82
- "type": "custom_gemini",
82
+ "type": "antigravity",
83
83
  "name": model_id,
84
84
  "custom_endpoint": {
85
85
  "url": ANTIGRAVITY_ENDPOINT,
@@ -90,7 +90,6 @@ def add_models_to_config(access_token: str, project_id: str = "") -> bool:
90
90
  "context_length": model_info.get("context_length", 200000),
91
91
  "family": model_info.get("family", "other"),
92
92
  "oauth_source": "antigravity-plugin",
93
- "antigravity": True, # Flag to use Antigravity transport
94
93
  }
95
94
 
96
95
  # Add thinking budget if present
@@ -1,9 +1,13 @@
1
- """ChatGPT OAuth plugin callbacks aligned with ChatMock flow."""
1
+ """ChatGPT OAuth plugin callbacks aligned with ChatMock flow.
2
+
3
+ Provides OAuth authentication for ChatGPT models and registers
4
+ the 'chatgpt_oauth' model type handler.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
5
9
  import os
6
- from typing import List, Optional, Tuple
10
+ from typing import Any, Dict, List, Optional, Tuple
7
11
 
8
12
  from code_puppy.callbacks import register_callback
9
13
  from code_puppy.messaging import emit_info, emit_success, emit_warning
@@ -11,7 +15,12 @@ from code_puppy.model_switching import set_model_and_reload_agent
11
15
 
12
16
  from .config import CHATGPT_OAUTH_CONFIG, get_token_storage_path
13
17
  from .oauth_flow import run_oauth_flow
14
- from .utils import load_chatgpt_models, load_stored_tokens, remove_chatgpt_models
18
+ from .utils import (
19
+ get_valid_access_token,
20
+ load_chatgpt_models,
21
+ load_stored_tokens,
22
+ remove_chatgpt_models,
23
+ )
15
24
 
16
25
 
17
26
  def _custom_help() -> List[Tuple[str, str]]:
@@ -90,5 +99,78 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
90
99
  return None
91
100
 
92
101
 
102
+ def _create_chatgpt_oauth_model(
103
+ model_name: str, model_config: Dict, config: Dict
104
+ ) -> Any:
105
+ """Create a ChatGPT OAuth model instance.
106
+
107
+ This handler is registered via the 'register_model_type' callback to handle
108
+ models with type='chatgpt_oauth'.
109
+ """
110
+ from pydantic_ai.models.openai import OpenAIResponsesModel
111
+ from pydantic_ai.providers.openai import OpenAIProvider
112
+
113
+ from code_puppy.chatgpt_codex_client import create_codex_async_client
114
+ from code_puppy.http_utils import get_cert_bundle_path
115
+
116
+ # Get a valid access token (refreshing if needed)
117
+ access_token = get_valid_access_token()
118
+ if not access_token:
119
+ emit_warning(
120
+ f"Failed to get valid ChatGPT OAuth token; skipping model '{model_config.get('name')}'. "
121
+ "Run /chatgpt-auth to authenticate."
122
+ )
123
+ return None
124
+
125
+ # Get account_id from stored tokens (required for ChatGPT-Account-Id header)
126
+ tokens = load_stored_tokens()
127
+ account_id = tokens.get("account_id", "") if tokens else ""
128
+ if not account_id:
129
+ emit_warning(
130
+ f"No account_id found in ChatGPT OAuth tokens; skipping model '{model_config.get('name')}'. "
131
+ "Run /chatgpt-auth to re-authenticate."
132
+ )
133
+ return None
134
+
135
+ # Build headers for ChatGPT Codex API
136
+ originator = CHATGPT_OAUTH_CONFIG.get("originator", "codex_cli_rs")
137
+ client_version = CHATGPT_OAUTH_CONFIG.get("client_version", "0.72.0")
138
+
139
+ headers = {
140
+ "ChatGPT-Account-Id": account_id,
141
+ "originator": originator,
142
+ "User-Agent": f"{originator}/{client_version}",
143
+ }
144
+ # Merge with any headers from model config
145
+ config_headers = model_config.get("custom_endpoint", {}).get("headers", {})
146
+ headers.update(config_headers)
147
+
148
+ # Get base URL - Codex API uses chatgpt.com, not api.openai.com
149
+ base_url = model_config.get("custom_endpoint", {}).get(
150
+ "url", CHATGPT_OAUTH_CONFIG["api_base_url"]
151
+ )
152
+
153
+ # Create HTTP client with Codex interceptor for store=false injection
154
+ verify = get_cert_bundle_path()
155
+ client = create_codex_async_client(headers=headers, verify=verify)
156
+
157
+ provider = OpenAIProvider(
158
+ api_key=access_token,
159
+ base_url=base_url,
160
+ http_client=client,
161
+ )
162
+
163
+ # ChatGPT Codex API only supports Responses format
164
+ model = OpenAIResponsesModel(model_name=model_config["name"], provider=provider)
165
+ setattr(model, "provider", provider)
166
+ return model
167
+
168
+
169
+ def _register_model_types() -> List[Dict[str, Any]]:
170
+ """Register the chatgpt_oauth model type handler."""
171
+ return [{"type": "chatgpt_oauth", "handler": _create_chatgpt_oauth_model}]
172
+
173
+
93
174
  register_callback("custom_command_help", _custom_help)
94
175
  register_callback("custom_command", _handle_custom_command)
176
+ register_callback("register_model_type", _register_model_types)
@@ -1,5 +1,8 @@
1
1
  """
2
2
  Claude Code OAuth Plugin for Code Puppy.
3
+
4
+ Provides OAuth authentication for Claude Code models and registers
5
+ the 'claude_code' model type handler.
3
6
  """
4
7
 
5
8
  from __future__ import annotations
@@ -24,6 +27,7 @@ from .utils import (
24
27
  build_authorization_url,
25
28
  exchange_code_for_tokens,
26
29
  fetch_claude_code_models,
30
+ get_valid_access_token,
27
31
  load_claude_models_filtered,
28
32
  load_stored_tokens,
29
33
  prepare_oauth_context,
@@ -276,5 +280,89 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
276
280
  return None
277
281
 
278
282
 
283
+ def _create_claude_code_model(model_name: str, model_config: Dict, config: Dict) -> Any:
284
+ """Create a Claude Code model instance.
285
+
286
+ This handler is registered via the 'register_model_type' callback to handle
287
+ models with type='claude_code'.
288
+ """
289
+ from anthropic import AsyncAnthropic
290
+ from pydantic_ai.models.anthropic import AnthropicModel
291
+ from pydantic_ai.providers.anthropic import AnthropicProvider
292
+
293
+ from code_puppy.claude_cache_client import (
294
+ ClaudeCacheAsyncClient,
295
+ patch_anthropic_client_messages,
296
+ )
297
+ from code_puppy.config import get_effective_model_settings
298
+ from code_puppy.http_utils import get_cert_bundle_path, get_http2
299
+ from code_puppy.model_factory import get_custom_config
300
+
301
+ url, headers, verify, api_key = get_custom_config(model_config)
302
+
303
+ # Refresh token if this is from the plugin
304
+ if model_config.get("oauth_source") == "claude-code-plugin":
305
+ refreshed_token = get_valid_access_token()
306
+ if refreshed_token:
307
+ api_key = refreshed_token
308
+ custom_endpoint = model_config.get("custom_endpoint")
309
+ if isinstance(custom_endpoint, dict):
310
+ custom_endpoint["api_key"] = refreshed_token
311
+
312
+ if not api_key:
313
+ emit_warning(
314
+ f"API key is not set for Claude Code endpoint; skipping model '{model_config.get('name')}'."
315
+ )
316
+ return None
317
+
318
+ # Check if interleaved thinking is enabled (defaults to True for OAuth models)
319
+ effective_settings = get_effective_model_settings(model_name)
320
+ interleaved_thinking = effective_settings.get("interleaved_thinking", True)
321
+
322
+ # Handle anthropic-beta header based on interleaved_thinking setting
323
+ if "anthropic-beta" in headers:
324
+ beta_parts = [p.strip() for p in headers["anthropic-beta"].split(",")]
325
+ if interleaved_thinking:
326
+ if "interleaved-thinking-2025-05-14" not in beta_parts:
327
+ beta_parts.append("interleaved-thinking-2025-05-14")
328
+ else:
329
+ beta_parts = [p for p in beta_parts if "interleaved-thinking" not in p]
330
+ headers["anthropic-beta"] = ",".join(beta_parts) if beta_parts else None
331
+ if headers.get("anthropic-beta") is None:
332
+ del headers["anthropic-beta"]
333
+ elif interleaved_thinking:
334
+ headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
335
+
336
+ # Use a dedicated client wrapper that injects cache_control on /v1/messages
337
+ if verify is None:
338
+ verify = get_cert_bundle_path()
339
+
340
+ http2_enabled = get_http2()
341
+
342
+ client = ClaudeCacheAsyncClient(
343
+ headers=headers,
344
+ verify=verify,
345
+ timeout=180,
346
+ http2=http2_enabled,
347
+ )
348
+
349
+ anthropic_client = AsyncAnthropic(
350
+ base_url=url,
351
+ http_client=client,
352
+ auth_token=api_key,
353
+ )
354
+ patch_anthropic_client_messages(anthropic_client)
355
+ anthropic_client.api_key = None
356
+ anthropic_client.auth_token = api_key
357
+ provider = AnthropicProvider(anthropic_client=anthropic_client)
358
+ return AnthropicModel(model_name=model_config["name"], provider=provider)
359
+
360
+
361
+ def _register_model_types() -> List[Dict[str, Any]]:
362
+ """Register the claude_code model type handler."""
363
+ return [{"type": "claude_code", "handler": _create_claude_code_model}]
364
+
365
+
279
366
  register_callback("custom_command_help", _custom_help)
280
367
  register_callback("custom_command", _handle_custom_command)
368
+ register_callback("register_model_type", _register_model_types)
@@ -0,0 +1,13 @@
1
+ """Ralph Plugin - Autonomous AI agent loop for completing PRDs.
2
+
3
+ Based on Geoffrey Huntley's Ralph pattern: https://ghuntley.com/ralph/
4
+
5
+ This plugin provides:
6
+ - PRD Generator agent for creating detailed requirements
7
+ - Ralph Converter agent for converting PRDs to JSON format
8
+ - Ralph Orchestrator agent for autonomous execution
9
+ - Tools for managing prd.json and progress.txt
10
+ - /ralph commands for controlling the workflow
11
+ """
12
+
13
+ __version__ = "0.1.0"