ripperdoc 0.3.0__py3-none-any.whl → 0.3.2__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 (40) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +9 -1
  3. ripperdoc/cli/commands/agents_cmd.py +93 -53
  4. ripperdoc/cli/commands/mcp_cmd.py +3 -0
  5. ripperdoc/cli/commands/models_cmd.py +768 -283
  6. ripperdoc/cli/commands/permissions_cmd.py +107 -52
  7. ripperdoc/cli/commands/resume_cmd.py +61 -51
  8. ripperdoc/cli/commands/themes_cmd.py +31 -1
  9. ripperdoc/cli/ui/agents_tui/__init__.py +3 -0
  10. ripperdoc/cli/ui/agents_tui/textual_app.py +1138 -0
  11. ripperdoc/cli/ui/choice.py +376 -0
  12. ripperdoc/cli/ui/interrupt_listener.py +233 -0
  13. ripperdoc/cli/ui/message_display.py +7 -0
  14. ripperdoc/cli/ui/models_tui/__init__.py +5 -0
  15. ripperdoc/cli/ui/models_tui/textual_app.py +698 -0
  16. ripperdoc/cli/ui/panels.py +19 -4
  17. ripperdoc/cli/ui/permissions_tui/__init__.py +3 -0
  18. ripperdoc/cli/ui/permissions_tui/textual_app.py +526 -0
  19. ripperdoc/cli/ui/provider_options.py +220 -80
  20. ripperdoc/cli/ui/rich_ui.py +91 -83
  21. ripperdoc/cli/ui/tips.py +89 -0
  22. ripperdoc/cli/ui/wizard.py +98 -45
  23. ripperdoc/core/config.py +3 -0
  24. ripperdoc/core/permissions.py +66 -104
  25. ripperdoc/core/providers/anthropic.py +11 -0
  26. ripperdoc/protocol/stdio.py +3 -1
  27. ripperdoc/tools/bash_tool.py +2 -0
  28. ripperdoc/tools/file_edit_tool.py +100 -181
  29. ripperdoc/tools/file_read_tool.py +101 -25
  30. ripperdoc/tools/multi_edit_tool.py +239 -91
  31. ripperdoc/tools/notebook_edit_tool.py +11 -29
  32. ripperdoc/utils/file_editing.py +164 -0
  33. ripperdoc/utils/permissions/tool_permission_utils.py +11 -0
  34. {ripperdoc-0.3.0.dist-info → ripperdoc-0.3.2.dist-info}/METADATA +3 -2
  35. {ripperdoc-0.3.0.dist-info → ripperdoc-0.3.2.dist-info}/RECORD +39 -30
  36. ripperdoc/cli/ui/interrupt_handler.py +0 -208
  37. {ripperdoc-0.3.0.dist-info → ripperdoc-0.3.2.dist-info}/WHEEL +0 -0
  38. {ripperdoc-0.3.0.dist-info → ripperdoc-0.3.2.dist-info}/entry_points.txt +0 -0
  39. {ripperdoc-0.3.0.dist-info → ripperdoc-0.3.2.dist-info}/licenses/LICENSE +0 -0
  40. {ripperdoc-0.3.0.dist-info → ripperdoc-0.3.2.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.3.0"
3
+ __version__ = "0.3.2"
ripperdoc/cli/cli.py CHANGED
@@ -17,7 +17,11 @@ from ripperdoc.core.config import (
17
17
  get_project_config,
18
18
  )
19
19
  from ripperdoc.cli.ui.wizard import check_onboarding
20
- from ripperdoc.core.default_tools import get_default_tools, BUILTIN_TOOL_NAMES
20
+ from ripperdoc.core.default_tools import (
21
+ BUILTIN_TOOL_NAMES,
22
+ filter_tools_by_names,
23
+ get_default_tools,
24
+ )
21
25
  from ripperdoc.core.query import query, QueryContext
22
26
  from ripperdoc.core.system_prompt import build_system_prompt
23
27
  from ripperdoc.core.skills import build_skill_summary, load_all_skills
@@ -95,6 +99,7 @@ async def run_query(
95
99
  custom_system_prompt: Optional[str] = None,
96
100
  append_system_prompt: Optional[str] = None,
97
101
  model: Optional[str] = None,
102
+ allowed_tools: Optional[List[str]] = None,
98
103
  ) -> None:
99
104
  """Run a single query and print the response."""
100
105
  logger.info(
@@ -154,6 +159,8 @@ async def run_query(
154
159
  dynamic_tools = await load_dynamic_mcp_tools_async(Path.cwd())
155
160
  if dynamic_tools:
156
161
  tools = merge_tools_with_dynamic(tools, dynamic_tools)
162
+ if allowed_tools is not None:
163
+ tools = filter_tools_by_names(tools, allowed_tools)
157
164
  query_context.tools = tools
158
165
  mcp_instructions = format_mcp_instructions(servers)
159
166
  skill_result = load_all_skills(Path.cwd())
@@ -484,6 +491,7 @@ def cli(
484
491
  custom_system_prompt=system_prompt,
485
492
  append_system_prompt=append_system_prompt,
486
493
  model=model,
494
+ allowed_tools=allowed_tools,
487
495
  )
488
496
  )
489
497
  return
@@ -1,5 +1,8 @@
1
- from rich.markup import escape
1
+ import sys
2
+ from typing import Any, Dict
3
+
2
4
  from rich import box
5
+ from rich.markup import escape
3
6
  from rich.panel import Panel
4
7
  from rich.table import Table
5
8
 
@@ -18,7 +21,6 @@ from ripperdoc.tools.task_tool import (
18
21
  )
19
22
  from ripperdoc.utils.log import get_logger
20
23
 
21
- from typing import Any, Dict
22
24
  from .base import SlashCommand
23
25
 
24
26
  logger = get_logger()
@@ -43,6 +45,77 @@ def _format_duration(duration_ms: float | None) -> str:
43
45
  return f"{hours}h {mins}m"
44
46
 
45
47
 
48
+ def _print_agents_usage(console: Any) -> None:
49
+ console.print("[bold]/agents[/bold] — open interactive agents UI")
50
+ console.print("[bold]/agents tui[/bold] — open interactive agents UI")
51
+ console.print("[bold]/agents list[/bold] — list configured agents (plain)")
52
+ console.print(
53
+ "[bold]/agents create <name> [location] [model][/bold] — "
54
+ "create agent (location: user|project, default user)"
55
+ )
56
+ console.print("[bold]/agents edit <name> [location][/bold] — edit an existing agent")
57
+ console.print(
58
+ "[bold]/agents delete <name> [location][/bold] — "
59
+ "delete agent (location: user|project, default user)"
60
+ )
61
+ console.print("[bold]/agents runs[/bold] — list subagent runs")
62
+ console.print("[bold]/agents show <id>[/bold] — show subagent run details")
63
+ console.print("[bold]/agents cancel <id>[/bold] — cancel a background subagent run")
64
+ console.print(
65
+ f"[dim]Agent files live in ~/.ripperdoc/{AGENT_DIR_NAME} "
66
+ f"or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
67
+ )
68
+ console.print(
69
+ "[dim]Model can be a profile name or pointer (main/quick). Defaults to 'main'.[/dim]"
70
+ )
71
+
72
+
73
+ def _render_agents_plain(console: Any) -> None:
74
+ agents = load_agent_definitions()
75
+ console.print("\n[bold]Agents:[/bold]")
76
+ _print_agents_usage(console)
77
+ if not agents.active_agents:
78
+ console.print(" • None configured")
79
+ for agent in agents.active_agents:
80
+ location = getattr(agent.location, "value", agent.location)
81
+ tools_str = "all tools" if "*" in agent.tools else ", ".join(agent.tools)
82
+ console.print(f" • {escape(agent.agent_type)} ({escape(str(location))})", markup=False)
83
+ console.print(f" {escape(agent.when_to_use)}", markup=False)
84
+ console.print(f" tools: {escape(tools_str)}", markup=False)
85
+ console.print(f" model: {escape(agent.model or 'main (default)')}", markup=False)
86
+ if agents.failed_files:
87
+ console.print("[yellow]Some agent files could not be loaded:[/yellow]")
88
+ for path, error in agents.failed_files:
89
+ console.print(f" - {escape(str(path))}: {escape(str(error))}", markup=False)
90
+ console.print(
91
+ f"[dim]Add agents in ~/.ripperdoc/{AGENT_DIR_NAME} or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
92
+ )
93
+
94
+
95
+ def _handle_agents_tui(ui: Any) -> bool:
96
+ console = ui.console
97
+ if not sys.stdin.isatty():
98
+ console.print("[yellow]Interactive UI requires a TTY. Showing plain list instead.[/yellow]")
99
+ _render_agents_plain(console)
100
+ return True
101
+
102
+ try:
103
+ from ripperdoc.cli.ui.agents_tui import run_agents_tui
104
+ except (ImportError, ModuleNotFoundError) as exc:
105
+ console.print(
106
+ f"[yellow]Textual UI not available ({escape(str(exc))}). Showing plain list.[/yellow]"
107
+ )
108
+ _render_agents_plain(console)
109
+ return True
110
+
111
+ try:
112
+ return bool(run_agents_tui())
113
+ except Exception as exc: # noqa: BLE001 - fail safe in interactive UI
114
+ console.print(f"[red]Textual UI failed: {escape(str(exc))}[/red]")
115
+ _render_agents_plain(console)
116
+ return True
117
+
118
+
46
119
  def _handle(ui: Any, trimmed_arg: str) -> bool:
47
120
  console = ui.console
48
121
  tokens = trimmed_arg.split()
@@ -50,35 +123,20 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
50
123
  logger.info(
51
124
  "[agents_cmd] Handling /agents command",
52
125
  extra={
53
- "subcommand": subcmd or "list",
126
+ "subcommand": subcmd or "tui",
54
127
  "session_id": getattr(ui, "session_id", None),
55
128
  },
56
129
  )
57
130
 
58
- def print_agents_usage() -> None:
59
- console.print("[bold]/agents[/bold] — list configured agents")
60
- console.print(
61
- "[bold]/agents create <name> [location] [model][/bold] — "
62
- "create agent (location: user|project, default user)"
63
- )
64
- console.print("[bold]/agents edit <name> [location][/bold] — edit an existing agent")
65
- console.print(
66
- "[bold]/agents delete <name> [location][/bold] — "
67
- "delete agent (location: user|project, default user)"
68
- )
69
- console.print("[bold]/agents runs[/bold] — list subagent runs")
70
- console.print("[bold]/agents show <id>[/bold] — show subagent run details")
71
- console.print("[bold]/agents cancel <id>[/bold] — cancel a background subagent run")
72
- console.print(
73
- f"[dim]Agent files live in ~/.ripperdoc/{AGENT_DIR_NAME} "
74
- f"or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
75
- )
76
- console.print(
77
- "[dim]Model can be a profile name or pointer (main/quick). Defaults to 'main'.[/dim]"
78
- )
79
-
80
131
  if subcmd in ("help", "-h", "--help"):
81
- print_agents_usage()
132
+ _print_agents_usage(console)
133
+ return True
134
+
135
+ if subcmd in ("", "tui", "ui"):
136
+ return _handle_agents_tui(ui)
137
+
138
+ if subcmd in ("list", "ls"):
139
+ _render_agents_plain(console)
82
140
  return True
83
141
 
84
142
  if subcmd in ("runs", "run", "tasks", "status"):
@@ -178,7 +236,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
178
236
  agent_name = tokens[1] if len(tokens) > 1 else console.input("Agent name: ").strip()
179
237
  if not agent_name:
180
238
  console.print("[red]Agent name is required.[/red]")
181
- print_agents_usage()
239
+ _print_agents_usage(console)
182
240
  return True
183
241
 
184
242
  description = console.input("Description (when to use this agent): ").strip()
@@ -192,7 +250,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
192
250
  system_prompt = console.input("System prompt (single line, use \\n for newlines): ").strip()
193
251
  if not system_prompt:
194
252
  console.print("[red]System prompt is required.[/red]")
195
- print_agents_usage()
253
+ _print_agents_usage(console)
196
254
  return True
197
255
 
198
256
  location_arg = tokens[2] if len(tokens) > 2 else ""
@@ -236,7 +294,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
236
294
  )
237
295
  except (OSError, IOError, PermissionError, ValueError) as exc:
238
296
  console.print(f"[red]Failed to create agent: {escape(str(exc))}[/red]")
239
- print_agents_usage()
297
+ _print_agents_usage(console)
240
298
  logger.warning(
241
299
  "[agents_cmd] Failed to create agent: %s: %s",
242
300
  type(exc).__name__,
@@ -251,7 +309,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
251
309
  )
252
310
  if not agent_name:
253
311
  console.print("[red]Agent name is required.[/red]")
254
- print_agents_usage()
312
+ _print_agents_usage(console)
255
313
  return True
256
314
 
257
315
  location_raw = (
@@ -269,7 +327,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
269
327
  console.print(f"[yellow]{escape(str(exc))}[/yellow]")
270
328
  except (OSError, IOError, PermissionError, ValueError) as exc:
271
329
  console.print(f"[red]Failed to delete agent: {escape(str(exc))}[/red]")
272
- print_agents_usage()
330
+ _print_agents_usage(console)
273
331
  logger.warning(
274
332
  "[agents_cmd] Failed to delete agent: %s: %s",
275
333
  type(exc).__name__,
@@ -282,14 +340,14 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
282
340
  agent_name = tokens[1] if len(tokens) > 1 else console.input("Agent to edit: ").strip()
283
341
  if not agent_name:
284
342
  console.print("[red]Agent name is required.[/red]")
285
- print_agents_usage()
343
+ _print_agents_usage(console)
286
344
  return True
287
345
 
288
346
  agents = load_agent_definitions()
289
347
  target_agent = next((a for a in agents.active_agents if a.agent_type == agent_name), None)
290
348
  if not target_agent:
291
349
  console.print(f"[red]Agent '{escape(agent_name)}' not found.[/red]")
292
- print_agents_usage()
350
+ _print_agents_usage(console)
293
351
  return True
294
352
  if target_agent.location == AgentLocation.BUILT_IN:
295
353
  console.print("[yellow]Built-in agents cannot be edited.[/yellow]")
@@ -348,7 +406,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
348
406
  )
349
407
  except (OSError, IOError, PermissionError, ValueError) as exc:
350
408
  console.print(f"[red]Failed to update agent: {escape(str(exc))}[/red]")
351
- print_agents_usage()
409
+ _print_agents_usage(console)
352
410
  logger.warning(
353
411
  "[agents_cmd] Failed to update agent: %s: %s",
354
412
  type(exc).__name__,
@@ -357,25 +415,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
357
415
  )
358
416
  return True
359
417
 
360
- agents = load_agent_definitions()
361
- console.print("\n[bold]Agents:[/bold]")
362
- print_agents_usage()
363
- if not agents.active_agents:
364
- console.print(" • None configured")
365
- for agent in agents.active_agents:
366
- location = getattr(agent.location, "value", agent.location)
367
- tools_str = "all tools" if "*" in agent.tools else ", ".join(agent.tools)
368
- console.print(f" • {escape(agent.agent_type)} ({escape(str(location))})", markup=False)
369
- console.print(f" {escape(agent.when_to_use)}", markup=False)
370
- console.print(f" tools: {escape(tools_str)}", markup=False)
371
- console.print(f" model: {escape(agent.model or 'main (default)')}", markup=False)
372
- if agents.failed_files:
373
- console.print("[yellow]Some agent files could not be loaded:[/yellow]")
374
- for path, error in agents.failed_files:
375
- console.print(f" - {escape(str(path))}: {escape(str(error))}", markup=False)
376
- console.print(
377
- f"[dim]Add agents in ~/.ripperdoc/{AGENT_DIR_NAME} or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
378
- )
418
+ _render_agents_plain(console)
379
419
  return True
380
420
 
381
421
 
@@ -48,6 +48,9 @@ def _handle(ui: Any, _: str) -> bool:
48
48
  ui.console.print(" Tools:")
49
49
  for tool in server.tools:
50
50
  desc = f" — {tool.description}" if tool.description else ""
51
+ # Truncate long descriptions
52
+ if tool.description and len(tool.description) > 80:
53
+ desc = f" — {tool.description[:77]}..."
51
54
  ui.console.print(f" • {tool.name}{desc}", markup=False)
52
55
  else:
53
56
  ui.console.print(" Tools: none discovered")