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.
Files changed (84) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +28 -115
  3. ripperdoc/cli/commands/__init__.py +0 -1
  4. ripperdoc/cli/commands/agents_cmd.py +6 -3
  5. ripperdoc/cli/commands/clear_cmd.py +1 -4
  6. ripperdoc/cli/commands/config_cmd.py +1 -1
  7. ripperdoc/cli/commands/context_cmd.py +3 -2
  8. ripperdoc/cli/commands/doctor_cmd.py +18 -4
  9. ripperdoc/cli/commands/hooks_cmd.py +27 -53
  10. ripperdoc/cli/commands/models_cmd.py +26 -9
  11. ripperdoc/cli/commands/permissions_cmd.py +27 -9
  12. ripperdoc/cli/commands/resume_cmd.py +5 -3
  13. ripperdoc/cli/commands/status_cmd.py +4 -4
  14. ripperdoc/cli/commands/tasks_cmd.py +8 -4
  15. ripperdoc/cli/ui/file_mention_completer.py +2 -1
  16. ripperdoc/cli/ui/interrupt_handler.py +2 -3
  17. ripperdoc/cli/ui/message_display.py +4 -2
  18. ripperdoc/cli/ui/provider_options.py +247 -0
  19. ripperdoc/cli/ui/rich_ui.py +110 -59
  20. ripperdoc/cli/ui/spinner.py +25 -1
  21. ripperdoc/cli/ui/tool_renderers.py +8 -2
  22. ripperdoc/cli/ui/wizard.py +215 -0
  23. ripperdoc/core/agents.py +9 -3
  24. ripperdoc/core/config.py +49 -12
  25. ripperdoc/core/custom_commands.py +7 -6
  26. ripperdoc/core/default_tools.py +11 -2
  27. ripperdoc/core/hooks/config.py +1 -3
  28. ripperdoc/core/hooks/events.py +23 -28
  29. ripperdoc/core/hooks/executor.py +4 -6
  30. ripperdoc/core/hooks/integration.py +12 -21
  31. ripperdoc/core/hooks/manager.py +40 -15
  32. ripperdoc/core/permissions.py +40 -8
  33. ripperdoc/core/providers/anthropic.py +109 -36
  34. ripperdoc/core/providers/gemini.py +70 -5
  35. ripperdoc/core/providers/openai.py +60 -5
  36. ripperdoc/core/query.py +82 -38
  37. ripperdoc/core/query_utils.py +2 -0
  38. ripperdoc/core/skills.py +9 -3
  39. ripperdoc/core/system_prompt.py +4 -2
  40. ripperdoc/core/tool.py +9 -5
  41. ripperdoc/sdk/client.py +2 -2
  42. ripperdoc/tools/ask_user_question_tool.py +5 -3
  43. ripperdoc/tools/background_shell.py +2 -1
  44. ripperdoc/tools/bash_output_tool.py +1 -1
  45. ripperdoc/tools/bash_tool.py +26 -16
  46. ripperdoc/tools/dynamic_mcp_tool.py +29 -8
  47. ripperdoc/tools/enter_plan_mode_tool.py +1 -1
  48. ripperdoc/tools/exit_plan_mode_tool.py +1 -1
  49. ripperdoc/tools/file_edit_tool.py +8 -4
  50. ripperdoc/tools/file_read_tool.py +8 -4
  51. ripperdoc/tools/file_write_tool.py +9 -5
  52. ripperdoc/tools/glob_tool.py +3 -2
  53. ripperdoc/tools/grep_tool.py +3 -2
  54. ripperdoc/tools/kill_bash_tool.py +1 -1
  55. ripperdoc/tools/ls_tool.py +1 -1
  56. ripperdoc/tools/mcp_tools.py +13 -10
  57. ripperdoc/tools/multi_edit_tool.py +8 -7
  58. ripperdoc/tools/notebook_edit_tool.py +7 -4
  59. ripperdoc/tools/skill_tool.py +1 -1
  60. ripperdoc/tools/task_tool.py +5 -4
  61. ripperdoc/tools/todo_tool.py +2 -2
  62. ripperdoc/tools/tool_search_tool.py +3 -2
  63. ripperdoc/utils/conversation_compaction.py +8 -4
  64. ripperdoc/utils/file_watch.py +8 -2
  65. ripperdoc/utils/json_utils.py +2 -1
  66. ripperdoc/utils/mcp.py +11 -3
  67. ripperdoc/utils/memory.py +4 -2
  68. ripperdoc/utils/message_compaction.py +21 -7
  69. ripperdoc/utils/message_formatting.py +11 -7
  70. ripperdoc/utils/messages.py +105 -66
  71. ripperdoc/utils/path_ignore.py +35 -8
  72. ripperdoc/utils/permissions/path_validation_utils.py +2 -1
  73. ripperdoc/utils/permissions/shell_command_validation.py +427 -91
  74. ripperdoc/utils/safe_get_cwd.py +2 -1
  75. ripperdoc/utils/session_history.py +13 -6
  76. ripperdoc/utils/todo.py +2 -1
  77. ripperdoc/utils/token_estimation.py +6 -1
  78. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +1 -1
  79. ripperdoc-0.2.9.dist-info/RECORD +123 -0
  80. ripperdoc-0.2.8.dist-info/RECORD +0 -121
  81. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
  82. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
  83. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
  84. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.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.8"
