ripperdoc 0.2.8__py3-none-any.whl → 0.2.9__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 +28 -115
- ripperdoc/cli/commands/__init__.py +0 -1
- ripperdoc/cli/commands/agents_cmd.py +6 -3
- ripperdoc/cli/commands/clear_cmd.py +1 -4
- ripperdoc/cli/commands/config_cmd.py +1 -1
- ripperdoc/cli/commands/context_cmd.py +3 -2
- ripperdoc/cli/commands/doctor_cmd.py +18 -4
- ripperdoc/cli/commands/hooks_cmd.py +27 -53
- ripperdoc/cli/commands/models_cmd.py +26 -9
- ripperdoc/cli/commands/permissions_cmd.py +27 -9
- ripperdoc/cli/commands/resume_cmd.py +5 -3
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +2 -1
- ripperdoc/cli/ui/interrupt_handler.py +2 -3
- ripperdoc/cli/ui/message_display.py +4 -2
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +110 -59
- ripperdoc/cli/ui/spinner.py +25 -1
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +215 -0
- ripperdoc/core/agents.py +9 -3
- ripperdoc/core/config.py +49 -12
- ripperdoc/core/custom_commands.py +7 -6
- ripperdoc/core/default_tools.py +11 -2
- ripperdoc/core/hooks/config.py +1 -3
- ripperdoc/core/hooks/events.py +23 -28
- ripperdoc/core/hooks/executor.py +4 -6
- ripperdoc/core/hooks/integration.py +12 -21
- ripperdoc/core/hooks/manager.py +40 -15
- ripperdoc/core/permissions.py +40 -8
- ripperdoc/core/providers/anthropic.py +109 -36
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +60 -5
- ripperdoc/core/query.py +82 -38
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +9 -5
- ripperdoc/sdk/client.py +2 -2
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +2 -1
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +26 -16
- ripperdoc/tools/dynamic_mcp_tool.py +29 -8
- ripperdoc/tools/enter_plan_mode_tool.py +1 -1
- ripperdoc/tools/exit_plan_mode_tool.py +1 -1
- ripperdoc/tools/file_edit_tool.py +8 -4
- ripperdoc/tools/file_read_tool.py +8 -4
- ripperdoc/tools/file_write_tool.py +9 -5
- ripperdoc/tools/glob_tool.py +3 -2
- ripperdoc/tools/grep_tool.py +3 -2
- ripperdoc/tools/kill_bash_tool.py +1 -1
- ripperdoc/tools/ls_tool.py +1 -1
- ripperdoc/tools/mcp_tools.py +13 -10
- ripperdoc/tools/multi_edit_tool.py +8 -7
- ripperdoc/tools/notebook_edit_tool.py +7 -4
- ripperdoc/tools/skill_tool.py +1 -1
- ripperdoc/tools/task_tool.py +5 -4
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +8 -4
- ripperdoc/utils/file_watch.py +8 -2
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +11 -7
- ripperdoc/utils/messages.py +105 -66
- ripperdoc/utils/path_ignore.py +35 -8
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +1 -1
- ripperdoc-0.2.9.dist-info/RECORD +123 -0
- ripperdoc-0.2.8.dist-info/RECORD +0 -121
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
ripperdoc/__init__.py
CHANGED
ripperdoc/cli/cli.py
CHANGED
|
@@ -13,11 +13,9 @@ from typing import Any, Dict, List, Optional
|
|
|
13
13
|
from ripperdoc import __version__
|
|
14
14
|
from ripperdoc.core.config import (
|
|
15
15
|
get_global_config,
|
|
16
|
-
save_global_config,
|
|
17
16
|
get_project_config,
|
|
18
|
-
ModelProfile,
|
|
19
|
-
ProviderType,
|
|
20
17
|
)
|
|
18
|
+
from ripperdoc.cli.ui.wizard import check_onboarding
|
|
21
19
|
from ripperdoc.core.default_tools import get_default_tools
|
|
22
20
|
from ripperdoc.core.query import query, QueryContext
|
|
23
21
|
from ripperdoc.core.system_prompt import build_system_prompt
|
|
@@ -33,7 +31,7 @@ from ripperdoc.utils.mcp import (
|
|
|
33
31
|
)
|
|
34
32
|
from ripperdoc.tools.mcp_tools import load_dynamic_mcp_tools_async, merge_tools_with_dynamic
|
|
35
33
|
from ripperdoc.utils.log import enable_session_file_logging, get_logger
|
|
36
|
-
|
|
34
|
+
|
|
37
35
|
|
|
38
36
|
from rich.console import Console
|
|
39
37
|
from rich.markdown import Markdown
|
|
@@ -47,7 +45,7 @@ logger = get_logger()
|
|
|
47
45
|
async def run_query(
|
|
48
46
|
prompt: str,
|
|
49
47
|
tools: list,
|
|
50
|
-
|
|
48
|
+
yolo_mode: bool = False,
|
|
51
49
|
verbose: bool = False,
|
|
52
50
|
session_id: Optional[str] = None,
|
|
53
51
|
) -> None:
|
|
@@ -56,7 +54,7 @@ async def run_query(
|
|
|
56
54
|
logger.info(
|
|
57
55
|
"[cli] Running single prompt session",
|
|
58
56
|
extra={
|
|
59
|
-
"
|
|
57
|
+
"yolo_mode": yolo_mode,
|
|
60
58
|
"verbose": verbose,
|
|
61
59
|
"session_id": session_id,
|
|
62
60
|
"prompt_length": len(prompt),
|
|
@@ -69,7 +67,7 @@ async def run_query(
|
|
|
69
67
|
)
|
|
70
68
|
|
|
71
69
|
project_path = Path.cwd()
|
|
72
|
-
can_use_tool = make_permission_checker(project_path,
|
|
70
|
+
can_use_tool = None if yolo_mode else make_permission_checker(project_path, yolo_mode=False)
|
|
73
71
|
|
|
74
72
|
# Initialize hook manager
|
|
75
73
|
hook_manager.set_project_dir(project_path)
|
|
@@ -81,7 +79,7 @@ async def run_query(
|
|
|
81
79
|
messages: List[UserMessage | AssistantMessage | ProgressMessage] = [create_user_message(prompt)]
|
|
82
80
|
|
|
83
81
|
# Create query context
|
|
84
|
-
query_context = QueryContext(tools=tools,
|
|
82
|
+
query_context = QueryContext(tools=tools, yolo_mode=yolo_mode, verbose=verbose)
|
|
85
83
|
|
|
86
84
|
try:
|
|
87
85
|
context: Dict[str, Any] = {}
|
|
@@ -169,7 +167,8 @@ async def run_query(
|
|
|
169
167
|
console.print(f"[red]Error: {escape(str(e))}[/red]")
|
|
170
168
|
logger.warning(
|
|
171
169
|
"[cli] Unhandled error while running prompt: %s: %s",
|
|
172
|
-
type(e).__name__,
|
|
170
|
+
type(e).__name__,
|
|
171
|
+
e,
|
|
173
172
|
extra={"session_id": session_id},
|
|
174
173
|
)
|
|
175
174
|
if verbose:
|
|
@@ -185,104 +184,6 @@ async def run_query(
|
|
|
185
184
|
logger.debug("[cli] Shutdown MCP runtime", extra={"session_id": session_id})
|
|
186
185
|
|
|
187
186
|
|
|
188
|
-
def check_onboarding() -> bool:
|
|
189
|
-
"""Check if onboarding is complete and run if needed."""
|
|
190
|
-
config = get_global_config()
|
|
191
|
-
|
|
192
|
-
if config.has_completed_onboarding:
|
|
193
|
-
return True
|
|
194
|
-
|
|
195
|
-
console.print("[bold cyan]Welcome to Ripperdoc![/bold cyan]\n")
|
|
196
|
-
console.print("Let's set up your AI model configuration.\n")
|
|
197
|
-
|
|
198
|
-
# Simple onboarding
|
|
199
|
-
provider_choices = [
|
|
200
|
-
*[p.value for p in ProviderType],
|
|
201
|
-
"openai",
|
|
202
|
-
"deepseek",
|
|
203
|
-
"mistral",
|
|
204
|
-
"kimi",
|
|
205
|
-
"qwen",
|
|
206
|
-
"glm",
|
|
207
|
-
"custom",
|
|
208
|
-
]
|
|
209
|
-
provider_choice = click.prompt(
|
|
210
|
-
"Choose your model protocol",
|
|
211
|
-
type=click.Choice(provider_choices),
|
|
212
|
-
default=ProviderType.ANTHROPIC.value,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
api_base = None
|
|
216
|
-
if provider_choice == "custom":
|
|
217
|
-
provider_choice = click.prompt(
|
|
218
|
-
"Protocol family (for API compatibility)",
|
|
219
|
-
type=click.Choice([p.value for p in ProviderType]),
|
|
220
|
-
default=ProviderType.OPENAI_COMPATIBLE.value,
|
|
221
|
-
)
|
|
222
|
-
api_base = click.prompt("API Base URL")
|
|
223
|
-
|
|
224
|
-
api_key = ""
|
|
225
|
-
while not api_key:
|
|
226
|
-
api_key = prompt_secret("Enter your API key").strip()
|
|
227
|
-
if not api_key:
|
|
228
|
-
console.print("[red]API key is required.[/red]")
|
|
229
|
-
|
|
230
|
-
provider = ProviderType(provider_choice)
|
|
231
|
-
|
|
232
|
-
# Get model name
|
|
233
|
-
if provider == ProviderType.ANTHROPIC:
|
|
234
|
-
model = click.prompt("Model name", default="claude-3-5-sonnet-20241022")
|
|
235
|
-
elif provider == ProviderType.OPENAI_COMPATIBLE:
|
|
236
|
-
default_model = "gpt-4o-mini"
|
|
237
|
-
if provider_choice == "deepseek":
|
|
238
|
-
default_model = "deepseek-chat"
|
|
239
|
-
api_base = api_base or "https://api.deepseek.com"
|
|
240
|
-
model = click.prompt("Model name", default=default_model)
|
|
241
|
-
if api_base is None:
|
|
242
|
-
api_base = (
|
|
243
|
-
click.prompt("API base URL (optional)", default="", show_default=False) or None
|
|
244
|
-
)
|
|
245
|
-
elif provider == ProviderType.GEMINI:
|
|
246
|
-
console.print(
|
|
247
|
-
"[yellow]Gemini protocol support is not yet available; configuration is saved for "
|
|
248
|
-
"future support.[/yellow]"
|
|
249
|
-
)
|
|
250
|
-
model = click.prompt("Model name", default="gemini-1.5-pro")
|
|
251
|
-
if api_base is None:
|
|
252
|
-
api_base = (
|
|
253
|
-
click.prompt("API base URL (optional)", default="", show_default=False) or None
|
|
254
|
-
)
|
|
255
|
-
else:
|
|
256
|
-
model = click.prompt("Model name")
|
|
257
|
-
|
|
258
|
-
context_window_input = click.prompt(
|
|
259
|
-
"Context window in tokens (optional, press Enter to skip)", default="", show_default=False
|
|
260
|
-
)
|
|
261
|
-
context_window = None
|
|
262
|
-
if context_window_input.strip():
|
|
263
|
-
try:
|
|
264
|
-
context_window = int(context_window_input.strip())
|
|
265
|
-
except ValueError:
|
|
266
|
-
console.print("[yellow]Invalid context window, using auto-detected defaults.[/yellow]")
|
|
267
|
-
|
|
268
|
-
# Create model profile
|
|
269
|
-
config.model_profiles["default"] = ModelProfile(
|
|
270
|
-
provider=provider,
|
|
271
|
-
model=model,
|
|
272
|
-
api_key=api_key,
|
|
273
|
-
api_base=api_base,
|
|
274
|
-
context_window=context_window,
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
config.has_completed_onboarding = True
|
|
278
|
-
config.last_onboarding_version = __version__
|
|
279
|
-
|
|
280
|
-
save_global_config(config)
|
|
281
|
-
|
|
282
|
-
console.print("\n[green]✓ Configuration saved![/green]\n")
|
|
283
|
-
|
|
284
|
-
return True
|
|
285
|
-
|
|
286
187
|
|
|
287
188
|
@click.group(invoke_without_command=True)
|
|
288
189
|
@click.version_option(version=__version__)
|
|
@@ -293,10 +194,11 @@ def check_onboarding() -> bool:
|
|
|
293
194
|
help="YOLO mode: skip all permission prompts for tools",
|
|
294
195
|
)
|
|
295
196
|
@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")
|
|
296
198
|
@click.option("-p", "--prompt", type=str, help="Direct prompt (non-interactive)")
|
|
297
199
|
@click.pass_context
|
|
298
200
|
def cli(
|
|
299
|
-
ctx: click.Context, cwd: Optional[str], yolo: bool, verbose: bool, prompt: Optional[str]
|
|
201
|
+
ctx: click.Context, cwd: Optional[str], yolo: bool, verbose: bool, show_full_thinking: bool, prompt: Optional[str]
|
|
300
202
|
) -> None:
|
|
301
203
|
"""Ripperdoc - AI-powered coding agent"""
|
|
302
204
|
session_id = str(uuid.uuid4())
|
|
@@ -334,16 +236,16 @@ def cli(
|
|
|
334
236
|
# Initialize project configuration for the current working directory
|
|
335
237
|
get_project_config(project_path)
|
|
336
238
|
|
|
337
|
-
|
|
239
|
+
yolo_mode = yolo
|
|
338
240
|
logger.debug(
|
|
339
241
|
"[cli] Configuration initialized",
|
|
340
|
-
extra={"session_id": session_id, "
|
|
242
|
+
extra={"session_id": session_id, "yolo_mode": yolo_mode, "verbose": verbose},
|
|
341
243
|
)
|
|
342
244
|
|
|
343
245
|
# If prompt is provided, run directly
|
|
344
246
|
if prompt:
|
|
345
247
|
tools = get_default_tools()
|
|
346
|
-
asyncio.run(run_query(prompt, tools,
|
|
248
|
+
asyncio.run(run_query(prompt, tools, yolo_mode, verbose, session_id=session_id))
|
|
347
249
|
return
|
|
348
250
|
|
|
349
251
|
# If no command specified, start interactive REPL with Rich interface
|
|
@@ -352,8 +254,9 @@ def cli(
|
|
|
352
254
|
from ripperdoc.cli.ui.rich_ui import main_rich
|
|
353
255
|
|
|
354
256
|
main_rich(
|
|
355
|
-
|
|
257
|
+
yolo_mode=yolo_mode,
|
|
356
258
|
verbose=verbose,
|
|
259
|
+
show_full_thinking=show_full_thinking,
|
|
357
260
|
session_id=session_id,
|
|
358
261
|
log_file_path=log_file,
|
|
359
262
|
)
|
|
@@ -370,7 +273,8 @@ def config_cmd() -> None:
|
|
|
370
273
|
console.print(f"Onboarding Complete: {config.has_completed_onboarding}")
|
|
371
274
|
console.print(f"Theme: {config.theme}")
|
|
372
275
|
console.print(f"Verbose: {config.verbose}")
|
|
373
|
-
console.print(f"
|
|
276
|
+
console.print(f"Yolo Mode: {config.yolo_mode}")
|
|
277
|
+
console.print(f"Show Full Thinking: {config.show_full_thinking}\n")
|
|
374
278
|
|
|
375
279
|
if config.model_profiles:
|
|
376
280
|
console.print("[bold]Model Profiles:[/bold]")
|
|
@@ -397,11 +301,20 @@ def main() -> None:
|
|
|
397
301
|
sys.exit(130)
|
|
398
302
|
except SystemExit:
|
|
399
303
|
raise
|
|
400
|
-
except (
|
|
304
|
+
except (
|
|
305
|
+
RuntimeError,
|
|
306
|
+
ValueError,
|
|
307
|
+
TypeError,
|
|
308
|
+
OSError,
|
|
309
|
+
IOError,
|
|
310
|
+
ConnectionError,
|
|
311
|
+
click.ClickException,
|
|
312
|
+
) as e:
|
|
401
313
|
console.print(f"[red]Fatal error: {escape(str(e))}[/red]")
|
|
402
314
|
logger.warning(
|
|
403
315
|
"[cli] Fatal error in main CLI entrypoint: %s: %s",
|
|
404
|
-
type(e).__name__,
|
|
316
|
+
type(e).__name__,
|
|
317
|
+
e,
|
|
405
318
|
)
|
|
406
319
|
sys.exit(1)
|
|
407
320
|
|
|
@@ -116,7 +116,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
116
116
|
print_agents_usage()
|
|
117
117
|
logger.warning(
|
|
118
118
|
"[agents_cmd] Failed to create agent: %s: %s",
|
|
119
|
-
type(exc).__name__,
|
|
119
|
+
type(exc).__name__,
|
|
120
|
+
exc,
|
|
120
121
|
extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
|
|
121
122
|
)
|
|
122
123
|
return True
|
|
@@ -148,7 +149,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
148
149
|
print_agents_usage()
|
|
149
150
|
logger.warning(
|
|
150
151
|
"[agents_cmd] Failed to delete agent: %s: %s",
|
|
151
|
-
type(exc).__name__,
|
|
152
|
+
type(exc).__name__,
|
|
153
|
+
exc,
|
|
152
154
|
extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
|
|
153
155
|
)
|
|
154
156
|
return True
|
|
@@ -226,7 +228,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
226
228
|
print_agents_usage()
|
|
227
229
|
logger.warning(
|
|
228
230
|
"[agents_cmd] Failed to update agent: %s: %s",
|
|
229
|
-
type(exc).__name__,
|
|
231
|
+
type(exc).__name__,
|
|
232
|
+
exc,
|
|
230
233
|
extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
|
|
231
234
|
)
|
|
232
235
|
return True
|
|
@@ -9,10 +9,7 @@ def _handle(ui: Any, _: str) -> bool:
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
command = SlashCommand(
|
|
12
|
-
name="clear",
|
|
13
|
-
description="Clear conversation history",
|
|
14
|
-
handler=_handle,
|
|
15
|
-
aliases=("new",)
|
|
12
|
+
name="clear", description="Clear conversation history", handler=_handle, aliases=("new",)
|
|
16
13
|
)
|
|
17
14
|
|
|
18
15
|
|
|
@@ -16,7 +16,7 @@ def _handle(ui: Any, _: str) -> bool:
|
|
|
16
16
|
ui.console.print(
|
|
17
17
|
f"\n[bold]Model (main -> {escape(str(main_pointer))}):[/bold] {escape(str(model_label))}"
|
|
18
18
|
)
|
|
19
|
-
ui.console.print(f"[bold]
|
|
19
|
+
ui.console.print(f"[bold]Yolo Mode:[/bold] {escape(str(ui.yolo_mode))}")
|
|
20
20
|
ui.console.print(f"[bold]Verbose:[/bold] {escape(str(ui.verbose))}")
|
|
21
21
|
return True
|
|
22
22
|
|
|
@@ -40,7 +40,7 @@ def _handle(ui: Any, _: str) -> bool:
|
|
|
40
40
|
if not ui.query_context:
|
|
41
41
|
ui.query_context = QueryContext(
|
|
42
42
|
tools=ui.get_default_tools(),
|
|
43
|
-
|
|
43
|
+
yolo_mode=ui.yolo_mode,
|
|
44
44
|
verbose=ui.verbose,
|
|
45
45
|
)
|
|
46
46
|
|
|
@@ -126,7 +126,8 @@ def _handle(ui: Any, _: str) -> bool:
|
|
|
126
126
|
except (OSError, RuntimeError, AttributeError, TypeError) as exc:
|
|
127
127
|
logger.warning(
|
|
128
128
|
"[context_cmd] Failed to summarize MCP tools: %s: %s",
|
|
129
|
-
type(exc).__name__,
|
|
129
|
+
type(exc).__name__,
|
|
130
|
+
exc,
|
|
130
131
|
extra={"session_id": getattr(ui, "session_id", None)},
|
|
131
132
|
)
|
|
132
133
|
for line in lines:
|
|
@@ -125,10 +125,17 @@ def _mcp_status(
|
|
|
125
125
|
servers = asyncio.run(_load())
|
|
126
126
|
else:
|
|
127
127
|
servers = runner(_load())
|
|
128
|
-
except (
|
|
128
|
+
except (
|
|
129
|
+
OSError,
|
|
130
|
+
RuntimeError,
|
|
131
|
+
ConnectionError,
|
|
132
|
+
ValueError,
|
|
133
|
+
TypeError,
|
|
134
|
+
) as exc: # pragma: no cover - defensive
|
|
129
135
|
logger.warning(
|
|
130
136
|
"[doctor] Failed to load MCP servers: %s: %s",
|
|
131
|
-
type(exc).__name__,
|
|
137
|
+
type(exc).__name__,
|
|
138
|
+
exc,
|
|
132
139
|
exc_info=exc,
|
|
133
140
|
)
|
|
134
141
|
rows.append(_status_row("MCP", "error", f"Failed to load MCP config: {exc}"))
|
|
@@ -161,10 +168,17 @@ def _project_status(project_path: Path) -> Tuple[str, str, str]:
|
|
|
161
168
|
return _status_row(
|
|
162
169
|
"Project config", "ok", f".ripperdoc/config.json loaded for {project_path}"
|
|
163
170
|
)
|
|
164
|
-
except (
|
|
171
|
+
except (
|
|
172
|
+
OSError,
|
|
173
|
+
IOError,
|
|
174
|
+
json.JSONDecodeError,
|
|
175
|
+
ValueError,
|
|
176
|
+
TypeError,
|
|
177
|
+
) as exc: # pragma: no cover - defensive
|
|
165
178
|
logger.warning(
|
|
166
179
|
"[doctor] Failed to load project config: %s: %s",
|
|
167
|
-
type(exc).__name__,
|
|
180
|
+
type(exc).__name__,
|
|
181
|
+
exc,
|
|
168
182
|
exc_info=exc,
|
|
169
183
|
)
|
|
170
184
|
return _status_row(
|
|
@@ -56,15 +56,9 @@ class HookConfigTarget:
|
|
|
56
56
|
def _print_usage(console: Any) -> None:
|
|
57
57
|
"""Display available subcommands."""
|
|
58
58
|
console.print("[bold]/hooks[/bold] — show configured hooks")
|
|
59
|
-
console.print(
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
console.print(
|
|
63
|
-
"[bold]/hooks edit [scope][/bold] — step-by-step edit of an existing hook"
|
|
64
|
-
)
|
|
65
|
-
console.print(
|
|
66
|
-
"[bold]/hooks delete [scope][/bold] — remove a hook entry (alias: del)"
|
|
67
|
-
)
|
|
59
|
+
console.print("[bold]/hooks add [scope][/bold] — guided creator (scope: local|project|global)")
|
|
60
|
+
console.print("[bold]/hooks edit [scope][/bold] — step-by-step edit of an existing hook")
|
|
61
|
+
console.print("[bold]/hooks delete [scope][/bold] — remove a hook entry (alias: del)")
|
|
68
62
|
console.print(
|
|
69
63
|
"[dim]Scopes: local=.ripperdoc/hooks.local.json (git-ignored), "
|
|
70
64
|
"project=.ripperdoc/hooks.json (shared), "
|
|
@@ -96,7 +90,9 @@ def _get_targets(project_path: Path) -> List[HookConfigTarget]:
|
|
|
96
90
|
]
|
|
97
91
|
|
|
98
92
|
|
|
99
|
-
def _select_target(
|
|
93
|
+
def _select_target(
|
|
94
|
+
console: Any, project_path: Path, scope_hint: Optional[str]
|
|
95
|
+
) -> Optional[HookConfigTarget]:
|
|
100
96
|
"""Prompt user to choose a hooks config target."""
|
|
101
97
|
targets = _get_targets(project_path)
|
|
102
98
|
|
|
@@ -122,9 +118,7 @@ def _select_target(console: Any, project_path: Path, scope_hint: Optional[str])
|
|
|
122
118
|
f" [dim]{target.description}[/dim]"
|
|
123
119
|
)
|
|
124
120
|
|
|
125
|
-
choice = console.input(
|
|
126
|
-
f"Location [1-{len(targets)}, default {default_idx + 1}]: "
|
|
127
|
-
).strip()
|
|
121
|
+
choice = console.input(f"Location [1-{len(targets)}, default {default_idx + 1}]: ").strip()
|
|
128
122
|
|
|
129
123
|
if not choice:
|
|
130
124
|
return targets[default_idx]
|
|
@@ -151,9 +145,7 @@ def _load_hooks_json(console: Any, path: Path) -> Dict[str, List[Dict[str, Any]]
|
|
|
151
145
|
logger.warning("[hooks_cmd] Invalid JSON in %s: %s", path, exc)
|
|
152
146
|
return {}
|
|
153
147
|
except (OSError, IOError, PermissionError) as exc:
|
|
154
|
-
console.print(
|
|
155
|
-
f"[red]Unable to read {escape(str(path))}: {escape(str(exc))}[/red]"
|
|
156
|
-
)
|
|
148
|
+
console.print(f"[red]Unable to read {escape(str(path))}: {escape(str(exc))}[/red]")
|
|
157
149
|
logger.warning("[hooks_cmd] Failed to read %s: %s", path, exc)
|
|
158
150
|
return {}
|
|
159
151
|
|
|
@@ -176,9 +168,7 @@ def _load_hooks_json(console: Any, path: Path) -> Dict[str, List[Dict[str, Any]]
|
|
|
176
168
|
if not isinstance(hooks_list, list):
|
|
177
169
|
continue
|
|
178
170
|
cleaned_hooks = [h for h in hooks_list if isinstance(h, dict)]
|
|
179
|
-
cleaned_matchers.append(
|
|
180
|
-
{"matcher": matcher.get("matcher"), "hooks": cleaned_hooks}
|
|
181
|
-
)
|
|
171
|
+
cleaned_matchers.append({"matcher": matcher.get("matcher"), "hooks": cleaned_hooks})
|
|
182
172
|
if cleaned_matchers:
|
|
183
173
|
hooks[event_name] = cleaned_matchers
|
|
184
174
|
|
|
@@ -229,9 +219,7 @@ def _render_hooks_overview(ui: Any, project_path: Path) -> bool:
|
|
|
229
219
|
if target.path.exists():
|
|
230
220
|
ui.console.print(f" [green]✓[/green] {target.label}: {target.path}")
|
|
231
221
|
else:
|
|
232
|
-
ui.console.print(
|
|
233
|
-
f" [dim]○[/dim] {target.label}: {target.path} [dim](not found)[/dim]"
|
|
234
|
-
)
|
|
222
|
+
ui.console.print(f" [dim]○[/dim] {target.label}: {target.path} [dim](not found)[/dim]")
|
|
235
223
|
|
|
236
224
|
ui.console.print()
|
|
237
225
|
|
|
@@ -289,9 +277,7 @@ def _render_hooks_overview(ui: Any, project_path: Path) -> bool:
|
|
|
289
277
|
ui.console.print(table)
|
|
290
278
|
ui.console.print()
|
|
291
279
|
|
|
292
|
-
ui.console.print(
|
|
293
|
-
"[dim]Tip: Hooks run in order. /hooks add launches a guided setup.[/dim]"
|
|
294
|
-
)
|
|
280
|
+
ui.console.print("[dim]Tip: Hooks run in order. /hooks add launches a guided setup.[/dim]")
|
|
295
281
|
return True
|
|
296
282
|
|
|
297
283
|
|
|
@@ -338,32 +324,28 @@ def _prompt_matcher_selection(
|
|
|
338
324
|
if event_name not in MATCHER_EVENTS:
|
|
339
325
|
if matchers:
|
|
340
326
|
return matchers[0]
|
|
341
|
-
|
|
342
|
-
matchers.append(
|
|
343
|
-
return
|
|
327
|
+
default_matcher: Dict[str, Any] = {"matcher": None, "hooks": []}
|
|
328
|
+
matchers.append(default_matcher)
|
|
329
|
+
return default_matcher
|
|
344
330
|
|
|
345
331
|
if not matchers:
|
|
346
|
-
console.print(
|
|
347
|
-
"\nMatcher (tool name or regex). Leave empty to match all tools (*)."
|
|
348
|
-
)
|
|
332
|
+
console.print("\nMatcher (tool name or regex). Leave empty to match all tools (*).")
|
|
349
333
|
pattern = console.input("Matcher: ").strip() or "*"
|
|
350
|
-
|
|
351
|
-
matchers.append(
|
|
352
|
-
return
|
|
334
|
+
first_matcher: Dict[str, Any] = {"matcher": pattern, "hooks": []}
|
|
335
|
+
matchers.append(first_matcher)
|
|
336
|
+
return first_matcher
|
|
353
337
|
|
|
354
338
|
console.print("\nSelect matcher:")
|
|
355
339
|
for idx, matcher in enumerate(matchers, start=1):
|
|
356
340
|
label = matcher.get("matcher") or "*"
|
|
357
|
-
hook_count = len(matcher.get("hooks"
|
|
341
|
+
hook_count = len(matcher.get("hooks") or [])
|
|
358
342
|
console.print(f" [{idx}] {escape(str(label))} ({hook_count} hook(s))")
|
|
359
343
|
new_idx = len(matchers) + 1
|
|
360
344
|
console.print(f" [{new_idx}] New matcher pattern")
|
|
361
345
|
|
|
362
346
|
default_choice = 1
|
|
363
347
|
while True:
|
|
364
|
-
choice = console.input(
|
|
365
|
-
f"Matcher [1-{new_idx}, default {default_choice}]: "
|
|
366
|
-
).strip()
|
|
348
|
+
choice = console.input(f"Matcher [1-{new_idx}, default {default_choice}]: ").strip()
|
|
367
349
|
if not choice:
|
|
368
350
|
choice = str(default_choice)
|
|
369
351
|
if choice.isdigit():
|
|
@@ -372,9 +354,9 @@ def _prompt_matcher_selection(
|
|
|
372
354
|
return matchers[idx - 1]
|
|
373
355
|
if idx == new_idx:
|
|
374
356
|
pattern = console.input("New matcher (blank for '*'): ").strip() or "*"
|
|
375
|
-
|
|
376
|
-
matchers.append(
|
|
377
|
-
return
|
|
357
|
+
created_matcher: Dict[str, Any] = {"matcher": pattern, "hooks": []}
|
|
358
|
+
matchers.append(created_matcher)
|
|
359
|
+
return created_matcher
|
|
378
360
|
console.print("[red]Choose a matcher number from the list.[/red]")
|
|
379
361
|
|
|
380
362
|
|
|
@@ -410,9 +392,7 @@ def _prompt_hook_details(
|
|
|
410
392
|
)
|
|
411
393
|
while True:
|
|
412
394
|
hook_type = (
|
|
413
|
-
console.input(
|
|
414
|
-
f"Hook type ({type_label}) [default {default_type}]: "
|
|
415
|
-
).strip()
|
|
395
|
+
console.input(f"Hook type ({type_label}) [default {default_type}]: ").strip()
|
|
416
396
|
or default_type
|
|
417
397
|
).lower()
|
|
418
398
|
if hook_type in allowed_types:
|
|
@@ -424,14 +404,10 @@ def _prompt_hook_details(
|
|
|
424
404
|
if hook_type == "prompt":
|
|
425
405
|
existing_prompt = (existing_hook or {}).get("prompt", "")
|
|
426
406
|
if existing_prompt:
|
|
427
|
-
console.print(
|
|
428
|
-
f"[dim]Current prompt:[/dim] {escape(existing_prompt)}", markup=False
|
|
429
|
-
)
|
|
407
|
+
console.print(f"[dim]Current prompt:[/dim] {escape(existing_prompt)}", markup=False)
|
|
430
408
|
while True:
|
|
431
409
|
prompt_text = (
|
|
432
|
-
console.input(
|
|
433
|
-
"Prompt template (use $ARGUMENTS for JSON input): "
|
|
434
|
-
).strip()
|
|
410
|
+
console.input("Prompt template (use $ARGUMENTS for JSON input): ").strip()
|
|
435
411
|
or existing_prompt
|
|
436
412
|
)
|
|
437
413
|
if prompt_text:
|
|
@@ -519,9 +495,7 @@ def _handle_edit(ui: Any, tokens: List[str], project_path: Path) -> bool:
|
|
|
519
495
|
|
|
520
496
|
hooks = _load_hooks_json(console, target.path)
|
|
521
497
|
if not hooks:
|
|
522
|
-
console.print(
|
|
523
|
-
"[yellow]No hooks found in this file. Use /hooks add to create one.[/yellow]"
|
|
524
|
-
)
|
|
498
|
+
console.print("[yellow]No hooks found in this file. Use /hooks add to create one.[/yellow]")
|
|
525
499
|
return True
|
|
526
500
|
|
|
527
501
|
event_options = list(hooks.keys())
|
|
@@ -35,7 +35,9 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
35
35
|
console.print("[bold]/models edit <name>[/bold] — edit an existing model profile")
|
|
36
36
|
console.print("[bold]/models delete <name>[/bold] — delete a model profile")
|
|
37
37
|
console.print("[bold]/models use <name>[/bold] — set the main model pointer")
|
|
38
|
-
console.print(
|
|
38
|
+
console.print(
|
|
39
|
+
"[bold]/models use <pointer> <name>[/bold] — set a specific pointer (main/task/reasoning/quick)"
|
|
40
|
+
)
|
|
39
41
|
|
|
40
42
|
def parse_int(prompt_text: str, default_value: Optional[int]) -> Optional[int]:
|
|
41
43
|
raw = console.input(prompt_text).strip()
|
|
@@ -186,7 +188,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
186
188
|
console.print(f"[red]Failed to save model: {escape(str(exc))}[/red]")
|
|
187
189
|
logger.warning(
|
|
188
190
|
"[models_cmd] Failed to save model profile: %s: %s",
|
|
189
|
-
type(exc).__name__,
|
|
191
|
+
type(exc).__name__,
|
|
192
|
+
exc,
|
|
190
193
|
extra={"profile": profile_name, "session_id": getattr(ui, "session_id", None)},
|
|
191
194
|
)
|
|
192
195
|
return True
|
|
@@ -295,7 +298,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
295
298
|
console.print(f"[red]Failed to update model: {escape(str(exc))}[/red]")
|
|
296
299
|
logger.warning(
|
|
297
300
|
"[models_cmd] Failed to update model profile: %s: %s",
|
|
298
|
-
type(exc).__name__,
|
|
301
|
+
type(exc).__name__,
|
|
302
|
+
exc,
|
|
299
303
|
extra={"profile": profile_name, "session_id": getattr(ui, "session_id", None)},
|
|
300
304
|
)
|
|
301
305
|
return True
|
|
@@ -319,7 +323,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
319
323
|
print_models_usage()
|
|
320
324
|
logger.warning(
|
|
321
325
|
"[models_cmd] Failed to delete model profile: %s: %s",
|
|
322
|
-
type(exc).__name__,
|
|
326
|
+
type(exc).__name__,
|
|
327
|
+
exc,
|
|
323
328
|
extra={"profile": target, "session_id": getattr(ui, "session_id", None)},
|
|
324
329
|
)
|
|
325
330
|
return True
|
|
@@ -333,7 +338,9 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
333
338
|
pointer = tokens[1].lower()
|
|
334
339
|
target = tokens[2]
|
|
335
340
|
if pointer not in valid_pointers:
|
|
336
|
-
console.print(
|
|
341
|
+
console.print(
|
|
342
|
+
f"[red]Invalid pointer '{escape(pointer)}'. Valid pointers: {', '.join(valid_pointers)}[/red]"
|
|
343
|
+
)
|
|
337
344
|
print_models_usage()
|
|
338
345
|
return True
|
|
339
346
|
elif len(tokens) >= 2:
|
|
@@ -346,9 +353,14 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
346
353
|
pointer = "main"
|
|
347
354
|
target = tokens[1]
|
|
348
355
|
else:
|
|
349
|
-
pointer =
|
|
356
|
+
pointer = (
|
|
357
|
+
console.input("Pointer (main/task/reasoning/quick) [main]: ").strip().lower()
|
|
358
|
+
or "main"
|
|
359
|
+
)
|
|
350
360
|
if pointer not in valid_pointers:
|
|
351
|
-
console.print(
|
|
361
|
+
console.print(
|
|
362
|
+
f"[red]Invalid pointer '{escape(pointer)}'. Valid pointers: {', '.join(valid_pointers)}[/red]"
|
|
363
|
+
)
|
|
352
364
|
return True
|
|
353
365
|
target = console.input(f"Model to use for '{pointer}': ").strip()
|
|
354
366
|
|
|
@@ -364,8 +376,13 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
364
376
|
print_models_usage()
|
|
365
377
|
logger.warning(
|
|
366
378
|
"[models_cmd] Failed to set model pointer: %s: %s",
|
|
367
|
-
type(exc).__name__,
|
|
368
|
-
|
|
379
|
+
type(exc).__name__,
|
|
380
|
+
exc,
|
|
381
|
+
extra={
|
|
382
|
+
"pointer": pointer,
|
|
383
|
+
"profile": target,
|
|
384
|
+
"session_id": getattr(ui, "session_id", None),
|
|
385
|
+
},
|
|
369
386
|
)
|
|
370
387
|
return True
|
|
371
388
|
|