code-puppy 0.0.374__py3-none-any.whl → 0.0.376__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/agents/agent_manager.py +34 -2
- code_puppy/agents/base_agent.py +122 -41
- code_puppy/callbacks.py +173 -0
- code_puppy/messaging/rich_renderer.py +13 -7
- code_puppy/model_factory.py +63 -258
- code_puppy/model_utils.py +33 -1
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +106 -1
- code_puppy/plugins/antigravity_oauth/utils.py +2 -3
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +85 -3
- code_puppy/plugins/claude_code_oauth/__init__.py +19 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +160 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +242 -0
- code_puppy/plugins/ralph/__init__.py +13 -0
- code_puppy/plugins/ralph/agents.py +433 -0
- code_puppy/plugins/ralph/commands.py +208 -0
- code_puppy/plugins/ralph/loop_controller.py +289 -0
- code_puppy/plugins/ralph/models.py +125 -0
- code_puppy/plugins/ralph/register_callbacks.py +140 -0
- code_puppy/plugins/ralph/state_manager.py +322 -0
- code_puppy/plugins/ralph/tools.py +451 -0
- code_puppy/tools/__init__.py +31 -0
- code_puppy/tools/agent_tools.py +1 -1
- code_puppy/tools/command_runner.py +23 -9
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.376.dist-info}/METADATA +1 -1
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.376.dist-info}/RECORD +30 -21
- {code_puppy-0.0.374.data → code_puppy-0.0.376.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.374.data → code_puppy-0.0.376.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.376.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.376.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.376.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,161 @@ 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
|
+
|
|
366
|
+
# Global storage for the token refresh heartbeat
|
|
367
|
+
# Using a dict to allow multiple concurrent agent runs (keyed by session_id)
|
|
368
|
+
_active_heartbeats: Dict[str, Any] = {}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
async def _on_agent_run_start(
|
|
372
|
+
agent_name: str,
|
|
373
|
+
model_name: str,
|
|
374
|
+
session_id: Optional[str] = None,
|
|
375
|
+
) -> None:
|
|
376
|
+
"""Start token refresh heartbeat for Claude Code OAuth models.
|
|
377
|
+
|
|
378
|
+
This callback is triggered when an agent run starts. If the model is a
|
|
379
|
+
Claude Code OAuth model, we start a background heartbeat to keep the
|
|
380
|
+
token fresh during long-running operations.
|
|
381
|
+
"""
|
|
382
|
+
# Only start heartbeat for Claude Code models
|
|
383
|
+
if not model_name.startswith("claude-code"):
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
from .token_refresh_heartbeat import TokenRefreshHeartbeat
|
|
388
|
+
|
|
389
|
+
heartbeat = TokenRefreshHeartbeat()
|
|
390
|
+
await heartbeat.start()
|
|
391
|
+
|
|
392
|
+
# Store heartbeat for cleanup, keyed by session_id
|
|
393
|
+
key = session_id or "default"
|
|
394
|
+
_active_heartbeats[key] = heartbeat
|
|
395
|
+
logger.debug(
|
|
396
|
+
"Started token refresh heartbeat for session %s (model: %s)",
|
|
397
|
+
key,
|
|
398
|
+
model_name,
|
|
399
|
+
)
|
|
400
|
+
except ImportError:
|
|
401
|
+
logger.debug("Token refresh heartbeat module not available")
|
|
402
|
+
except Exception as exc:
|
|
403
|
+
logger.debug("Failed to start token refresh heartbeat: %s", exc)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
async def _on_agent_run_end(
|
|
407
|
+
agent_name: str,
|
|
408
|
+
model_name: str,
|
|
409
|
+
session_id: Optional[str] = None,
|
|
410
|
+
success: bool = True,
|
|
411
|
+
error: Optional[Exception] = None,
|
|
412
|
+
response_text: Optional[str] = None,
|
|
413
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
414
|
+
) -> None:
|
|
415
|
+
"""Stop token refresh heartbeat when agent run ends.
|
|
416
|
+
|
|
417
|
+
This callback is triggered when an agent run completes (success or failure).
|
|
418
|
+
We stop any heartbeat that was started for this session.
|
|
419
|
+
"""
|
|
420
|
+
# We don't use response_text or metadata, just cleanup the heartbeat
|
|
421
|
+
key = session_id or "default"
|
|
422
|
+
heartbeat = _active_heartbeats.pop(key, None)
|
|
423
|
+
|
|
424
|
+
if heartbeat is not None:
|
|
425
|
+
try:
|
|
426
|
+
await heartbeat.stop()
|
|
427
|
+
logger.debug(
|
|
428
|
+
"Stopped token refresh heartbeat for session %s (refreshed %d times)",
|
|
429
|
+
key,
|
|
430
|
+
heartbeat.refresh_count,
|
|
431
|
+
)
|
|
432
|
+
except Exception as exc:
|
|
433
|
+
logger.debug("Error stopping token refresh heartbeat: %s", exc)
|
|
434
|
+
|
|
435
|
+
|
|
279
436
|
register_callback("custom_command_help", _custom_help)
|
|
280
437
|
register_callback("custom_command", _handle_custom_command)
|
|
438
|
+
register_callback("register_model_type", _register_model_types)
|
|
439
|
+
register_callback("agent_run_start", _on_agent_run_start)
|
|
440
|
+
register_callback("agent_run_end", _on_agent_run_end)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Token refresh heartbeat for long-running Claude Code OAuth sessions.
|
|
2
|
+
|
|
3
|
+
This module provides a background task that periodically checks and refreshes
|
|
4
|
+
Claude Code OAuth tokens during long-running agentic operations. This ensures
|
|
5
|
+
that tokens don't expire during extended streaming responses or tool processing.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
async with token_refresh_heartbeat_context():
|
|
9
|
+
# Long running agent operation
|
|
10
|
+
await agent.run(...)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
from contextlib import asynccontextmanager
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Heartbeat interval in seconds - check token every 2 minutes
|
|
24
|
+
# This is frequent enough to catch expiring tokens before they cause issues
|
|
25
|
+
# but not so frequent as to spam the token endpoint
|
|
26
|
+
HEARTBEAT_INTERVAL_SECONDS = 120
|
|
27
|
+
|
|
28
|
+
# Minimum time between refresh attempts to avoid hammering the endpoint
|
|
29
|
+
MIN_REFRESH_INTERVAL_SECONDS = 60
|
|
30
|
+
|
|
31
|
+
# Global tracking of last refresh time to coordinate across heartbeats
|
|
32
|
+
_last_refresh_time: float = 0.0
|
|
33
|
+
_heartbeat_lock = asyncio.Lock()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TokenRefreshHeartbeat:
|
|
37
|
+
"""Background task that periodically refreshes Claude Code OAuth tokens.
|
|
38
|
+
|
|
39
|
+
This runs as an asyncio task during agent operations and checks if the
|
|
40
|
+
token needs refreshing at regular intervals.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
interval: float = HEARTBEAT_INTERVAL_SECONDS,
|
|
46
|
+
min_refresh_interval: float = MIN_REFRESH_INTERVAL_SECONDS,
|
|
47
|
+
):
|
|
48
|
+
self._interval = interval
|
|
49
|
+
self._min_refresh_interval = min_refresh_interval
|
|
50
|
+
self._task: Optional[asyncio.Task] = None
|
|
51
|
+
self._stop_event = asyncio.Event()
|
|
52
|
+
self._refresh_count = 0
|
|
53
|
+
|
|
54
|
+
async def start(self) -> None:
|
|
55
|
+
"""Start the heartbeat background task."""
|
|
56
|
+
if self._task is not None:
|
|
57
|
+
logger.debug("Heartbeat already running")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
self._stop_event.clear()
|
|
61
|
+
self._task = asyncio.create_task(self._heartbeat_loop())
|
|
62
|
+
logger.debug("Token refresh heartbeat started")
|
|
63
|
+
|
|
64
|
+
async def stop(self) -> None:
|
|
65
|
+
"""Stop the heartbeat background task."""
|
|
66
|
+
if self._task is None:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
self._stop_event.set()
|
|
70
|
+
self._task.cancel()
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
await self._task
|
|
74
|
+
except asyncio.CancelledError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
self._task = None
|
|
78
|
+
logger.debug(
|
|
79
|
+
"Token refresh heartbeat stopped (refreshed %d times)",
|
|
80
|
+
self._refresh_count,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async def _heartbeat_loop(self) -> None:
|
|
84
|
+
"""Main heartbeat loop that periodically checks token status."""
|
|
85
|
+
global _last_refresh_time
|
|
86
|
+
|
|
87
|
+
while not self._stop_event.is_set():
|
|
88
|
+
try:
|
|
89
|
+
# Wait for the interval or until stopped
|
|
90
|
+
try:
|
|
91
|
+
await asyncio.wait_for(
|
|
92
|
+
self._stop_event.wait(), timeout=self._interval
|
|
93
|
+
)
|
|
94
|
+
# If we got here, stop event was set
|
|
95
|
+
break
|
|
96
|
+
except asyncio.TimeoutError:
|
|
97
|
+
# Normal timeout - time to check token
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# Check if we should attempt refresh
|
|
101
|
+
async with _heartbeat_lock:
|
|
102
|
+
now = time.time()
|
|
103
|
+
if now - _last_refresh_time < self._min_refresh_interval:
|
|
104
|
+
logger.debug(
|
|
105
|
+
"Skipping refresh - last refresh was %.1f seconds ago",
|
|
106
|
+
now - _last_refresh_time,
|
|
107
|
+
)
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Attempt the refresh
|
|
111
|
+
refreshed = await self._attempt_refresh()
|
|
112
|
+
if refreshed:
|
|
113
|
+
_last_refresh_time = now
|
|
114
|
+
self._refresh_count += 1
|
|
115
|
+
|
|
116
|
+
except asyncio.CancelledError:
|
|
117
|
+
break
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
logger.debug("Error in heartbeat loop: %s", exc)
|
|
120
|
+
# Continue running - don't let errors kill the heartbeat
|
|
121
|
+
await asyncio.sleep(5) # Brief pause before retrying
|
|
122
|
+
|
|
123
|
+
async def _attempt_refresh(self) -> bool:
|
|
124
|
+
"""Attempt to refresh the token if needed.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if a refresh was performed, False otherwise.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
# Import here to avoid circular imports
|
|
131
|
+
from .utils import (
|
|
132
|
+
is_token_expired,
|
|
133
|
+
load_stored_tokens,
|
|
134
|
+
refresh_access_token,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
tokens = load_stored_tokens()
|
|
138
|
+
if not tokens:
|
|
139
|
+
logger.debug("No stored tokens found")
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
if not is_token_expired(tokens):
|
|
143
|
+
logger.debug("Token not yet expired, skipping refresh")
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
# Token is expiring soon, refresh it
|
|
147
|
+
logger.info("Heartbeat: Token expiring soon, refreshing proactively")
|
|
148
|
+
refreshed_token = refresh_access_token(force=False)
|
|
149
|
+
|
|
150
|
+
if refreshed_token:
|
|
151
|
+
logger.info("Heartbeat: Successfully refreshed token")
|
|
152
|
+
return True
|
|
153
|
+
else:
|
|
154
|
+
logger.warning("Heartbeat: Token refresh returned None")
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
logger.error("Heartbeat: Error during token refresh: %s", exc)
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def refresh_count(self) -> int:
|
|
163
|
+
"""Get the number of successful refreshes performed by this heartbeat."""
|
|
164
|
+
return self._refresh_count
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def is_running(self) -> bool:
|
|
168
|
+
"""Check if the heartbeat is currently running."""
|
|
169
|
+
return self._task is not None and not self._task.done()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# Global heartbeat instance for the current session
|
|
173
|
+
_current_heartbeat: Optional[TokenRefreshHeartbeat] = None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@asynccontextmanager
|
|
177
|
+
async def token_refresh_heartbeat_context(
|
|
178
|
+
interval: float = HEARTBEAT_INTERVAL_SECONDS,
|
|
179
|
+
):
|
|
180
|
+
"""Context manager that runs token refresh heartbeat during its scope.
|
|
181
|
+
|
|
182
|
+
Use this around long-running agent operations to ensure tokens stay fresh.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
interval: Seconds between heartbeat checks. Default is 2 minutes.
|
|
186
|
+
|
|
187
|
+
Example:
|
|
188
|
+
async with token_refresh_heartbeat_context():
|
|
189
|
+
result = await agent.run(prompt)
|
|
190
|
+
"""
|
|
191
|
+
global _current_heartbeat
|
|
192
|
+
|
|
193
|
+
heartbeat = TokenRefreshHeartbeat(interval=interval)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
await heartbeat.start()
|
|
197
|
+
_current_heartbeat = heartbeat
|
|
198
|
+
yield heartbeat
|
|
199
|
+
finally:
|
|
200
|
+
await heartbeat.stop()
|
|
201
|
+
_current_heartbeat = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def is_heartbeat_running() -> bool:
|
|
205
|
+
"""Check if a token refresh heartbeat is currently active."""
|
|
206
|
+
return _current_heartbeat is not None and _current_heartbeat.is_running
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_current_heartbeat() -> Optional[TokenRefreshHeartbeat]:
|
|
210
|
+
"""Get the currently running heartbeat instance, if any."""
|
|
211
|
+
return _current_heartbeat
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def force_token_refresh() -> bool:
|
|
215
|
+
"""Force an immediate token refresh.
|
|
216
|
+
|
|
217
|
+
This can be called from anywhere to trigger a token refresh,
|
|
218
|
+
regardless of whether a heartbeat is running.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if refresh was successful, False otherwise.
|
|
222
|
+
"""
|
|
223
|
+
global _last_refresh_time
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
from .utils import refresh_access_token
|
|
227
|
+
|
|
228
|
+
logger.info("Forcing token refresh")
|
|
229
|
+
refreshed_token = refresh_access_token(force=True)
|
|
230
|
+
|
|
231
|
+
if refreshed_token:
|
|
232
|
+
async with _heartbeat_lock:
|
|
233
|
+
_last_refresh_time = time.time()
|
|
234
|
+
logger.info("Force refresh successful")
|
|
235
|
+
return True
|
|
236
|
+
else:
|
|
237
|
+
logger.warning("Force refresh returned None")
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
logger.error("Force refresh error: %s", exc)
|
|
242
|
+
return False
|
|
@@ -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"
|