ripperdoc 0.2.9__py3-none-any.whl → 0.3.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.
Files changed (76) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +379 -51
  3. ripperdoc/cli/commands/__init__.py +6 -0
  4. ripperdoc/cli/commands/agents_cmd.py +128 -5
  5. ripperdoc/cli/commands/clear_cmd.py +8 -0
  6. ripperdoc/cli/commands/doctor_cmd.py +29 -0
  7. ripperdoc/cli/commands/exit_cmd.py +1 -0
  8. ripperdoc/cli/commands/memory_cmd.py +2 -1
  9. ripperdoc/cli/commands/models_cmd.py +63 -7
  10. ripperdoc/cli/commands/resume_cmd.py +5 -0
  11. ripperdoc/cli/commands/skills_cmd.py +103 -0
  12. ripperdoc/cli/commands/stats_cmd.py +244 -0
  13. ripperdoc/cli/commands/status_cmd.py +10 -0
  14. ripperdoc/cli/commands/tasks_cmd.py +6 -3
  15. ripperdoc/cli/commands/themes_cmd.py +139 -0
  16. ripperdoc/cli/ui/file_mention_completer.py +63 -13
  17. ripperdoc/cli/ui/helpers.py +6 -3
  18. ripperdoc/cli/ui/interrupt_handler.py +34 -0
  19. ripperdoc/cli/ui/panels.py +14 -8
  20. ripperdoc/cli/ui/rich_ui.py +737 -47
  21. ripperdoc/cli/ui/spinner.py +93 -18
  22. ripperdoc/cli/ui/thinking_spinner.py +1 -2
  23. ripperdoc/cli/ui/tool_renderers.py +10 -9
  24. ripperdoc/cli/ui/wizard.py +24 -19
  25. ripperdoc/core/agents.py +14 -3
  26. ripperdoc/core/config.py +238 -6
  27. ripperdoc/core/default_tools.py +91 -10
  28. ripperdoc/core/hooks/events.py +4 -0
  29. ripperdoc/core/hooks/llm_callback.py +58 -0
  30. ripperdoc/core/hooks/manager.py +6 -0
  31. ripperdoc/core/permissions.py +160 -9
  32. ripperdoc/core/providers/openai.py +84 -28
  33. ripperdoc/core/query.py +489 -87
  34. ripperdoc/core/query_utils.py +17 -14
  35. ripperdoc/core/skills.py +1 -0
  36. ripperdoc/core/theme.py +298 -0
  37. ripperdoc/core/tool.py +15 -5
  38. ripperdoc/protocol/__init__.py +14 -0
  39. ripperdoc/protocol/models.py +300 -0
  40. ripperdoc/protocol/stdio.py +1453 -0
  41. ripperdoc/tools/background_shell.py +354 -139
  42. ripperdoc/tools/bash_tool.py +117 -22
  43. ripperdoc/tools/file_edit_tool.py +228 -50
  44. ripperdoc/tools/file_read_tool.py +154 -3
  45. ripperdoc/tools/file_write_tool.py +53 -11
  46. ripperdoc/tools/grep_tool.py +98 -8
  47. ripperdoc/tools/lsp_tool.py +609 -0
  48. ripperdoc/tools/multi_edit_tool.py +26 -3
  49. ripperdoc/tools/skill_tool.py +52 -1
  50. ripperdoc/tools/task_tool.py +539 -65
  51. ripperdoc/utils/conversation_compaction.py +1 -1
  52. ripperdoc/utils/file_watch.py +216 -7
  53. ripperdoc/utils/image_utils.py +125 -0
  54. ripperdoc/utils/log.py +30 -3
  55. ripperdoc/utils/lsp.py +812 -0
  56. ripperdoc/utils/mcp.py +80 -18
  57. ripperdoc/utils/message_formatting.py +7 -4
  58. ripperdoc/utils/messages.py +198 -33
  59. ripperdoc/utils/pending_messages.py +50 -0
  60. ripperdoc/utils/permissions/shell_command_validation.py +3 -3
  61. ripperdoc/utils/permissions/tool_permission_utils.py +180 -15
  62. ripperdoc/utils/platform.py +198 -0
  63. ripperdoc/utils/session_heatmap.py +242 -0
  64. ripperdoc/utils/session_history.py +2 -2
  65. ripperdoc/utils/session_stats.py +294 -0
  66. ripperdoc/utils/shell_utils.py +8 -5
  67. ripperdoc/utils/todo.py +0 -6
  68. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/METADATA +55 -17
  69. ripperdoc-0.3.0.dist-info/RECORD +136 -0
  70. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/WHEEL +1 -1
  71. ripperdoc/sdk/__init__.py +0 -9
  72. ripperdoc/sdk/client.py +0 -333
  73. ripperdoc-0.2.9.dist-info/RECORD +0 -123
  74. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/entry_points.txt +0 -0
  75. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/licenses/LICENSE +0 -0
  76. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/top_level.txt +0 -0
