ripperdoc 0.2.6__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.
- ripperdoc/__init__.py +3 -0
- ripperdoc/__main__.py +20 -0
- ripperdoc/cli/__init__.py +1 -0
- ripperdoc/cli/cli.py +405 -0
- ripperdoc/cli/commands/__init__.py +82 -0
- ripperdoc/cli/commands/agents_cmd.py +263 -0
- ripperdoc/cli/commands/base.py +19 -0
- ripperdoc/cli/commands/clear_cmd.py +18 -0
- ripperdoc/cli/commands/compact_cmd.py +23 -0
- ripperdoc/cli/commands/config_cmd.py +31 -0
- ripperdoc/cli/commands/context_cmd.py +144 -0
- ripperdoc/cli/commands/cost_cmd.py +82 -0
- ripperdoc/cli/commands/doctor_cmd.py +221 -0
- ripperdoc/cli/commands/exit_cmd.py +19 -0
- ripperdoc/cli/commands/help_cmd.py +20 -0
- ripperdoc/cli/commands/mcp_cmd.py +70 -0
- ripperdoc/cli/commands/memory_cmd.py +202 -0
- ripperdoc/cli/commands/models_cmd.py +413 -0
- ripperdoc/cli/commands/permissions_cmd.py +302 -0
- ripperdoc/cli/commands/resume_cmd.py +98 -0
- ripperdoc/cli/commands/status_cmd.py +167 -0
- ripperdoc/cli/commands/tasks_cmd.py +278 -0
- ripperdoc/cli/commands/todos_cmd.py +69 -0
- ripperdoc/cli/commands/tools_cmd.py +19 -0
- ripperdoc/cli/ui/__init__.py +1 -0
- ripperdoc/cli/ui/context_display.py +298 -0
- ripperdoc/cli/ui/helpers.py +22 -0
- ripperdoc/cli/ui/rich_ui.py +1557 -0
- ripperdoc/cli/ui/spinner.py +49 -0
- ripperdoc/cli/ui/thinking_spinner.py +128 -0
- ripperdoc/cli/ui/tool_renderers.py +298 -0
- ripperdoc/core/__init__.py +1 -0
- ripperdoc/core/agents.py +486 -0
- ripperdoc/core/commands.py +33 -0
- ripperdoc/core/config.py +559 -0
- ripperdoc/core/default_tools.py +88 -0
- ripperdoc/core/permissions.py +252 -0
- ripperdoc/core/providers/__init__.py +47 -0
- ripperdoc/core/providers/anthropic.py +250 -0
- ripperdoc/core/providers/base.py +265 -0
- ripperdoc/core/providers/gemini.py +615 -0
- ripperdoc/core/providers/openai.py +487 -0
- ripperdoc/core/query.py +1058 -0
- ripperdoc/core/query_utils.py +622 -0
- ripperdoc/core/skills.py +295 -0
- ripperdoc/core/system_prompt.py +431 -0
- ripperdoc/core/tool.py +240 -0
- ripperdoc/sdk/__init__.py +9 -0
- ripperdoc/sdk/client.py +333 -0
- ripperdoc/tools/__init__.py +1 -0
- ripperdoc/tools/ask_user_question_tool.py +431 -0
- ripperdoc/tools/background_shell.py +389 -0
- ripperdoc/tools/bash_output_tool.py +98 -0
- ripperdoc/tools/bash_tool.py +1016 -0
- ripperdoc/tools/dynamic_mcp_tool.py +428 -0
- ripperdoc/tools/enter_plan_mode_tool.py +226 -0
- ripperdoc/tools/exit_plan_mode_tool.py +153 -0
- ripperdoc/tools/file_edit_tool.py +346 -0
- ripperdoc/tools/file_read_tool.py +203 -0
- ripperdoc/tools/file_write_tool.py +205 -0
- ripperdoc/tools/glob_tool.py +179 -0
- ripperdoc/tools/grep_tool.py +370 -0
- ripperdoc/tools/kill_bash_tool.py +136 -0
- ripperdoc/tools/ls_tool.py +471 -0
- ripperdoc/tools/mcp_tools.py +591 -0
- ripperdoc/tools/multi_edit_tool.py +456 -0
- ripperdoc/tools/notebook_edit_tool.py +386 -0
- ripperdoc/tools/skill_tool.py +205 -0
- ripperdoc/tools/task_tool.py +379 -0
- ripperdoc/tools/todo_tool.py +494 -0
- ripperdoc/tools/tool_search_tool.py +380 -0
- ripperdoc/utils/__init__.py +1 -0
- ripperdoc/utils/bash_constants.py +51 -0
- ripperdoc/utils/bash_output_utils.py +43 -0
- ripperdoc/utils/coerce.py +34 -0
- ripperdoc/utils/context_length_errors.py +252 -0
- ripperdoc/utils/exit_code_handlers.py +241 -0
- ripperdoc/utils/file_watch.py +135 -0
- ripperdoc/utils/git_utils.py +274 -0
- ripperdoc/utils/json_utils.py +27 -0
- ripperdoc/utils/log.py +176 -0
- ripperdoc/utils/mcp.py +560 -0
- ripperdoc/utils/memory.py +253 -0
- ripperdoc/utils/message_compaction.py +676 -0
- ripperdoc/utils/messages.py +519 -0
- ripperdoc/utils/output_utils.py +258 -0
- ripperdoc/utils/path_ignore.py +677 -0
- ripperdoc/utils/path_utils.py +46 -0
- ripperdoc/utils/permissions/__init__.py +27 -0
- ripperdoc/utils/permissions/path_validation_utils.py +174 -0
- ripperdoc/utils/permissions/shell_command_validation.py +552 -0
- ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
- ripperdoc/utils/prompt.py +17 -0
- ripperdoc/utils/safe_get_cwd.py +31 -0
- ripperdoc/utils/sandbox_utils.py +38 -0
- ripperdoc/utils/session_history.py +260 -0
- ripperdoc/utils/session_usage.py +117 -0
- ripperdoc/utils/shell_token_utils.py +95 -0
- ripperdoc/utils/shell_utils.py +159 -0
- ripperdoc/utils/todo.py +203 -0
- ripperdoc/utils/token_estimation.py +34 -0
- ripperdoc-0.2.6.dist-info/METADATA +193 -0
- ripperdoc-0.2.6.dist-info/RECORD +107 -0
- ripperdoc-0.2.6.dist-info/WHEEL +5 -0
- ripperdoc-0.2.6.dist-info/entry_points.txt +3 -0
- ripperdoc-0.2.6.dist-info/licenses/LICENSE +53 -0
- ripperdoc-0.2.6.dist-info/top_level.txt +1 -0
ripperdoc/__init__.py
ADDED
ripperdoc/__main__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ripperdoc - AI-Powered Coding Agent
|
|
3
|
+
|
|
4
|
+
A Python implementation of an AI coding assistant.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Execute bash commands
|
|
8
|
+
- Read and edit files
|
|
9
|
+
- Search code with glob and grep
|
|
10
|
+
- AI-powered code assistance
|
|
11
|
+
- Multi-model support
|
|
12
|
+
|
|
13
|
+
Quick Start:
|
|
14
|
+
pip install -e .
|
|
15
|
+
ripperdoc -p "your prompt here"
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from ripperdoc import __version__
|
|
19
|
+
|
|
20
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI interface for Ripperdoc."""
|
ripperdoc/cli/cli.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""Main CLI entry point for Ripperdoc.
|
|
2
|
+
|
|
3
|
+
This module provides the command-line interface for the Ripperdoc agent.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import click
|
|
8
|
+
import sys
|
|
9
|
+
import uuid
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from ripperdoc import __version__
|
|
14
|
+
from ripperdoc.core.config import (
|
|
15
|
+
get_global_config,
|
|
16
|
+
save_global_config,
|
|
17
|
+
get_project_config,
|
|
18
|
+
ModelProfile,
|
|
19
|
+
ProviderType,
|
|
20
|
+
)
|
|
21
|
+
from ripperdoc.core.default_tools import get_default_tools
|
|
22
|
+
from ripperdoc.core.query import query, QueryContext
|
|
23
|
+
from ripperdoc.core.system_prompt import build_system_prompt
|
|
24
|
+
from ripperdoc.core.skills import build_skill_summary, load_all_skills
|
|
25
|
+
from ripperdoc.utils.messages import create_user_message
|
|
26
|
+
from ripperdoc.utils.memory import build_memory_instructions
|
|
27
|
+
from ripperdoc.core.permissions import make_permission_checker
|
|
28
|
+
from ripperdoc.utils.mcp import (
|
|
29
|
+
load_mcp_servers_async,
|
|
30
|
+
format_mcp_instructions,
|
|
31
|
+
shutdown_mcp_runtime,
|
|
32
|
+
)
|
|
33
|
+
from ripperdoc.tools.mcp_tools import load_dynamic_mcp_tools_async, merge_tools_with_dynamic
|
|
34
|
+
from ripperdoc.utils.log import enable_session_file_logging, get_logger
|
|
35
|
+
from ripperdoc.utils.prompt import prompt_secret
|
|
36
|
+
|
|
37
|
+
from rich.console import Console
|
|
38
|
+
from rich.markdown import Markdown
|
|
39
|
+
from rich.panel import Panel
|
|
40
|
+
from rich.markup import escape
|
|
41
|
+
|
|
42
|
+
console = Console()
|
|
43
|
+
logger = get_logger()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def run_query(
|
|
47
|
+
prompt: str,
|
|
48
|
+
tools: list,
|
|
49
|
+
safe_mode: bool = False,
|
|
50
|
+
verbose: bool = False,
|
|
51
|
+
session_id: Optional[str] = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Run a single query and print the response."""
|
|
54
|
+
|
|
55
|
+
logger.info(
|
|
56
|
+
"[cli] Running single prompt session",
|
|
57
|
+
extra={
|
|
58
|
+
"safe_mode": safe_mode,
|
|
59
|
+
"verbose": verbose,
|
|
60
|
+
"session_id": session_id,
|
|
61
|
+
"prompt_length": len(prompt),
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
if prompt:
|
|
65
|
+
logger.debug(
|
|
66
|
+
"[cli] Prompt preview",
|
|
67
|
+
extra={"session_id": session_id, "prompt_preview": prompt[:200]},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
project_path = Path.cwd()
|
|
71
|
+
can_use_tool = make_permission_checker(project_path, safe_mode) if safe_mode else None
|
|
72
|
+
|
|
73
|
+
# Create initial user message
|
|
74
|
+
from ripperdoc.utils.messages import UserMessage, AssistantMessage, ProgressMessage
|
|
75
|
+
|
|
76
|
+
messages: List[UserMessage | AssistantMessage | ProgressMessage] = [create_user_message(prompt)]
|
|
77
|
+
|
|
78
|
+
# Create query context
|
|
79
|
+
query_context = QueryContext(tools=tools, safe_mode=safe_mode, verbose=verbose)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
context: Dict[str, Any] = {}
|
|
83
|
+
# System prompt
|
|
84
|
+
servers = await load_mcp_servers_async(Path.cwd())
|
|
85
|
+
dynamic_tools = await load_dynamic_mcp_tools_async(Path.cwd())
|
|
86
|
+
if dynamic_tools:
|
|
87
|
+
tools = merge_tools_with_dynamic(tools, dynamic_tools)
|
|
88
|
+
query_context.tools = tools
|
|
89
|
+
mcp_instructions = format_mcp_instructions(servers)
|
|
90
|
+
skill_result = load_all_skills(Path.cwd())
|
|
91
|
+
for err in skill_result.errors:
|
|
92
|
+
logger.warning(
|
|
93
|
+
"[skills] Failed to load skill",
|
|
94
|
+
extra={"path": str(err.path), "reason": err.reason},
|
|
95
|
+
)
|
|
96
|
+
skill_instructions = build_skill_summary(skill_result.skills)
|
|
97
|
+
additional_instructions: List[str] = []
|
|
98
|
+
if skill_instructions:
|
|
99
|
+
additional_instructions.append(skill_instructions)
|
|
100
|
+
memory_instructions = build_memory_instructions()
|
|
101
|
+
if memory_instructions:
|
|
102
|
+
additional_instructions.append(memory_instructions)
|
|
103
|
+
system_prompt = build_system_prompt(
|
|
104
|
+
tools,
|
|
105
|
+
prompt,
|
|
106
|
+
context,
|
|
107
|
+
additional_instructions=additional_instructions or None,
|
|
108
|
+
mcp_instructions=mcp_instructions,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Run the query
|
|
112
|
+
try:
|
|
113
|
+
async for message in query(
|
|
114
|
+
messages, system_prompt, context, query_context, can_use_tool
|
|
115
|
+
):
|
|
116
|
+
if message.type == "assistant" and hasattr(message, "message"):
|
|
117
|
+
# Print assistant message
|
|
118
|
+
if isinstance(message.message.content, str):
|
|
119
|
+
console.print(
|
|
120
|
+
Panel(
|
|
121
|
+
Markdown(message.message.content),
|
|
122
|
+
title="Ripperdoc",
|
|
123
|
+
border_style="cyan",
|
|
124
|
+
padding=(0, 1),
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
# Handle structured content
|
|
129
|
+
for block in message.message.content:
|
|
130
|
+
if isinstance(block, dict):
|
|
131
|
+
if block.get("type") == "text":
|
|
132
|
+
console.print(
|
|
133
|
+
Panel(
|
|
134
|
+
Markdown(block["text"]),
|
|
135
|
+
title="Ripperdoc",
|
|
136
|
+
border_style="cyan",
|
|
137
|
+
padding=(0, 1),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
if hasattr(block, "type") and block.type == "text":
|
|
142
|
+
console.print(
|
|
143
|
+
Panel(
|
|
144
|
+
Markdown(block.text or ""),
|
|
145
|
+
title="Ripperdoc",
|
|
146
|
+
border_style="cyan",
|
|
147
|
+
padding=(0, 1),
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
elif message.type == "progress" and hasattr(message, "content"):
|
|
152
|
+
# Print progress
|
|
153
|
+
if verbose:
|
|
154
|
+
console.print(f"[dim]Progress: {escape(str(message.content))}[/dim]")
|
|
155
|
+
|
|
156
|
+
# Add message to history
|
|
157
|
+
messages.append(message) # type: ignore[arg-type]
|
|
158
|
+
|
|
159
|
+
except KeyboardInterrupt:
|
|
160
|
+
console.print("\n[yellow]Interrupted by user[/yellow]")
|
|
161
|
+
except asyncio.CancelledError:
|
|
162
|
+
console.print("\n[yellow]Operation cancelled[/yellow]")
|
|
163
|
+
except (RuntimeError, ValueError, TypeError, OSError, IOError, ConnectionError) as e:
|
|
164
|
+
console.print(f"[red]Error: {escape(str(e))}[/red]")
|
|
165
|
+
logger.warning(
|
|
166
|
+
"[cli] Unhandled error while running prompt: %s: %s",
|
|
167
|
+
type(e).__name__, e,
|
|
168
|
+
extra={"session_id": session_id},
|
|
169
|
+
)
|
|
170
|
+
if verbose:
|
|
171
|
+
import traceback
|
|
172
|
+
|
|
173
|
+
console.print(traceback.format_exc(), markup=False)
|
|
174
|
+
logger.info(
|
|
175
|
+
"[cli] Prompt session completed",
|
|
176
|
+
extra={"session_id": session_id, "message_count": len(messages)},
|
|
177
|
+
)
|
|
178
|
+
finally:
|
|
179
|
+
await shutdown_mcp_runtime()
|
|
180
|
+
logger.debug("[cli] Shutdown MCP runtime", extra={"session_id": session_id})
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def check_onboarding() -> bool:
|
|
184
|
+
"""Check if onboarding is complete and run if needed."""
|
|
185
|
+
config = get_global_config()
|
|
186
|
+
|
|
187
|
+
if config.has_completed_onboarding:
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
console.print("[bold cyan]Welcome to Ripperdoc![/bold cyan]\n")
|
|
191
|
+
console.print("Let's set up your AI model configuration.\n")
|
|
192
|
+
|
|
193
|
+
# Simple onboarding
|
|
194
|
+
provider_choices = [
|
|
195
|
+
*[p.value for p in ProviderType],
|
|
196
|
+
"openai",
|
|
197
|
+
"deepseek",
|
|
198
|
+
"mistral",
|
|
199
|
+
"kimi",
|
|
200
|
+
"qwen",
|
|
201
|
+
"glm",
|
|
202
|
+
"custom",
|
|
203
|
+
]
|
|
204
|
+
provider_choice = click.prompt(
|
|
205
|
+
"Choose your model protocol",
|
|
206
|
+
type=click.Choice(provider_choices),
|
|
207
|
+
default=ProviderType.ANTHROPIC.value,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
api_base = None
|
|
211
|
+
if provider_choice == "custom":
|
|
212
|
+
provider_choice = click.prompt(
|
|
213
|
+
"Protocol family (for API compatibility)",
|
|
214
|
+
type=click.Choice([p.value for p in ProviderType]),
|
|
215
|
+
default=ProviderType.OPENAI_COMPATIBLE.value,
|
|
216
|
+
)
|
|
217
|
+
api_base = click.prompt("API Base URL")
|
|
218
|
+
|
|
219
|
+
api_key = ""
|
|
220
|
+
while not api_key:
|
|
221
|
+
api_key = prompt_secret("Enter your API key").strip()
|
|
222
|
+
if not api_key:
|
|
223
|
+
console.print("[red]API key is required.[/red]")
|
|
224
|
+
|
|
225
|
+
provider = ProviderType(provider_choice)
|
|
226
|
+
|
|
227
|
+
# Get model name
|
|
228
|
+
if provider == ProviderType.ANTHROPIC:
|
|
229
|
+
model = click.prompt("Model name", default="claude-3-5-sonnet-20241022")
|
|
230
|
+
elif provider == ProviderType.OPENAI_COMPATIBLE:
|
|
231
|
+
default_model = "gpt-4o-mini"
|
|
232
|
+
if provider_choice == "deepseek":
|
|
233
|
+
default_model = "deepseek-chat"
|
|
234
|
+
api_base = api_base or "https://api.deepseek.com"
|
|
235
|
+
model = click.prompt("Model name", default=default_model)
|
|
236
|
+
if api_base is None:
|
|
237
|
+
api_base = (
|
|
238
|
+
click.prompt("API base URL (optional)", default="", show_default=False) or None
|
|
239
|
+
)
|
|
240
|
+
elif provider == ProviderType.GEMINI:
|
|
241
|
+
console.print(
|
|
242
|
+
"[yellow]Gemini protocol support is not yet available; configuration is saved for "
|
|
243
|
+
"future support.[/yellow]"
|
|
244
|
+
)
|
|
245
|
+
model = click.prompt("Model name", default="gemini-1.5-pro")
|
|
246
|
+
if api_base is None:
|
|
247
|
+
api_base = (
|
|
248
|
+
click.prompt("API base URL (optional)", default="", show_default=False) or None
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
model = click.prompt("Model name")
|
|
252
|
+
|
|
253
|
+
context_window_input = click.prompt(
|
|
254
|
+
"Context window in tokens (optional, press Enter to skip)", default="", show_default=False
|
|
255
|
+
)
|
|
256
|
+
context_window = None
|
|
257
|
+
if context_window_input.strip():
|
|
258
|
+
try:
|
|
259
|
+
context_window = int(context_window_input.strip())
|
|
260
|
+
except ValueError:
|
|
261
|
+
console.print("[yellow]Invalid context window, using auto-detected defaults.[/yellow]")
|
|
262
|
+
|
|
263
|
+
# Create model profile
|
|
264
|
+
config.model_profiles["default"] = ModelProfile(
|
|
265
|
+
provider=provider,
|
|
266
|
+
model=model,
|
|
267
|
+
api_key=api_key,
|
|
268
|
+
api_base=api_base,
|
|
269
|
+
context_window=context_window,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
config.has_completed_onboarding = True
|
|
273
|
+
config.last_onboarding_version = __version__
|
|
274
|
+
|
|
275
|
+
save_global_config(config)
|
|
276
|
+
|
|
277
|
+
console.print("\n[green]✓ Configuration saved![/green]\n")
|
|
278
|
+
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@click.group(invoke_without_command=True)
|
|
283
|
+
@click.version_option(version=__version__)
|
|
284
|
+
@click.option("--cwd", type=click.Path(exists=True), help="Working directory")
|
|
285
|
+
@click.option(
|
|
286
|
+
"--yolo",
|
|
287
|
+
is_flag=True,
|
|
288
|
+
help="YOLO mode: skip all permission prompts for tools",
|
|
289
|
+
)
|
|
290
|
+
@click.option("--verbose", is_flag=True, help="Verbose output")
|
|
291
|
+
@click.option("-p", "--prompt", type=str, help="Direct prompt (non-interactive)")
|
|
292
|
+
@click.pass_context
|
|
293
|
+
def cli(
|
|
294
|
+
ctx: click.Context, cwd: Optional[str], yolo: bool, verbose: bool, prompt: Optional[str]
|
|
295
|
+
) -> None:
|
|
296
|
+
"""Ripperdoc - AI-powered coding agent"""
|
|
297
|
+
session_id = str(uuid.uuid4())
|
|
298
|
+
|
|
299
|
+
# Set working directory
|
|
300
|
+
if cwd:
|
|
301
|
+
import os
|
|
302
|
+
|
|
303
|
+
os.chdir(cwd)
|
|
304
|
+
logger.debug(
|
|
305
|
+
"[cli] Changed working directory via --cwd",
|
|
306
|
+
extra={"cwd": cwd, "session_id": session_id},
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
project_path = Path.cwd()
|
|
310
|
+
log_file = enable_session_file_logging(project_path, session_id)
|
|
311
|
+
logger.info(
|
|
312
|
+
"[cli] Starting CLI invocation",
|
|
313
|
+
extra={
|
|
314
|
+
"session_id": session_id,
|
|
315
|
+
"project_path": str(project_path),
|
|
316
|
+
"log_file": str(log_file),
|
|
317
|
+
"prompt_mode": bool(prompt),
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Ensure onboarding is complete
|
|
322
|
+
if not check_onboarding():
|
|
323
|
+
logger.info(
|
|
324
|
+
"[cli] Onboarding check failed or aborted; exiting.",
|
|
325
|
+
extra={"session_id": session_id},
|
|
326
|
+
)
|
|
327
|
+
sys.exit(1)
|
|
328
|
+
|
|
329
|
+
# Initialize project configuration for the current working directory
|
|
330
|
+
get_project_config(project_path)
|
|
331
|
+
|
|
332
|
+
safe_mode = not yolo
|
|
333
|
+
logger.debug(
|
|
334
|
+
"[cli] Configuration initialized",
|
|
335
|
+
extra={"session_id": session_id, "safe_mode": safe_mode, "verbose": verbose},
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# If prompt is provided, run directly
|
|
339
|
+
if prompt:
|
|
340
|
+
tools = get_default_tools()
|
|
341
|
+
asyncio.run(run_query(prompt, tools, safe_mode, verbose, session_id=session_id))
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
# If no command specified, start interactive REPL with Rich interface
|
|
345
|
+
if ctx.invoked_subcommand is None:
|
|
346
|
+
# Use Rich interface by default
|
|
347
|
+
from ripperdoc.cli.ui.rich_ui import main_rich
|
|
348
|
+
|
|
349
|
+
main_rich(
|
|
350
|
+
safe_mode=safe_mode,
|
|
351
|
+
verbose=verbose,
|
|
352
|
+
session_id=session_id,
|
|
353
|
+
log_file_path=log_file,
|
|
354
|
+
)
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@cli.command(name="config")
|
|
359
|
+
def config_cmd() -> None:
|
|
360
|
+
"""Show current configuration"""
|
|
361
|
+
config = get_global_config()
|
|
362
|
+
|
|
363
|
+
console.print("\n[bold]Global Configuration[/bold]\n")
|
|
364
|
+
console.print(f"Version: {__version__}")
|
|
365
|
+
console.print(f"Onboarding Complete: {config.has_completed_onboarding}")
|
|
366
|
+
console.print(f"Theme: {config.theme}")
|
|
367
|
+
console.print(f"Verbose: {config.verbose}")
|
|
368
|
+
console.print(f"Safe Mode: {config.safe_mode}\n")
|
|
369
|
+
|
|
370
|
+
if config.model_profiles:
|
|
371
|
+
console.print("[bold]Model Profiles:[/bold]")
|
|
372
|
+
for name, profile in config.model_profiles.items():
|
|
373
|
+
console.print(f" {name}:")
|
|
374
|
+
console.print(f" Provider: {profile.provider}")
|
|
375
|
+
console.print(f" Model: {profile.model}")
|
|
376
|
+
console.print(f" API Key: {'***' if profile.api_key else 'Not set'}")
|
|
377
|
+
console.print()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@cli.command(name="version")
|
|
381
|
+
def version_cmd() -> None:
|
|
382
|
+
"""Show version information"""
|
|
383
|
+
console.print(f"Ripperdoc version {__version__}")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def main() -> None:
|
|
387
|
+
"""Main entry point."""
|
|
388
|
+
try:
|
|
389
|
+
cli()
|
|
390
|
+
except KeyboardInterrupt:
|
|
391
|
+
console.print("\n[yellow]Interrupted[/yellow]")
|
|
392
|
+
sys.exit(130)
|
|
393
|
+
except SystemExit:
|
|
394
|
+
raise
|
|
395
|
+
except (RuntimeError, ValueError, TypeError, OSError, IOError, ConnectionError, click.ClickException) as e:
|
|
396
|
+
console.print(f"[red]Fatal error: {escape(str(e))}[/red]")
|
|
397
|
+
logger.warning(
|
|
398
|
+
"[cli] Fatal error in main CLI entrypoint: %s: %s",
|
|
399
|
+
type(e).__name__, e,
|
|
400
|
+
)
|
|
401
|
+
sys.exit(1)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
if __name__ == "__main__":
|
|
405
|
+
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Slash command registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
|
|
7
|
+
from .base import SlashCommand
|
|
8
|
+
from .agents_cmd import command as agents_command
|
|
9
|
+
from .clear_cmd import command as clear_command
|
|
10
|
+
from .compact_cmd import command as compact_command
|
|
11
|
+
from .config_cmd import command as config_command
|
|
12
|
+
from .cost_cmd import command as cost_command
|
|
13
|
+
from .context_cmd import command as context_command
|
|
14
|
+
from .doctor_cmd import command as doctor_command
|
|
15
|
+
from .exit_cmd import command as exit_command
|
|
16
|
+
from .help_cmd import command as help_command
|
|
17
|
+
from .memory_cmd import command as memory_command
|
|
18
|
+
from .mcp_cmd import command as mcp_command
|
|
19
|
+
from .models_cmd import command as models_command
|
|
20
|
+
from .permissions_cmd import command as permissions_command
|
|
21
|
+
from .resume_cmd import command as resume_command
|
|
22
|
+
from .tasks_cmd import command as tasks_command
|
|
23
|
+
from .status_cmd import command as status_command
|
|
24
|
+
from .todos_cmd import command as todos_command
|
|
25
|
+
from .tools_cmd import command as tools_command
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _build_registry(commands: List[SlashCommand]) -> Dict[str, SlashCommand]:
|
|
29
|
+
"""Map command names and aliases to SlashCommand entries."""
|
|
30
|
+
registry: Dict[str, SlashCommand] = {}
|
|
31
|
+
for cmd in commands:
|
|
32
|
+
registry[cmd.name] = cmd
|
|
33
|
+
for alias in cmd.aliases:
|
|
34
|
+
registry[alias] = cmd
|
|
35
|
+
return registry
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
ALL_COMMANDS: List[SlashCommand] = [
|
|
39
|
+
help_command,
|
|
40
|
+
clear_command,
|
|
41
|
+
config_command,
|
|
42
|
+
tools_command,
|
|
43
|
+
models_command,
|
|
44
|
+
exit_command,
|
|
45
|
+
status_command,
|
|
46
|
+
doctor_command,
|
|
47
|
+
memory_command,
|
|
48
|
+
permissions_command,
|
|
49
|
+
tasks_command,
|
|
50
|
+
todos_command,
|
|
51
|
+
mcp_command,
|
|
52
|
+
cost_command,
|
|
53
|
+
context_command,
|
|
54
|
+
compact_command,
|
|
55
|
+
resume_command,
|
|
56
|
+
agents_command,
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
COMMAND_REGISTRY: Dict[str, SlashCommand] = _build_registry(ALL_COMMANDS)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def list_slash_commands() -> List[SlashCommand]:
|
|
63
|
+
"""Return the ordered list of base slash commands (no aliases)."""
|
|
64
|
+
return ALL_COMMANDS
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_slash_command(name: str) -> SlashCommand | None:
|
|
68
|
+
"""Return a command by name or alias."""
|
|
69
|
+
return COMMAND_REGISTRY.get(name)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def slash_command_completions() -> List[tuple[str, SlashCommand]]:
|
|
73
|
+
"""Return (name, command) pairs for completion including aliases."""
|
|
74
|
+
return list(COMMAND_REGISTRY.items())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"SlashCommand",
|
|
79
|
+
"list_slash_commands",
|
|
80
|
+
"get_slash_command",
|
|
81
|
+
"slash_command_completions",
|
|
82
|
+
]
|