ripperdoc 0.1.0__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 +25 -0
- ripperdoc/cli/__init__.py +1 -0
- ripperdoc/cli/cli.py +317 -0
- ripperdoc/cli/commands/__init__.py +76 -0
- ripperdoc/cli/commands/agents_cmd.py +234 -0
- ripperdoc/cli/commands/base.py +19 -0
- ripperdoc/cli/commands/clear_cmd.py +18 -0
- ripperdoc/cli/commands/compact_cmd.py +19 -0
- ripperdoc/cli/commands/config_cmd.py +31 -0
- ripperdoc/cli/commands/context_cmd.py +114 -0
- ripperdoc/cli/commands/cost_cmd.py +77 -0
- ripperdoc/cli/commands/exit_cmd.py +19 -0
- ripperdoc/cli/commands/help_cmd.py +20 -0
- ripperdoc/cli/commands/mcp_cmd.py +65 -0
- ripperdoc/cli/commands/models_cmd.py +327 -0
- ripperdoc/cli/commands/resume_cmd.py +97 -0
- ripperdoc/cli/commands/status_cmd.py +167 -0
- ripperdoc/cli/commands/tasks_cmd.py +240 -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 +297 -0
- ripperdoc/cli/ui/helpers.py +22 -0
- ripperdoc/cli/ui/rich_ui.py +1010 -0
- ripperdoc/cli/ui/spinner.py +50 -0
- ripperdoc/core/__init__.py +1 -0
- ripperdoc/core/agents.py +306 -0
- ripperdoc/core/commands.py +33 -0
- ripperdoc/core/config.py +382 -0
- ripperdoc/core/default_tools.py +57 -0
- ripperdoc/core/permissions.py +227 -0
- ripperdoc/core/query.py +682 -0
- ripperdoc/core/system_prompt.py +418 -0
- ripperdoc/core/tool.py +214 -0
- ripperdoc/sdk/__init__.py +9 -0
- ripperdoc/sdk/client.py +309 -0
- ripperdoc/tools/__init__.py +1 -0
- ripperdoc/tools/background_shell.py +291 -0
- ripperdoc/tools/bash_output_tool.py +98 -0
- ripperdoc/tools/bash_tool.py +822 -0
- ripperdoc/tools/file_edit_tool.py +281 -0
- ripperdoc/tools/file_read_tool.py +168 -0
- ripperdoc/tools/file_write_tool.py +141 -0
- ripperdoc/tools/glob_tool.py +134 -0
- ripperdoc/tools/grep_tool.py +232 -0
- ripperdoc/tools/kill_bash_tool.py +136 -0
- ripperdoc/tools/ls_tool.py +298 -0
- ripperdoc/tools/mcp_tools.py +804 -0
- ripperdoc/tools/multi_edit_tool.py +393 -0
- ripperdoc/tools/notebook_edit_tool.py +325 -0
- ripperdoc/tools/task_tool.py +282 -0
- ripperdoc/tools/todo_tool.py +362 -0
- ripperdoc/tools/tool_search_tool.py +366 -0
- ripperdoc/utils/__init__.py +1 -0
- ripperdoc/utils/bash_constants.py +51 -0
- ripperdoc/utils/bash_output_utils.py +43 -0
- ripperdoc/utils/exit_code_handlers.py +241 -0
- ripperdoc/utils/log.py +76 -0
- ripperdoc/utils/mcp.py +427 -0
- ripperdoc/utils/memory.py +239 -0
- ripperdoc/utils/message_compaction.py +640 -0
- ripperdoc/utils/messages.py +399 -0
- ripperdoc/utils/output_utils.py +233 -0
- ripperdoc/utils/path_utils.py +46 -0
- ripperdoc/utils/permissions/__init__.py +21 -0
- ripperdoc/utils/permissions/path_validation_utils.py +165 -0
- ripperdoc/utils/permissions/shell_command_validation.py +74 -0
- ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
- ripperdoc/utils/safe_get_cwd.py +24 -0
- ripperdoc/utils/sandbox_utils.py +38 -0
- ripperdoc/utils/session_history.py +223 -0
- ripperdoc/utils/session_usage.py +110 -0
- ripperdoc/utils/shell_token_utils.py +95 -0
- ripperdoc/utils/todo.py +199 -0
- ripperdoc-0.1.0.dist-info/METADATA +178 -0
- ripperdoc-0.1.0.dist-info/RECORD +81 -0
- ripperdoc-0.1.0.dist-info/WHEEL +5 -0
- ripperdoc-0.1.0.dist-info/entry_points.txt +3 -0
- ripperdoc-0.1.0.dist-info/licenses/LICENSE +53 -0
- ripperdoc-0.1.0.dist-info/top_level.txt +1 -0
ripperdoc/__init__.py
ADDED
ripperdoc/__main__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
For more information:
|
|
18
|
+
- README.md: Project overview
|
|
19
|
+
- QUICKSTART.md: Quick start guide
|
|
20
|
+
- DEVELOPMENT.md: Development guide
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from ripperdoc import __version__
|
|
24
|
+
|
|
25
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI interface for Ripperdoc."""
|
ripperdoc/cli/cli.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
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
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from ripperdoc import __version__
|
|
13
|
+
from ripperdoc.core.config import (
|
|
14
|
+
get_global_config,
|
|
15
|
+
save_global_config,
|
|
16
|
+
get_project_config,
|
|
17
|
+
ModelProfile,
|
|
18
|
+
ProviderType,
|
|
19
|
+
)
|
|
20
|
+
from ripperdoc.core.default_tools import get_default_tools
|
|
21
|
+
from ripperdoc.core.query import query, QueryContext
|
|
22
|
+
from ripperdoc.core.system_prompt import build_system_prompt
|
|
23
|
+
from ripperdoc.utils.messages import create_user_message
|
|
24
|
+
from ripperdoc.utils.memory import build_memory_instructions
|
|
25
|
+
from ripperdoc.core.permissions import make_permission_checker
|
|
26
|
+
from ripperdoc.utils.mcp import (
|
|
27
|
+
load_mcp_servers_async,
|
|
28
|
+
format_mcp_instructions,
|
|
29
|
+
shutdown_mcp_runtime,
|
|
30
|
+
)
|
|
31
|
+
from ripperdoc.tools.mcp_tools import load_dynamic_mcp_tools_async, merge_tools_with_dynamic
|
|
32
|
+
|
|
33
|
+
from rich.console import Console
|
|
34
|
+
from rich.markdown import Markdown
|
|
35
|
+
from rich.panel import Panel
|
|
36
|
+
from rich.markup import escape
|
|
37
|
+
|
|
38
|
+
console = Console()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def run_query(
|
|
42
|
+
prompt: str, tools: list, safe_mode: bool = False, verbose: bool = False
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Run a single query and print the response."""
|
|
45
|
+
|
|
46
|
+
project_path = Path.cwd()
|
|
47
|
+
can_use_tool = make_permission_checker(project_path, safe_mode) if safe_mode else None
|
|
48
|
+
|
|
49
|
+
# Create initial user message
|
|
50
|
+
messages = [create_user_message(prompt)]
|
|
51
|
+
|
|
52
|
+
# Create query context
|
|
53
|
+
query_context = QueryContext(tools=tools, safe_mode=safe_mode, verbose=verbose)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
context = {}
|
|
57
|
+
# System prompt
|
|
58
|
+
servers = await load_mcp_servers_async(Path.cwd())
|
|
59
|
+
dynamic_tools = await load_dynamic_mcp_tools_async(Path.cwd())
|
|
60
|
+
if dynamic_tools:
|
|
61
|
+
tools = merge_tools_with_dynamic(tools, dynamic_tools)
|
|
62
|
+
query_context.tools = tools
|
|
63
|
+
mcp_instructions = format_mcp_instructions(servers)
|
|
64
|
+
base_system_prompt = build_system_prompt(
|
|
65
|
+
tools,
|
|
66
|
+
prompt,
|
|
67
|
+
context,
|
|
68
|
+
mcp_instructions=mcp_instructions,
|
|
69
|
+
)
|
|
70
|
+
memory_instructions = build_memory_instructions()
|
|
71
|
+
system_prompt = (
|
|
72
|
+
f"{base_system_prompt}\n\n{memory_instructions}"
|
|
73
|
+
if memory_instructions
|
|
74
|
+
else base_system_prompt
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Run the query
|
|
78
|
+
try:
|
|
79
|
+
async for message in query(
|
|
80
|
+
messages, system_prompt, context, query_context, can_use_tool
|
|
81
|
+
):
|
|
82
|
+
if message.type == "assistant":
|
|
83
|
+
# Print assistant message
|
|
84
|
+
if isinstance(message.message.content, str):
|
|
85
|
+
console.print(
|
|
86
|
+
Panel(
|
|
87
|
+
Markdown(message.message.content),
|
|
88
|
+
title="Ripperdoc",
|
|
89
|
+
border_style="cyan",
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
# Handle structured content
|
|
94
|
+
for block in message.message.content:
|
|
95
|
+
if isinstance(block, dict):
|
|
96
|
+
if block.get("type") == "text":
|
|
97
|
+
console.print(
|
|
98
|
+
Panel(
|
|
99
|
+
Markdown(block["text"]),
|
|
100
|
+
title="Ripperdoc",
|
|
101
|
+
border_style="cyan",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
if hasattr(block, "type") and block.type == "text":
|
|
106
|
+
console.print(
|
|
107
|
+
Panel(
|
|
108
|
+
Markdown(block.text),
|
|
109
|
+
title="Ripperdoc",
|
|
110
|
+
border_style="cyan",
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
elif message.type == "progress":
|
|
115
|
+
# Print progress
|
|
116
|
+
if verbose:
|
|
117
|
+
console.print(f"[dim]Progress: {escape(str(message.content))}[/dim]")
|
|
118
|
+
|
|
119
|
+
# Add message to history
|
|
120
|
+
messages.append(message)
|
|
121
|
+
|
|
122
|
+
except KeyboardInterrupt:
|
|
123
|
+
console.print("\n[yellow]Interrupted by user[/yellow]")
|
|
124
|
+
except Exception as e:
|
|
125
|
+
console.print(f"[red]Error: {escape(str(e))}[/red]")
|
|
126
|
+
if verbose:
|
|
127
|
+
import traceback
|
|
128
|
+
|
|
129
|
+
console.print(traceback.format_exc(), markup=False)
|
|
130
|
+
finally:
|
|
131
|
+
await shutdown_mcp_runtime()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def check_onboarding() -> bool:
|
|
135
|
+
"""Check if onboarding is complete and run if needed."""
|
|
136
|
+
config = get_global_config()
|
|
137
|
+
|
|
138
|
+
if config.has_completed_onboarding:
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
console.print("[bold cyan]Welcome to Ripperdoc![/bold cyan]\n")
|
|
142
|
+
console.print("Let's set up your AI model configuration.\n")
|
|
143
|
+
|
|
144
|
+
# Simple onboarding
|
|
145
|
+
provider_choices = [
|
|
146
|
+
*[p.value for p in ProviderType],
|
|
147
|
+
"openai",
|
|
148
|
+
"deepseek",
|
|
149
|
+
"mistral",
|
|
150
|
+
"kimi",
|
|
151
|
+
"qwen",
|
|
152
|
+
"glm",
|
|
153
|
+
"custom",
|
|
154
|
+
]
|
|
155
|
+
provider_choice = click.prompt(
|
|
156
|
+
"Choose your model protocol",
|
|
157
|
+
type=click.Choice(provider_choices),
|
|
158
|
+
default=ProviderType.ANTHROPIC.value,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
api_base = None
|
|
162
|
+
if provider_choice == "custom":
|
|
163
|
+
provider_choice = click.prompt(
|
|
164
|
+
"Protocol family (for API compatibility)",
|
|
165
|
+
type=click.Choice([p.value for p in ProviderType]),
|
|
166
|
+
default=ProviderType.OPENAI_COMPATIBLE.value,
|
|
167
|
+
)
|
|
168
|
+
api_base = click.prompt("API Base URL")
|
|
169
|
+
|
|
170
|
+
api_key = click.prompt("Enter your API key", hide_input=True)
|
|
171
|
+
|
|
172
|
+
provider = ProviderType(provider_choice)
|
|
173
|
+
|
|
174
|
+
# Get model name
|
|
175
|
+
if provider == ProviderType.ANTHROPIC:
|
|
176
|
+
model = click.prompt("Model name", default="claude-3-5-sonnet-20241022")
|
|
177
|
+
elif provider == ProviderType.OPENAI_COMPATIBLE:
|
|
178
|
+
default_model = "gpt-4o-mini"
|
|
179
|
+
if provider_choice == "deepseek":
|
|
180
|
+
default_model = "deepseek-chat"
|
|
181
|
+
api_base = api_base or "https://api.deepseek.com"
|
|
182
|
+
model = click.prompt("Model name", default=default_model)
|
|
183
|
+
if api_base is None:
|
|
184
|
+
api_base = (
|
|
185
|
+
click.prompt("API base URL (optional)", default="", show_default=False) or None
|
|
186
|
+
)
|
|
187
|
+
elif provider == ProviderType.GEMINI:
|
|
188
|
+
console.print(
|
|
189
|
+
"[yellow]Gemini protocol support is not yet available; configuration is saved for "
|
|
190
|
+
"future support.[/yellow]"
|
|
191
|
+
)
|
|
192
|
+
model = click.prompt("Model name", default="gemini-1.5-pro")
|
|
193
|
+
if api_base is None:
|
|
194
|
+
api_base = (
|
|
195
|
+
click.prompt("API base URL (optional)", default="", show_default=False) or None
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
model = click.prompt("Model name")
|
|
199
|
+
|
|
200
|
+
context_window_input = click.prompt(
|
|
201
|
+
"Context window in tokens (optional, press Enter to skip)", default="", show_default=False
|
|
202
|
+
)
|
|
203
|
+
context_window = None
|
|
204
|
+
if context_window_input.strip():
|
|
205
|
+
try:
|
|
206
|
+
context_window = int(context_window_input.strip())
|
|
207
|
+
except ValueError:
|
|
208
|
+
console.print("[yellow]Invalid context window, using auto-detected defaults.[/yellow]")
|
|
209
|
+
|
|
210
|
+
# Create model profile
|
|
211
|
+
config.model_profiles["default"] = ModelProfile(
|
|
212
|
+
provider=provider,
|
|
213
|
+
model=model,
|
|
214
|
+
api_key=api_key,
|
|
215
|
+
api_base=api_base,
|
|
216
|
+
context_window=context_window,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
config.has_completed_onboarding = True
|
|
220
|
+
config.last_onboarding_version = __version__
|
|
221
|
+
|
|
222
|
+
save_global_config(config)
|
|
223
|
+
|
|
224
|
+
console.print("\n[green]✓ Configuration saved![/green]\n")
|
|
225
|
+
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@click.group(invoke_without_command=True)
|
|
230
|
+
@click.version_option(version=__version__)
|
|
231
|
+
@click.option("--cwd", type=click.Path(exists=True), help="Working directory")
|
|
232
|
+
@click.option(
|
|
233
|
+
"--unsafe",
|
|
234
|
+
is_flag=True,
|
|
235
|
+
help="Disable safe mode (skip permission prompts for tools)",
|
|
236
|
+
)
|
|
237
|
+
@click.option("--verbose", is_flag=True, help="Verbose output")
|
|
238
|
+
@click.option("-p", "--prompt", type=str, help="Direct prompt (non-interactive)")
|
|
239
|
+
@click.pass_context
|
|
240
|
+
def cli(
|
|
241
|
+
ctx: click.Context, cwd: Optional[str], unsafe: bool, verbose: bool, prompt: Optional[str]
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Ripperdoc - AI-powered coding agent"""
|
|
244
|
+
|
|
245
|
+
# Ensure onboarding is complete
|
|
246
|
+
if not check_onboarding():
|
|
247
|
+
sys.exit(1)
|
|
248
|
+
|
|
249
|
+
# Set working directory
|
|
250
|
+
if cwd:
|
|
251
|
+
import os
|
|
252
|
+
|
|
253
|
+
os.chdir(cwd)
|
|
254
|
+
|
|
255
|
+
# Initialize project configuration for the current working directory
|
|
256
|
+
project_path = Path.cwd()
|
|
257
|
+
get_project_config(project_path)
|
|
258
|
+
|
|
259
|
+
safe_mode = not unsafe
|
|
260
|
+
|
|
261
|
+
# If prompt is provided, run directly
|
|
262
|
+
if prompt:
|
|
263
|
+
tools = get_default_tools()
|
|
264
|
+
asyncio.run(run_query(prompt, tools, safe_mode, verbose))
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
# If no command specified, start interactive REPL with Rich interface
|
|
268
|
+
if ctx.invoked_subcommand is None:
|
|
269
|
+
# Use Rich interface by default
|
|
270
|
+
from ripperdoc.cli.ui.rich_ui import main_rich
|
|
271
|
+
|
|
272
|
+
main_rich(safe_mode=safe_mode, verbose=verbose)
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@cli.command(name="config")
|
|
277
|
+
def config_cmd() -> None:
|
|
278
|
+
"""Show current configuration"""
|
|
279
|
+
config = get_global_config()
|
|
280
|
+
|
|
281
|
+
console.print("\n[bold]Global Configuration[/bold]\n")
|
|
282
|
+
console.print(f"Version: {__version__}")
|
|
283
|
+
console.print(f"Onboarding Complete: {config.has_completed_onboarding}")
|
|
284
|
+
console.print(f"Theme: {config.theme}")
|
|
285
|
+
console.print(f"Verbose: {config.verbose}")
|
|
286
|
+
console.print(f"Safe Mode: {config.safe_mode}\n")
|
|
287
|
+
|
|
288
|
+
if config.model_profiles:
|
|
289
|
+
console.print("[bold]Model Profiles:[/bold]")
|
|
290
|
+
for name, profile in config.model_profiles.items():
|
|
291
|
+
console.print(f" {name}:")
|
|
292
|
+
console.print(f" Provider: {profile.provider}")
|
|
293
|
+
console.print(f" Model: {profile.model}")
|
|
294
|
+
console.print(f" API Key: {'***' if profile.api_key else 'Not set'}")
|
|
295
|
+
console.print()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@cli.command(name="version")
|
|
299
|
+
def version_cmd() -> None:
|
|
300
|
+
"""Show version information"""
|
|
301
|
+
console.print(f"Ripperdoc version {__version__}")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def main() -> None:
|
|
305
|
+
"""Main entry point."""
|
|
306
|
+
try:
|
|
307
|
+
cli()
|
|
308
|
+
except KeyboardInterrupt:
|
|
309
|
+
console.print("\n[yellow]Interrupted[/yellow]")
|
|
310
|
+
sys.exit(130)
|
|
311
|
+
except Exception as e:
|
|
312
|
+
console.print(f"[red]Fatal error: {escape(str(e))}[/red]")
|
|
313
|
+
sys.exit(1)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == "__main__":
|
|
317
|
+
main()
|
|
@@ -0,0 +1,76 @@
|
|
|
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 .exit_cmd import command as exit_command
|
|
15
|
+
from .help_cmd import command as help_command
|
|
16
|
+
from .mcp_cmd import command as mcp_command
|
|
17
|
+
from .models_cmd import command as models_command
|
|
18
|
+
from .resume_cmd import command as resume_command
|
|
19
|
+
from .tasks_cmd import command as tasks_command
|
|
20
|
+
from .status_cmd import command as status_command
|
|
21
|
+
from .todos_cmd import command as todos_command
|
|
22
|
+
from .tools_cmd import command as tools_command
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_registry(commands: List[SlashCommand]) -> Dict[str, SlashCommand]:
|
|
26
|
+
"""Map command names and aliases to SlashCommand entries."""
|
|
27
|
+
registry: Dict[str, SlashCommand] = {}
|
|
28
|
+
for cmd in commands:
|
|
29
|
+
registry[cmd.name] = cmd
|
|
30
|
+
for alias in cmd.aliases:
|
|
31
|
+
registry[alias] = cmd
|
|
32
|
+
return registry
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
ALL_COMMANDS: List[SlashCommand] = [
|
|
36
|
+
help_command,
|
|
37
|
+
clear_command,
|
|
38
|
+
config_command,
|
|
39
|
+
tools_command,
|
|
40
|
+
models_command,
|
|
41
|
+
exit_command,
|
|
42
|
+
status_command,
|
|
43
|
+
tasks_command,
|
|
44
|
+
todos_command,
|
|
45
|
+
mcp_command,
|
|
46
|
+
cost_command,
|
|
47
|
+
context_command,
|
|
48
|
+
compact_command,
|
|
49
|
+
resume_command,
|
|
50
|
+
agents_command,
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
COMMAND_REGISTRY: Dict[str, SlashCommand] = _build_registry(ALL_COMMANDS)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def list_slash_commands() -> List[SlashCommand]:
|
|
57
|
+
"""Return the ordered list of base slash commands (no aliases)."""
|
|
58
|
+
return ALL_COMMANDS
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_slash_command(name: str) -> SlashCommand | None:
|
|
62
|
+
"""Return a command by name or alias."""
|
|
63
|
+
return COMMAND_REGISTRY.get(name)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def slash_command_completions() -> List[tuple[str, SlashCommand]]:
|
|
67
|
+
"""Return (name, command) pairs for completion including aliases."""
|
|
68
|
+
return list(COMMAND_REGISTRY.items())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"SlashCommand",
|
|
73
|
+
"list_slash_commands",
|
|
74
|
+
"get_slash_command",
|
|
75
|
+
"slash_command_completions",
|
|
76
|
+
]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from rich.markup import escape
|
|
2
|
+
|
|
3
|
+
from ripperdoc.core.agents import (
|
|
4
|
+
AGENT_DIR_NAME,
|
|
5
|
+
AgentLocation,
|
|
6
|
+
delete_agent_definition,
|
|
7
|
+
load_agent_definitions,
|
|
8
|
+
save_agent_definition,
|
|
9
|
+
)
|
|
10
|
+
from ripperdoc.core.config import get_global_config
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
from .base import SlashCommand
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
17
|
+
console = ui.console
|
|
18
|
+
tokens = trimmed_arg.split()
|
|
19
|
+
subcmd = tokens[0].lower() if tokens else ""
|
|
20
|
+
|
|
21
|
+
def print_agents_usage() -> None:
|
|
22
|
+
console.print("[bold]/agents[/bold] — list configured agents")
|
|
23
|
+
console.print(
|
|
24
|
+
"[bold]/agents create <name> [location] [model][/bold] — create agent (location: user|project, default user)"
|
|
25
|
+
)
|
|
26
|
+
console.print("[bold]/agents edit <name> [location][/bold] — edit an existing agent")
|
|
27
|
+
console.print(
|
|
28
|
+
"[bold]/agents delete <name> [location][/bold] — delete agent (location: user|project, default user)"
|
|
29
|
+
)
|
|
30
|
+
console.print(
|
|
31
|
+
f"[dim]Agent files live in ~/.ripperdoc/{AGENT_DIR_NAME} or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
|
|
32
|
+
)
|
|
33
|
+
console.print(
|
|
34
|
+
"[dim]Model can be a profile name or pointer (task/main/etc). Defaults to 'task'.[/dim]"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if subcmd in ("help", "-h", "--help"):
|
|
38
|
+
print_agents_usage()
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
if subcmd in ("create", "add"):
|
|
42
|
+
agent_name = tokens[1] if len(tokens) > 1 else console.input("Agent name: ").strip()
|
|
43
|
+
if not agent_name:
|
|
44
|
+
console.print("[red]Agent name is required.[/red]")
|
|
45
|
+
print_agents_usage()
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
description = console.input("Description (when to use this agent): ").strip()
|
|
49
|
+
if not description:
|
|
50
|
+
console.print("[red]Description is required.[/red]")
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
tools_input = console.input("Tools (comma-separated, * for all) [*]: ").strip() or "*"
|
|
54
|
+
tools = [t.strip() for t in tools_input.split(",") if t.strip()] or ["*"]
|
|
55
|
+
|
|
56
|
+
system_prompt = console.input("System prompt (single line, use \\n for newlines): ").strip()
|
|
57
|
+
if not system_prompt:
|
|
58
|
+
console.print("[red]System prompt is required.[/red]")
|
|
59
|
+
print_agents_usage()
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
location_arg = tokens[2] if len(tokens) > 2 else ""
|
|
63
|
+
model_arg = tokens[3] if len(tokens) > 3 else ""
|
|
64
|
+
if location_arg and location_arg.lower() not in ("user", "project"):
|
|
65
|
+
model_arg, location_arg = location_arg, ""
|
|
66
|
+
|
|
67
|
+
location_raw = (
|
|
68
|
+
location_arg or console.input("Location [user/project, default user]: ").strip()
|
|
69
|
+
).lower()
|
|
70
|
+
location = AgentLocation.PROJECT if location_raw == "project" else AgentLocation.USER
|
|
71
|
+
|
|
72
|
+
config = get_global_config()
|
|
73
|
+
pointer_map = config.model_pointers.model_dump()
|
|
74
|
+
default_model_value = model_arg or pointer_map.get("task", "task")
|
|
75
|
+
model_input = (
|
|
76
|
+
console.input(f"Model profile or pointer [{default_model_value}]: ").strip()
|
|
77
|
+
or default_model_value
|
|
78
|
+
)
|
|
79
|
+
if (
|
|
80
|
+
model_input
|
|
81
|
+
and model_input not in config.model_profiles
|
|
82
|
+
and model_input not in pointer_map
|
|
83
|
+
):
|
|
84
|
+
console.print(
|
|
85
|
+
"[yellow]Model not found in profiles or pointers; will fall back to main if unavailable.[/yellow]"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
path = save_agent_definition(
|
|
90
|
+
agent_type=agent_name,
|
|
91
|
+
description=description,
|
|
92
|
+
tools=tools,
|
|
93
|
+
system_prompt=system_prompt,
|
|
94
|
+
location=location,
|
|
95
|
+
model=model_input,
|
|
96
|
+
)
|
|
97
|
+
console.print(
|
|
98
|
+
f"[green]✓ Agent '{escape(agent_name)}' created at {escape(str(path))}[/green]"
|
|
99
|
+
)
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
console.print(f"[red]Failed to create agent: {escape(str(exc))}[/red]")
|
|
102
|
+
print_agents_usage()
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
if subcmd in ("delete", "del", "remove"):
|
|
106
|
+
agent_name = (
|
|
107
|
+
tokens[1] if len(tokens) > 1 else console.input("Agent name to delete: ").strip()
|
|
108
|
+
)
|
|
109
|
+
if not agent_name:
|
|
110
|
+
console.print("[red]Agent name is required.[/red]")
|
|
111
|
+
print_agents_usage()
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
location_raw = (
|
|
115
|
+
tokens[2]
|
|
116
|
+
if len(tokens) > 2
|
|
117
|
+
else console.input("Location to delete from [user/project, default user]: ").strip()
|
|
118
|
+
).lower()
|
|
119
|
+
location = AgentLocation.PROJECT if location_raw == "project" else AgentLocation.USER
|
|
120
|
+
try:
|
|
121
|
+
path = delete_agent_definition(agent_name, location)
|
|
122
|
+
console.print(
|
|
123
|
+
f"[green]✓ Deleted agent '{escape(agent_name)}' at {escape(str(path))}[/green]"
|
|
124
|
+
)
|
|
125
|
+
except FileNotFoundError as exc:
|
|
126
|
+
console.print(f"[yellow]{escape(str(exc))}[/yellow]")
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
console.print(f"[red]Failed to delete agent: {escape(str(exc))}[/red]")
|
|
129
|
+
print_agents_usage()
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
if subcmd in ("edit", "update"):
|
|
133
|
+
agent_name = tokens[1] if len(tokens) > 1 else console.input("Agent to edit: ").strip()
|
|
134
|
+
if not agent_name:
|
|
135
|
+
console.print("[red]Agent name is required.[/red]")
|
|
136
|
+
print_agents_usage()
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
agents = load_agent_definitions()
|
|
140
|
+
target_agent = next((a for a in agents.active_agents if a.agent_type == agent_name), None)
|
|
141
|
+
if not target_agent:
|
|
142
|
+
console.print(f"[red]Agent '{escape(agent_name)}' not found.[/red]")
|
|
143
|
+
print_agents_usage()
|
|
144
|
+
return True
|
|
145
|
+
if target_agent.location == AgentLocation.BUILT_IN:
|
|
146
|
+
console.print("[yellow]Built-in agents cannot be edited.[/yellow]")
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
default_location = target_agent.location
|
|
150
|
+
location_raw = (
|
|
151
|
+
tokens[2]
|
|
152
|
+
if len(tokens) > 2
|
|
153
|
+
else console.input(
|
|
154
|
+
f"Location to save [user/project, default {default_location.value}]: "
|
|
155
|
+
).strip()
|
|
156
|
+
).lower()
|
|
157
|
+
location = AgentLocation.PROJECT if location_raw == "project" else AgentLocation.USER
|
|
158
|
+
|
|
159
|
+
description = (
|
|
160
|
+
console.input(f"Description (when to use) [{target_agent.when_to_use}]: ").strip()
|
|
161
|
+
or target_agent.when_to_use
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
tools_default = "*" if "*" in target_agent.tools else ", ".join(target_agent.tools)
|
|
165
|
+
tools_input = (
|
|
166
|
+
console.input(f"Tools (comma-separated, * for all) [{tools_default}]: ").strip()
|
|
167
|
+
or tools_default
|
|
168
|
+
)
|
|
169
|
+
tools = [t.strip() for t in tools_input.split(",") if t.strip()] or ["*"]
|
|
170
|
+
|
|
171
|
+
console.print("[dim]Current system prompt:[/dim]")
|
|
172
|
+
console.print(escape(target_agent.system_prompt or "(empty)"), markup=False)
|
|
173
|
+
system_prompt = (
|
|
174
|
+
console.input(
|
|
175
|
+
"System prompt (single line, use \\n for newlines) " "[Enter to keep current]: "
|
|
176
|
+
).strip()
|
|
177
|
+
or target_agent.system_prompt
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
config = get_global_config()
|
|
181
|
+
pointer_map = config.model_pointers.model_dump()
|
|
182
|
+
model_default = target_agent.model or pointer_map.get("task", "task")
|
|
183
|
+
model_input = (
|
|
184
|
+
console.input(f"Model profile or pointer [{model_default}]: ").strip() or model_default
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
path = save_agent_definition(
|
|
189
|
+
agent_type=agent_name,
|
|
190
|
+
description=description,
|
|
191
|
+
tools=tools,
|
|
192
|
+
system_prompt=system_prompt,
|
|
193
|
+
location=location,
|
|
194
|
+
model=model_input,
|
|
195
|
+
overwrite=True,
|
|
196
|
+
)
|
|
197
|
+
console.print(
|
|
198
|
+
f"[green]✓ Agent '{escape(agent_name)}' updated at {escape(str(path))}[/green]"
|
|
199
|
+
)
|
|
200
|
+
except Exception as exc:
|
|
201
|
+
console.print(f"[red]Failed to update agent: {escape(str(exc))}[/red]")
|
|
202
|
+
print_agents_usage()
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
agents = load_agent_definitions()
|
|
206
|
+
console.print("\n[bold]Agents:[/bold]")
|
|
207
|
+
print_agents_usage()
|
|
208
|
+
if not agents.active_agents:
|
|
209
|
+
console.print(" • None configured")
|
|
210
|
+
for agent in agents.active_agents:
|
|
211
|
+
location = getattr(agent.location, "value", agent.location)
|
|
212
|
+
tools_str = "all tools" if "*" in agent.tools else ", ".join(agent.tools)
|
|
213
|
+
console.print(f" • {escape(agent.agent_type)} ({escape(str(location))})", markup=False)
|
|
214
|
+
console.print(f" {escape(agent.when_to_use)}", markup=False)
|
|
215
|
+
console.print(f" tools: {escape(tools_str)}", markup=False)
|
|
216
|
+
console.print(f" model: {escape(agent.model or 'task (default)')}", markup=False)
|
|
217
|
+
if agents.failed_files:
|
|
218
|
+
console.print("[yellow]Some agent files could not be loaded:[/yellow]")
|
|
219
|
+
for path, error in agents.failed_files:
|
|
220
|
+
console.print(f" - {escape(str(path))}: {escape(str(error))}", markup=False)
|
|
221
|
+
console.print(
|
|
222
|
+
f"[dim]Add agents in ~/.ripperdoc/{AGENT_DIR_NAME} or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
|
|
223
|
+
)
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
command = SlashCommand(
|
|
228
|
+
name="agents",
|
|
229
|
+
description="Manage subagents: list/create/delete",
|
|
230
|
+
handler=_handle,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
__all__ = ["command"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Shared types for slash command handlers."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Callable, Tuple
|
|
5
|
+
|
|
6
|
+
Handler = Callable[[Any, str], bool]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class SlashCommand:
|
|
11
|
+
"""A single slash command implementation."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
description: str
|
|
15
|
+
handler: Handler
|
|
16
|
+
aliases: Tuple[str, ...] = ()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["SlashCommand", "Handler"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from .base import SlashCommand
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _handle(ui: Any, _: str) -> bool:
|
|
6
|
+
ui.conversation_messages = []
|
|
7
|
+
ui.console.print("[green]✓ Conversation cleared[/green]")
|
|
8
|
+
return True
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
command = SlashCommand(
|
|
12
|
+
name="clear",
|
|
13
|
+
description="Clear conversation history",
|
|
14
|
+
handler=_handle,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["command"]
|