ripperdoc/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Ripperdoc - AI-powered coding agent."""
2
2
 
3
- __version__ = "0.2.9"
3
+ __version__ = "0.3.0"
ripperdoc/cli/cli.py CHANGED
@@ -6,6 +6,7 @@ This module provides the command-line interface for the Ripperdoc agent.
6
6
  import asyncio
7
7
  import click
8
8
  import sys
9
+ import time
9
10
  import uuid
10
11
  from pathlib import Path
11
12
  from typing import Any, Dict, List, Optional
@@ -16,41 +17,86 @@ from ripperdoc.core.config import (
16
17
  get_project_config,
17
18
  )
18
19
  from ripperdoc.cli.ui.wizard import check_onboarding
19
- from ripperdoc.core.default_tools import get_default_tools
20
+ from ripperdoc.core.default_tools import get_default_tools, BUILTIN_TOOL_NAMES
20
21
  from ripperdoc.core.query import query, QueryContext
21
22
  from ripperdoc.core.system_prompt import build_system_prompt
22
23
  from ripperdoc.core.skills import build_skill_summary, load_all_skills
23
24
  from ripperdoc.core.hooks.manager import hook_manager
25
+ from ripperdoc.core.hooks.llm_callback import build_hook_llm_callback
24
26
  from ripperdoc.utils.messages import create_user_message
25
27
  from ripperdoc.utils.memory import build_memory_instructions
26
28
  from ripperdoc.core.permissions import make_permission_checker
29
+ from ripperdoc.utils.session_history import (
30
+ SessionHistory,
31
+ list_session_summaries,
32
+ load_session_messages,
33
+ )
27
34
  from ripperdoc.utils.mcp import (
28
35
  load_mcp_servers_async,
29
36
  format_mcp_instructions,
30
37
  shutdown_mcp_runtime,
31
38
  )
39
+ from ripperdoc.utils.lsp import shutdown_lsp_manager
40
+ from ripperdoc.tools.background_shell import shutdown_background_shell
32
41
  from ripperdoc.tools.mcp_tools import load_dynamic_mcp_tools_async, merge_tools_with_dynamic
33
42
  from ripperdoc.utils.log import enable_session_file_logging, get_logger
34
43
 
35
44
 
36
45
  from rich.console import Console
37
46
  from rich.markdown import Markdown
38
- from rich.panel import Panel
39
47
  from rich.markup import escape
40
48
 
41
49
  console = Console()
42
50
  logger = get_logger()
43
51
 
44
52
 
53
+ def parse_tools_option(tools_arg: Optional[str]) -> Optional[List[str]]:
54
+ """Parse the --tools argument.
55
+
56
+ Args:
57
+ tools_arg: The raw tools argument from CLI.
58
+
59
+ Returns:
60
+ None for default (all tools), empty list for "" (no tools),
61
+ or a list of tool names for filtering.
62
+ """
63
+ if tools_arg is None:
64
+ return None # Use all default tools
65
+
66
+ tools_arg = tools_arg.strip()
67
+
68
+ if tools_arg == "":
69
+ return [] # Disable all tools
70
+
71
+ if tools_arg.lower() == "default":
72
+ return None # Use all default tools
73
+
74
+ # Parse comma-separated list
75
+ tool_names = [name.strip() for name in tools_arg.split(",") if name.strip()]
76
+
77
+ # Validate tool names
78
+ invalid_tools = [name for name in tool_names if name not in BUILTIN_TOOL_NAMES]
79
+ if invalid_tools:
80
+ logger.warning(
81
+ "[cli] Unknown tools specified: %s. Available tools: %s",
82
+ ", ".join(invalid_tools),
83
+ ", ".join(BUILTIN_TOOL_NAMES),
84
+ )
85
+
86
+ return tool_names if tool_names else None
87
+
88
+
45
89
  async def run_query(
46
90
  prompt: str,
47
91
  tools: list,
48
92
  yolo_mode: bool = False,
49
93
  verbose: bool = False,
50
94
  session_id: Optional[str] = None,
95
+ custom_system_prompt: Optional[str] = None,
96
+ append_system_prompt: Optional[str] = None,
97
+ model: Optional[str] = None,
51
98
  ) -> None:
52
99
  """Run a single query and print the response."""
53
-
54
100
  logger.info(
55
101
  "[cli] Running single prompt session",
56
102
  extra={
@@ -58,6 +104,9 @@ async def run_query(
58
104
  "verbose": verbose,
59
105
  "session_id": session_id,
60
106
  "prompt_length": len(prompt),
107
+ "model": model,
108
+ "has_custom_system_prompt": custom_system_prompt is not None,
109
+ "has_append_system_prompt": append_system_prompt is not None,
61
110
  },
62
111
  )
63
112
  if prompt:
@@ -72,15 +121,32 @@ async def run_query(
72
121
  # Initialize hook manager
73
122
  hook_manager.set_project_dir(project_path)
74
123
  hook_manager.set_session_id(session_id)
124
+ hook_manager.set_llm_callback(build_hook_llm_callback())
125
+ session_history = SessionHistory(project_path, session_id or str(uuid.uuid4()))
126
+ hook_manager.set_transcript_path(str(session_history.path))
127
+
128
+ def _collect_hook_contexts(result: Any) -> List[str]:
129
+ contexts: List[str] = []
130
+ system_message = getattr(result, "system_message", None)
131
+ additional_context = getattr(result, "additional_context", None)
132
+ if system_message:
133
+ contexts.append(str(system_message))
134
+ if additional_context:
135
+ contexts.append(str(additional_context))
136
+ return contexts
75
137
 
76
138
  # Create initial user message
77
139
  from ripperdoc.utils.messages import UserMessage, AssistantMessage, ProgressMessage
78
140
 
79
141
  messages: List[UserMessage | AssistantMessage | ProgressMessage] = [create_user_message(prompt)]
142
+ session_history.append(messages[0])
80
143
 
81
144
  # Create query context
82
- query_context = QueryContext(tools=tools, yolo_mode=yolo_mode, verbose=verbose)
145
+ query_context = QueryContext(
146
+ tools=tools, yolo_mode=yolo_mode, verbose=verbose, model=model or "main"
147
+ )
83
148
 
149
+ session_start_time = time.time()
84
150
  try:
85
151
  context: Dict[str, Any] = {}
86
152
  # System prompt
@@ -103,61 +169,77 @@ async def run_query(
103
169
  memory_instructions = build_memory_instructions()
104
170
  if memory_instructions:
105
171
  additional_instructions.append(memory_instructions)
106
- system_prompt = build_system_prompt(
107
- tools,
108
- prompt,
109
- context,
110
- additional_instructions=additional_instructions or None,
111
- mcp_instructions=mcp_instructions,
112
- )
113
172
 
114
- # Run the query
173
+ session_start_result = await hook_manager.run_session_start_async("startup")
174
+ session_hook_contexts = _collect_hook_contexts(session_start_result)
175
+ if session_hook_contexts:
176
+ additional_instructions.extend(session_hook_contexts)
177
+
178
+ prompt_hook_result = await hook_manager.run_user_prompt_submit_async(prompt)
179
+ if prompt_hook_result.should_block or not prompt_hook_result.should_continue:
180
+ reason = (
181
+ prompt_hook_result.block_reason
182
+ or prompt_hook_result.stop_reason
183
+ or "Prompt blocked by hook."
184
+ )
185
+ console.print(f"[red]{escape(str(reason))}[/red]")
186
+ return
187
+ prompt_hook_contexts = _collect_hook_contexts(prompt_hook_result)
188
+ if prompt_hook_contexts:
189
+ additional_instructions.extend(prompt_hook_contexts)
190
+
191
+ # Build system prompt based on options:
192
+ # - custom_system_prompt: replaces the default entirely
193
+ # - append_system_prompt: appends to the default system prompt
194
+ if custom_system_prompt:
195
+ # Complete replacement
196
+ system_prompt = custom_system_prompt
197
+ # Still append if both are provided
198
+ if append_system_prompt:
199
+ system_prompt = f"{system_prompt}\n\n{append_system_prompt}"
200
+ else:
201
+ # Build default with optional append
202
+ all_instructions = list(additional_instructions) if additional_instructions else []
203
+ if append_system_prompt:
204
+ all_instructions.append(append_system_prompt)
205
+ system_prompt = build_system_prompt(
206
+ tools,
207
+ prompt,
208
+ context,
209
+ additional_instructions=all_instructions or None,
210
+ mcp_instructions=mcp_instructions,
211
+ )
212
+
213
+ # Run the query - collect final response text
214
+ final_response_parts: List[str] = []
115
215
  try:
116
216
  async for message in query(
117
217
  messages, system_prompt, context, query_context, can_use_tool
118
218
  ):
119
219
  if message.type == "assistant" and hasattr(message, "message"):
120
- # Print assistant message
220
+ # Collect assistant message text for final output
121
221
  if isinstance(message.message.content, str):
122
- console.print(
123
- Panel(
124
- Markdown(message.message.content),
125
- title="Ripperdoc",
126
- border_style="cyan",
127
- padding=(0, 1),
128
- )
129
- )
222
+ final_response_parts.append(message.message.content)
130
223
  else:
131
224
  # Handle structured content
132
225
  for block in message.message.content:
133
226
  if isinstance(block, dict):
134
227
  if block.get("type") == "text":
135
- console.print(
136
- Panel(
137
- Markdown(block["text"]),
138
- title="Ripperdoc",
139
- border_style="cyan",
140
- padding=(0, 1),
141
- )
142
- )
228
+ final_response_parts.append(block["text"])
143
229
  else:
144
230
  if hasattr(block, "type") and block.type == "text":
145
- console.print(
146
- Panel(
147
- Markdown(block.text or ""),
148
- title="Ripperdoc",
149
- border_style="cyan",
150
- padding=(0, 1),
151
- )
152
- )
153
-
154
- elif message.type == "progress" and hasattr(message, "content"):
155
- # Print progress
156
- if verbose:
157
- console.print(f"[dim]Progress: {escape(str(message.content))}[/dim]")
231
+ final_response_parts.append(block.text or "")
232
+
233
+ # Skip progress messages entirely for -p mode
158
234
 
159
235
  # Add message to history
160
236
  messages.append(message) # type: ignore[arg-type]
237
+ session_history.append(message) # type: ignore[arg-type]
238
+
239
+ # Print final response as clean markdown (no panel, no decoration)
240
+ if final_response_parts:
241
+ final_text = "\n".join(final_response_parts)
242
+ console.print(Markdown(final_text))
161
243
 
162
244
  except KeyboardInterrupt:
163
245
  console.print("\n[yellow]Interrupted by user[/yellow]")
@@ -180,11 +262,32 @@ async def run_query(
180
262
  extra={"session_id": session_id, "message_count": len(messages)},
181
263
  )
182
264
  finally:
265
+ duration = max(time.time() - session_start_time, 0.0)
266
+ try:
267
+ await hook_manager.run_session_end_async(
268
+ "other", duration_seconds=duration, message_count=len(messages)
269
+ )
270
+ except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc:
271
+ logger.warning(
272
+ "[cli] SessionEnd hook failed: %s: %s",
273
+ type(exc).__name__,
274
+ exc,
275
+ extra={"session_id": session_id},
276
+ )
183
277
  await shutdown_mcp_runtime()
278
+ await shutdown_lsp_manager()
279
+ # Shutdown background shell manager
280
+ try:
281
+ shutdown_background_shell(force=True)
282
+ except (OSError, RuntimeError) as exc:
283
+ logger.debug(
284
+ "[cli] Failed to shut down background shell: %s: %s",
285
+ type(exc).__name__,
286
+ exc,
287
+ )
184
288
  logger.debug("[cli] Shutdown MCP runtime", extra={"session_id": session_id})
185
289
 
186
290
 
187
-
188
291
  @click.group(invoke_without_command=True)
189
292
  @click.version_option(version=__version__)
190
293
  @click.option("--cwd", type=click.Path(exists=True), help="Working directory")
@@ -194,27 +297,107 @@ async def run_query(
194
297
  help="YOLO mode: skip all permission prompts for tools",
195
298
  )
196
299
  @click.option("--verbose", is_flag=True, help="Verbose output")
197
- @click.option("--show-full-thinking", is_flag=True, help="Show full reasoning content instead of truncated preview")
300
+ @click.option(
301
+ "--show-full-thinking/--hide-full-thinking",
302
+ default=None,
303
+ help="Show full reasoning content instead of truncated preview",
304
+ )
198
305
  @click.option("-p", "--prompt", type=str, help="Direct prompt (non-interactive)")
306
+ @click.option(
307
+ "--tools",
308
+ type=str,
309
+ default=None,
310
+ help=(
311
+ 'Specify the list of available tools. Use "" to disable all tools, '
312
+ '"default" to use all tools, or specify tool names (e.g. "Bash,Edit,Read").'
313
+ ),
314
+ )
315
+ @click.option(
316
+ "--system-prompt",
317
+ type=str,
318
+ default=None,
319
+ help="System prompt to use for the session (replaces default).",
320
+ )
321
+ @click.option(
322
+ "--append-system-prompt",
323
+ type=str,
324
+ default=None,
325
+ help="Additional instructions to append to the system prompt.",
326
+ )
327
+ @click.option(
328
+ "--model",
329
+ type=str,
330
+ default=None,
331
+ help="Model profile for the current session.",
332
+ )
333
+ @click.option(
334
+ "-c",
335
+ "--continue",
336
+ "continue_session",
337
+ is_flag=True,
338
+ help="Continue the most recent conversation in the current directory.",
339
+ )
199
340
  @click.pass_context
200
341
  def cli(
201
- ctx: click.Context, cwd: Optional[str], yolo: bool, verbose: bool, show_full_thinking: bool, prompt: Optional[str]
342
+ ctx: click.Context,
343
+ cwd: Optional[str],
344
+ yolo: bool,
345
+ verbose: bool,
346
+ show_full_thinking: Optional[bool],
347
+ prompt: Optional[str],
348
+ tools: Optional[str],
349
+ system_prompt: Optional[str],
350
+ append_system_prompt: Optional[str],
351
+ model: Optional[str],
352
+ continue_session: bool,
202
353
  ) -> None:
203
354
  """Ripperdoc - AI-powered coding agent"""
204
355
  session_id = str(uuid.uuid4())
356
+ cwd_changed: Optional[str] = None
205
357
 
206
358
  # Set working directory
207
359
  if cwd:
208
360
  import os
209
361
 
210
362
  os.chdir(cwd)
363
+ cwd_changed = cwd
364
+
365
+ project_path = Path.cwd()
366
+
367
+ # Handle --continue option: load the most recent session
368
+ resume_messages = None
369
+ most_recent = None
370
+ if continue_session:
371
+ summaries = list_session_summaries(project_path)
372
+ if summaries:
373
+ most_recent = summaries[0]
374
+ session_id = most_recent.session_id
375
+ resume_messages = load_session_messages(project_path, session_id)
376
+ console.print(f"[dim]Continuing session: {most_recent.last_prompt}[/dim]")
377
+ else:
378
+ console.print("[yellow]No previous sessions found in this directory.[/yellow]")
379
+
380
+ log_file = enable_session_file_logging(project_path, session_id)
381
+
382
+ if cwd_changed:
211
383
  logger.debug(
212
384
  "[cli] Changed working directory via --cwd",
213
- extra={"cwd": cwd, "session_id": session_id},
385
+ extra={"cwd": cwd_changed, "session_id": session_id},
214
386
  )
215
387
 
216
- project_path = Path.cwd()
217
- log_file = enable_session_file_logging(project_path, session_id)
388
+ if most_recent:
389
+ logger.info(
390
+ "[cli] Continuing session",
391
+ extra={
392
+ "session_id": session_id,
393
+ "message_count": len(resume_messages) if resume_messages else 0,
394
+ "last_prompt": most_recent.last_prompt,
395
+ "log_file": str(log_file),
396
+ },
397
+ )
398
+ elif continue_session:
399
+ logger.warning("[cli] No previous sessions found to continue")
400
+
218
401
  logger.info(
219
402
  "[cli] Starting CLI invocation",
220
403
  extra={
@@ -237,15 +420,72 @@ def cli(
237
420
  get_project_config(project_path)
238
421
 
239
422
  yolo_mode = yolo
423
+ # Parse --tools option
424
+ allowed_tools = parse_tools_option(tools)
425
+
426
+ # Handle piped stdin input
427
+ # - With -p flag: Not applicable (prompt comes from -p argument)
428
+ # - Without -p: stdin becomes the initial query in interactive mode
429
+ initial_query: Optional[str] = None
430
+ if prompt is None and ctx.invoked_subcommand is None:
431
+ stdin_stream = click.get_text_stream("stdin")
432
+ try:
433
+ stdin_is_tty = stdin_stream.isatty()
434
+ except Exception:
435
+ stdin_is_tty = True
436
+
437
+ if not stdin_is_tty:
438
+ try:
439
+ stdin_data = stdin_stream.read()
440
+ except (OSError, ValueError) as exc:
441
+ logger.warning(
442
+ "[cli] Failed to read stdin for initial query: %s: %s",
443
+ type(exc).__name__,
444
+ exc,
445
+ extra={"session_id": session_id},
446
+ )
447
+ else:
448
+ trimmed = stdin_data.rstrip("\n")
449
+ if trimmed.strip():
450
+ initial_query = trimmed
451
+ logger.info(
452
+ "[cli] Received initial query from stdin",
453
+ extra={
454
+ "session_id": session_id,
455
+ "query_length": len(initial_query),
456
+ "query_preview": initial_query[:200],
457
+ },
458
+ )
459
+
240
460
  logger.debug(
241
461
  "[cli] Configuration initialized",
242
- extra={"session_id": session_id, "yolo_mode": yolo_mode, "verbose": verbose},
462
+ extra={
463
+ "session_id": session_id,
464
+ "yolo_mode": yolo_mode,
465
+ "verbose": verbose,
466
+ "allowed_tools": allowed_tools,
467
+ "model": model,
468
+ "has_system_prompt": system_prompt is not None,
469
+ "has_append_system_prompt": append_system_prompt is not None,
470
+ "continue_session": continue_session,
471
+ },
243
472
  )
244
473
 
245
474
  # If prompt is provided, run directly
246
475
  if prompt:
247
- tools = get_default_tools()
248
- asyncio.run(run_query(prompt, tools, yolo_mode, verbose, session_id=session_id))
476
+ tool_list = get_default_tools(allowed_tools=allowed_tools)
477
+ asyncio.run(
478
+ run_query(
479
+ prompt,
480
+ tool_list,
481
+ yolo_mode,
482
+ verbose,
483
+ session_id=session_id,
484
+ custom_system_prompt=system_prompt,
485
+ append_system_prompt=append_system_prompt,
486
+ model=model,
487
+ )
488
+ )
249
489
  return
250
490
 
251
491
  # If no command specified, start interactive REPL with Rich interface
@@ -259,6 +499,12 @@ def cli(
259
499
  show_full_thinking=show_full_thinking,
260
500
  session_id=session_id,
261
501
  log_file_path=log_file,
502
+ allowed_tools=allowed_tools,
503
+ custom_system_prompt=system_prompt,
504
+ append_system_prompt=append_system_prompt,
505
+ model=model,
506
+ resume_messages=resume_messages,
507
+ initial_query=initial_query,
262
508
  )
263
509
  return
264
510
 
@@ -286,6 +532,88 @@ def config_cmd() -> None:
286
532
  console.print()
287
533
 
288
534
 
535
+ @cli.command(name="stdio")
536
+ @click.option(
537
+ "--input-format",
538
+ type=click.Choice(["stream-json", "auto"]),
539
+ default="stream-json",
540
+ help="Input format for messages.",
541
+ )
542
+ @click.option(
543
+ "--output-format",
544
+ type=click.Choice(["stream-json"]),
545
+ default="stream-json",
546
+ help="Output format for messages.",
547
+ )
548
+ @click.option(
549
+ "--model",
550
+ type=str,
551
+ default=None,
552
+ help="Model profile for the current session.",
553
+ )
554
+ @click.option(
555
+ "--permission-mode",
556
+ type=click.Choice(["default", "acceptEdits", "plan", "bypassPermissions"]),
557
+ default="default",
558
+ help="Permission mode for tool usage.",
559
+ )
560
+ @click.option(
561
+ "--max-turns",
562
+ type=int,
563
+ default=None,
564
+ help="Maximum number of conversation turns.",
565
+ )
566
+ @click.option(
567
+ "--system-prompt",
568
+ type=str,
569
+ default=None,
570
+ help="System prompt to use for the session.",
571
+ )
572
+ @click.option(
573
+ "--print",
574
+ "-p",
575
+ is_flag=True,
576
+ help="Print mode (for single prompt queries).",
577
+ )
578
+ @click.option(
579
+ "--",
580
+ "prompt",
581
+ type=str,
582
+ default=None,
583
+ help="Direct prompt (for print mode).",
584
+ )
585
+ def stdio_cmd(
586
+ input_format: str,
587
+ output_format: str,
588
+ model: str | None,
589
+ permission_mode: str,
590
+ max_turns: int | None,
591
+ system_prompt: str | None,
592
+ print: bool,
593
+ prompt: str | None,
594
+ ) -> None:
595
+ """Stdio mode for SDK subprocess communication.
596
+
597
+ This command enables Ripperdoc to communicate with SDKs via JSON Control
598
+ Protocol over stdin/stdout.
599
+ """
600
+ from ripperdoc.protocol.stdio import _run_stdio
601
+ import asyncio
602
+
603
+ asyncio.run(
604
+ _run_stdio(
605
+ input_format=input_format,
606
+ output_format=output_format,
607
+ model=model,
608
+ permission_mode=permission_mode,
609
+ max_turns=max_turns,
610
+ system_prompt=system_prompt,
611
+ print_mode=print,
612
+ prompt=prompt,
613
+ )
614
+ )
615
+
616
+
289
617
  @cli.command(name="version")
290
618
  def version_cmd() -> None:
291
619
  """Show version information"""
@@ -21,8 +21,11 @@ from .mcp_cmd import command as mcp_command
21
21
  from .models_cmd import command as models_command
22
22
  from .permissions_cmd import command as permissions_command
23
23
  from .resume_cmd import command as resume_command
24
+ from .skills_cmd import command as skills_command
25
+ from .stats_cmd import command as stats_command
24
26
  from .tasks_cmd import command as tasks_command
25
27
  from .status_cmd import command as status_command
28
+ from .themes_cmd import command as themes_command
26
29
  from .todos_cmd import command as todos_command
27
30
  from .tools_cmd import command as tools_command
28
31
 
@@ -51,6 +54,7 @@ ALL_COMMANDS: List[SlashCommand] = [
51
54
  models_command,
52
55
  exit_command,
53
56
  status_command,
57
+ stats_command,
54
58
  doctor_command,
55
59
  memory_command,
56
60
  permissions_command,
@@ -62,7 +66,9 @@ ALL_COMMANDS: List[SlashCommand] = [
62
66
  context_command,
63
67
  compact_command,
64
68
  resume_command,
69
+ skills_command,
65
70
  agents_command,
71
+ themes_command,
66
72
  ]
67
73
 
68
74
  COMMAND_REGISTRY: Dict[str, SlashCommand] = _build_registry(ALL_COMMANDS)