3
+ __version__ = "0.2.9"
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
- from ripperdoc.utils.prompt import prompt_secret
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
- safe_mode: bool = False,
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
- "safe_mode": safe_mode,
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, safe_mode) if safe_mode else None
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, safe_mode=safe_mode, verbose=verbose)
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__, e,
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
- safe_mode = not yolo
239
+ yolo_mode = yolo
338
240
  logger.debug(
339
241
  "[cli] Configuration initialized",
340
- extra={"session_id": session_id, "safe_mode": safe_mode, "verbose": verbose},
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, safe_mode, verbose, session_id=session_id))
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
- safe_mode=safe_mode,
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"Safe Mode: {config.safe_mode}\n")
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 (RuntimeError, ValueError, TypeError, OSError, IOError, ConnectionError, click.ClickException) as e:
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__, e,
316
+ type(e).__name__,
317
+ e,
405
318
  )
406
319
  sys.exit(1)
407
320
 
@@ -29,7 +29,6 @@ from .tools_cmd import command as tools_command
29
29
  from ripperdoc.core.custom_commands import (
30
30
  CustomCommandDefinition,
31
31
  load_all_custom_commands,
32
- find_custom_command,
33
32
  expand_command_content,
34
33
  )
35
34
 
@@ -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__, exc,
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__, exc,
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__, exc,
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]Safe Mode:[/bold] {escape(str(ui.safe_mode))}")
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
- safe_mode=ui.safe_mode,
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__, exc,
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 (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc: # pragma: no cover - defensive
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__, exc,
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 (OSError, IOError, json.JSONDecodeError, ValueError, TypeError) as exc: # pragma: no cover - defensive
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__, exc,
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
- "[bold]/hooks add [scope][/bold] — guided creator (scope: local|project|global)"
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(console: Any, project_path: Path, scope_hint: Optional[str]) -> Optional[HookConfigTarget]:
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
- matcher = {"matcher": None, "hooks": []}
342
- matchers.append(matcher)
343
- return matcher
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
- matcher = {"matcher": pattern, "hooks": []}
351
- matchers.append(matcher)
352
- return matcher
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
- matcher = {"matcher": pattern, "hooks": []}
376
- matchers.append(matcher)
377
- return matcher
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("[bold]/models use <pointer> <name>[/bold] — set a specific pointer (main/task/reasoning/quick)")
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__, exc,
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__, exc,
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__, exc,
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(f"[red]Invalid pointer '{escape(pointer)}'. Valid pointers: {', '.join(valid_pointers)}[/red]")
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 = console.input("Pointer (main/task/reasoning/quick) [main]: ").strip().lower() or "main"
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(f"[red]Invalid pointer '{escape(pointer)}'. Valid pointers: {', '.join(valid_pointers)}[/red]")
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__, exc,
368
- extra={"pointer": pointer, "profile": target, "session_id": getattr(ui, "session_id", None)},
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