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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +379 -51
- ripperdoc/cli/commands/__init__.py +6 -0
- ripperdoc/cli/commands/agents_cmd.py +128 -5
- ripperdoc/cli/commands/clear_cmd.py +8 -0
- ripperdoc/cli/commands/doctor_cmd.py +29 -0
- ripperdoc/cli/commands/exit_cmd.py +1 -0
- ripperdoc/cli/commands/memory_cmd.py +2 -1
- ripperdoc/cli/commands/models_cmd.py +63 -7
- ripperdoc/cli/commands/resume_cmd.py +5 -0
- ripperdoc/cli/commands/skills_cmd.py +103 -0
- ripperdoc/cli/commands/stats_cmd.py +244 -0
- ripperdoc/cli/commands/status_cmd.py +10 -0
- ripperdoc/cli/commands/tasks_cmd.py +6 -3
- ripperdoc/cli/commands/themes_cmd.py +139 -0
- ripperdoc/cli/ui/file_mention_completer.py +63 -13
- ripperdoc/cli/ui/helpers.py +6 -3
- ripperdoc/cli/ui/interrupt_handler.py +34 -0
- ripperdoc/cli/ui/panels.py +14 -8
- ripperdoc/cli/ui/rich_ui.py +737 -47
- ripperdoc/cli/ui/spinner.py +93 -18
- ripperdoc/cli/ui/thinking_spinner.py +1 -2
- ripperdoc/cli/ui/tool_renderers.py +10 -9
- ripperdoc/cli/ui/wizard.py +24 -19
- ripperdoc/core/agents.py +14 -3
- ripperdoc/core/config.py +238 -6
- ripperdoc/core/default_tools.py +91 -10
- ripperdoc/core/hooks/events.py +4 -0
- ripperdoc/core/hooks/llm_callback.py +58 -0
- ripperdoc/core/hooks/manager.py +6 -0
- ripperdoc/core/permissions.py +160 -9
- ripperdoc/core/providers/openai.py +84 -28
- ripperdoc/core/query.py +489 -87
- ripperdoc/core/query_utils.py +17 -14
- ripperdoc/core/skills.py +1 -0
- ripperdoc/core/theme.py +298 -0
- ripperdoc/core/tool.py +15 -5
- ripperdoc/protocol/__init__.py +14 -0
- ripperdoc/protocol/models.py +300 -0
- ripperdoc/protocol/stdio.py +1453 -0
- ripperdoc/tools/background_shell.py +354 -139
- ripperdoc/tools/bash_tool.py +117 -22
- ripperdoc/tools/file_edit_tool.py +228 -50
- ripperdoc/tools/file_read_tool.py +154 -3
- ripperdoc/tools/file_write_tool.py +53 -11
- ripperdoc/tools/grep_tool.py +98 -8
- ripperdoc/tools/lsp_tool.py +609 -0
- ripperdoc/tools/multi_edit_tool.py +26 -3
- ripperdoc/tools/skill_tool.py +52 -1
- ripperdoc/tools/task_tool.py +539 -65
- ripperdoc/utils/conversation_compaction.py +1 -1
- ripperdoc/utils/file_watch.py +216 -7
- ripperdoc/utils/image_utils.py +125 -0
- ripperdoc/utils/log.py +30 -3
- ripperdoc/utils/lsp.py +812 -0
- ripperdoc/utils/mcp.py +80 -18
- ripperdoc/utils/message_formatting.py +7 -4
- ripperdoc/utils/messages.py +198 -33
- ripperdoc/utils/pending_messages.py +50 -0
- ripperdoc/utils/permissions/shell_command_validation.py +3 -3
- ripperdoc/utils/permissions/tool_permission_utils.py +180 -15
- ripperdoc/utils/platform.py +198 -0
- ripperdoc/utils/session_heatmap.py +242 -0
- ripperdoc/utils/session_history.py +2 -2
- ripperdoc/utils/session_stats.py +294 -0
- ripperdoc/utils/shell_utils.py +8 -5
- ripperdoc/utils/todo.py +0 -6
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/METADATA +55 -17
- ripperdoc-0.3.0.dist-info/RECORD +136 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/WHEEL +1 -1
- ripperdoc/sdk/__init__.py +0 -9
- ripperdoc/sdk/client.py +0 -333
- ripperdoc-0.2.9.dist-info/RECORD +0 -123
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/top_level.txt +0 -0
ripperdoc/__init__.py
CHANGED
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(
|
|
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
|
-
|
|
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
|
-
#
|
|
220
|
+
# Collect assistant message text for final output
|
|
121
221
|
if isinstance(message.message.content, str):
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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(
|
|
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,
|
|
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":
|
|
385
|
+
extra={"cwd": cwd_changed, "session_id": session_id},
|
|
214
386
|
)
|
|
215
387
|
|
|
216
|
-
|
|
217
|
-
|
|
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={
|
|
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
|
-
|
|
248
|
-
asyncio.run(
|
|
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)
|