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.
Files changed (107) hide show
  1. ripperdoc/__init__.py +3 -0
  2. ripperdoc/__main__.py +20 -0
  3. ripperdoc/cli/__init__.py +1 -0
  4. ripperdoc/cli/cli.py +405 -0
  5. ripperdoc/cli/commands/__init__.py +82 -0
  6. ripperdoc/cli/commands/agents_cmd.py +263 -0
  7. ripperdoc/cli/commands/base.py +19 -0
  8. ripperdoc/cli/commands/clear_cmd.py +18 -0
  9. ripperdoc/cli/commands/compact_cmd.py +23 -0
  10. ripperdoc/cli/commands/config_cmd.py +31 -0
  11. ripperdoc/cli/commands/context_cmd.py +144 -0
  12. ripperdoc/cli/commands/cost_cmd.py +82 -0
  13. ripperdoc/cli/commands/doctor_cmd.py +221 -0
  14. ripperdoc/cli/commands/exit_cmd.py +19 -0
  15. ripperdoc/cli/commands/help_cmd.py +20 -0
  16. ripperdoc/cli/commands/mcp_cmd.py +70 -0
  17. ripperdoc/cli/commands/memory_cmd.py +202 -0
  18. ripperdoc/cli/commands/models_cmd.py +413 -0
  19. ripperdoc/cli/commands/permissions_cmd.py +302 -0
  20. ripperdoc/cli/commands/resume_cmd.py +98 -0
  21. ripperdoc/cli/commands/status_cmd.py +167 -0
  22. ripperdoc/cli/commands/tasks_cmd.py +278 -0
  23. ripperdoc/cli/commands/todos_cmd.py +69 -0
  24. ripperdoc/cli/commands/tools_cmd.py +19 -0
  25. ripperdoc/cli/ui/__init__.py +1 -0
  26. ripperdoc/cli/ui/context_display.py +298 -0
  27. ripperdoc/cli/ui/helpers.py +22 -0
  28. ripperdoc/cli/ui/rich_ui.py +1557 -0
  29. ripperdoc/cli/ui/spinner.py +49 -0
  30. ripperdoc/cli/ui/thinking_spinner.py +128 -0
  31. ripperdoc/cli/ui/tool_renderers.py +298 -0
  32. ripperdoc/core/__init__.py +1 -0
  33. ripperdoc/core/agents.py +486 -0
  34. ripperdoc/core/commands.py +33 -0
  35. ripperdoc/core/config.py +559 -0
  36. ripperdoc/core/default_tools.py +88 -0
  37. ripperdoc/core/permissions.py +252 -0
  38. ripperdoc/core/providers/__init__.py +47 -0
  39. ripperdoc/core/providers/anthropic.py +250 -0
  40. ripperdoc/core/providers/base.py +265 -0
  41. ripperdoc/core/providers/gemini.py +615 -0
  42. ripperdoc/core/providers/openai.py +487 -0
  43. ripperdoc/core/query.py +1058 -0
  44. ripperdoc/core/query_utils.py +622 -0
  45. ripperdoc/core/skills.py +295 -0
  46. ripperdoc/core/system_prompt.py +431 -0
  47. ripperdoc/core/tool.py +240 -0
  48. ripperdoc/sdk/__init__.py +9 -0
  49. ripperdoc/sdk/client.py +333 -0
  50. ripperdoc/tools/__init__.py +1 -0
  51. ripperdoc/tools/ask_user_question_tool.py +431 -0
  52. ripperdoc/tools/background_shell.py +389 -0
  53. ripperdoc/tools/bash_output_tool.py +98 -0
  54. ripperdoc/tools/bash_tool.py +1016 -0
  55. ripperdoc/tools/dynamic_mcp_tool.py +428 -0
  56. ripperdoc/tools/enter_plan_mode_tool.py +226 -0
  57. ripperdoc/tools/exit_plan_mode_tool.py +153 -0
  58. ripperdoc/tools/file_edit_tool.py +346 -0
  59. ripperdoc/tools/file_read_tool.py +203 -0
  60. ripperdoc/tools/file_write_tool.py +205 -0
  61. ripperdoc/tools/glob_tool.py +179 -0
  62. ripperdoc/tools/grep_tool.py +370 -0
  63. ripperdoc/tools/kill_bash_tool.py +136 -0
  64. ripperdoc/tools/ls_tool.py +471 -0
  65. ripperdoc/tools/mcp_tools.py +591 -0
  66. ripperdoc/tools/multi_edit_tool.py +456 -0
  67. ripperdoc/tools/notebook_edit_tool.py +386 -0
  68. ripperdoc/tools/skill_tool.py +205 -0
  69. ripperdoc/tools/task_tool.py +379 -0
  70. ripperdoc/tools/todo_tool.py +494 -0
  71. ripperdoc/tools/tool_search_tool.py +380 -0
  72. ripperdoc/utils/__init__.py +1 -0
  73. ripperdoc/utils/bash_constants.py +51 -0
  74. ripperdoc/utils/bash_output_utils.py +43 -0
  75. ripperdoc/utils/coerce.py +34 -0
  76. ripperdoc/utils/context_length_errors.py +252 -0
  77. ripperdoc/utils/exit_code_handlers.py +241 -0
  78. ripperdoc/utils/file_watch.py +135 -0
  79. ripperdoc/utils/git_utils.py +274 -0
  80. ripperdoc/utils/json_utils.py +27 -0
  81. ripperdoc/utils/log.py +176 -0
  82. ripperdoc/utils/mcp.py +560 -0
  83. ripperdoc/utils/memory.py +253 -0
  84. ripperdoc/utils/message_compaction.py +676 -0
  85. ripperdoc/utils/messages.py +519 -0
  86. ripperdoc/utils/output_utils.py +258 -0
  87. ripperdoc/utils/path_ignore.py +677 -0
  88. ripperdoc/utils/path_utils.py +46 -0
  89. ripperdoc/utils/permissions/__init__.py +27 -0
  90. ripperdoc/utils/permissions/path_validation_utils.py +174 -0
  91. ripperdoc/utils/permissions/shell_command_validation.py +552 -0
  92. ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
  93. ripperdoc/utils/prompt.py +17 -0
  94. ripperdoc/utils/safe_get_cwd.py +31 -0
  95. ripperdoc/utils/sandbox_utils.py +38 -0
  96. ripperdoc/utils/session_history.py +260 -0
  97. ripperdoc/utils/session_usage.py +117 -0
  98. ripperdoc/utils/shell_token_utils.py +95 -0
  99. ripperdoc/utils/shell_utils.py +159 -0
  100. ripperdoc/utils/todo.py +203 -0
  101. ripperdoc/utils/token_estimation.py +34 -0
  102. ripperdoc-0.2.6.dist-info/METADATA +193 -0
  103. ripperdoc-0.2.6.dist-info/RECORD +107 -0
  104. ripperdoc-0.2.6.dist-info/WHEEL +5 -0
  105. ripperdoc-0.2.6.dist-info/entry_points.txt +3 -0
  106. ripperdoc-0.2.6.dist-info/licenses/LICENSE +53 -0
  107. ripperdoc-0.2.6.dist-info/top_level.txt +1 -0
ripperdoc/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Ripperdoc - AI-powered coding agent."""
2
+
3
+ __version__ = "0.2.6"
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
+ ]