ripperdoc 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. ripperdoc/cli/cli.py +9 -7
  2. ripperdoc/cli/commands/agents_cmd.py +1 -1
  3. ripperdoc/cli/commands/context_cmd.py +2 -2
  4. ripperdoc/cli/commands/cost_cmd.py +1 -1
  5. ripperdoc/cli/commands/resume_cmd.py +3 -3
  6. ripperdoc/cli/commands/status_cmd.py +5 -5
  7. ripperdoc/cli/commands/tasks_cmd.py +5 -5
  8. ripperdoc/cli/ui/context_display.py +4 -3
  9. ripperdoc/cli/ui/rich_ui.py +49 -34
  10. ripperdoc/cli/ui/spinner.py +3 -4
  11. ripperdoc/core/agents.py +6 -4
  12. ripperdoc/core/default_tools.py +10 -4
  13. ripperdoc/core/query.py +9 -7
  14. ripperdoc/core/tool.py +1 -1
  15. ripperdoc/sdk/client.py +1 -1
  16. ripperdoc/tools/bash_tool.py +4 -4
  17. ripperdoc/tools/file_edit_tool.py +2 -2
  18. ripperdoc/tools/file_read_tool.py +2 -2
  19. ripperdoc/tools/file_write_tool.py +8 -2
  20. ripperdoc/tools/glob_tool.py +2 -2
  21. ripperdoc/tools/grep_tool.py +2 -2
  22. ripperdoc/tools/ls_tool.py +3 -3
  23. ripperdoc/tools/mcp_tools.py +15 -9
  24. ripperdoc/tools/multi_edit_tool.py +2 -2
  25. ripperdoc/tools/notebook_edit_tool.py +3 -3
  26. ripperdoc/tools/task_tool.py +13 -5
  27. ripperdoc/tools/todo_tool.py +4 -4
  28. ripperdoc/tools/tool_search_tool.py +6 -4
  29. ripperdoc/utils/mcp.py +12 -4
  30. ripperdoc/utils/message_compaction.py +25 -9
  31. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/METADATA +1 -1
  32. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/RECORD +36 -36
  33. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/WHEEL +0 -0
  34. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/entry_points.txt +0 -0
  35. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/licenses/LICENSE +0 -0
  36. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/top_level.txt +0 -0
ripperdoc/cli/cli.py CHANGED
@@ -7,7 +7,7 @@ import asyncio
7
7
  import click
8
8
  import sys
9
9
  from pathlib import Path
10
- from typing import Optional
10
+ from typing import Any, Dict, List, Optional
11
11
 
12
12
  from ripperdoc import __version__
13
13
  from ripperdoc.core.config import (
@@ -47,13 +47,15 @@ async def run_query(
47
47
  can_use_tool = make_permission_checker(project_path, safe_mode) if safe_mode else None
48
48
 
49
49
  # Create initial user message
50
- messages = [create_user_message(prompt)]
50
+ from ripperdoc.utils.messages import UserMessage, AssistantMessage, ProgressMessage
51
+
52
+ messages: List[UserMessage | AssistantMessage | ProgressMessage] = [create_user_message(prompt)]
51
53
 
52
54
  # Create query context
53
55
  query_context = QueryContext(tools=tools, safe_mode=safe_mode, verbose=verbose)
54
56
 
55
57
  try:
56
- context = {}
58
+ context: Dict[str, Any] = {}
57
59
  # System prompt
58
60
  servers = await load_mcp_servers_async(Path.cwd())
59
61
  dynamic_tools = await load_dynamic_mcp_tools_async(Path.cwd())
@@ -79,7 +81,7 @@ async def run_query(
79
81
  async for message in query(
80
82
  messages, system_prompt, context, query_context, can_use_tool
81
83
  ):
82
- if message.type == "assistant":
84
+ if message.type == "assistant" and hasattr(message, "message"):
83
85
  # Print assistant message
84
86
  if isinstance(message.message.content, str):
85
87
  console.print(
@@ -105,19 +107,19 @@ async def run_query(
105
107
  if hasattr(block, "type") and block.type == "text":
106
108
  console.print(
107
109
  Panel(
108
- Markdown(block.text),
110
+ Markdown(block.text or ""),
109
111
  title="Ripperdoc",
110
112
  border_style="cyan",
111
113
  )
112
114
  )
113
115
 
114
- elif message.type == "progress":
116
+ elif message.type == "progress" and hasattr(message, "content"):
115
117
  # Print progress
116
118
  if verbose:
117
119
  console.print(f"[dim]Progress: {escape(str(message.content))}[/dim]")
118
120
 
119
121
  # Add message to history
120
- messages.append(message)
122
+ messages.append(message) # type: ignore[arg-type]
121
123
 
122
124
  except KeyboardInterrupt:
123
125
  console.print("\n[yellow]Interrupted by user[/yellow]")
@@ -172,7 +172,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
172
172
  console.print(escape(target_agent.system_prompt or "(empty)"), markup=False)
173
173
  system_prompt = (
174
174
  console.input(
175
- "System prompt (single line, use \\n for newlines) " "[Enter to keep current]: "
175
+ "System prompt (single line, use \\n for newlines) [Enter to keep current]: "
176
176
  ).strip()
177
177
  or target_agent.system_prompt
178
178
  )
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import json
3
+ from typing import List, Any
3
4
 
4
5
  from ripperdoc.cli.ui.helpers import get_profile_for_pointer
5
6
  from ripperdoc.cli.ui.context_display import format_tokens
@@ -20,7 +21,6 @@ from ripperdoc.utils.mcp import (
20
21
  shutdown_mcp_runtime,
21
22
  )
22
23
 
23
- from typing import Any
24
24
  from .base import SlashCommand
25
25
 
26
26
 
@@ -38,7 +38,7 @@ def _handle(ui: Any, _: str) -> bool:
38
38
  verbose=ui.verbose,
39
39
  )
40
40
 
41
- async def _load_servers():
41
+ async def _load_servers() -> List[Any]:
42
42
  try:
43
43
  return await load_mcp_servers_async(ui.project_path)
44
44
  finally:
@@ -58,7 +58,7 @@ def _handle(ui: Any, _: str) -> bool:
58
58
  line += f", {_fmt_tokens(stats.cache_read_input_tokens)} cache read"
59
59
  if stats.cache_creation_input_tokens:
60
60
  line += f", {_fmt_tokens(stats.cache_creation_input_tokens)} cache write"
61
- line += f" ({stats.requests} call" f"{'' if stats.requests == 1 else 's'}"
61
+ line += f" ({stats.requests} call{'' if stats.requests == 1 else 's'}"
62
62
  if stats.duration_ms:
63
63
  line += f", {_format_duration(stats.duration_ms)} total"
64
64
  line += ")"
@@ -17,7 +17,7 @@ def _format_time(dt: datetime) -> str:
17
17
  return dt.strftime("%Y-%m-%d %H:%M")
18
18
 
19
19
 
20
- def _choose_session(ui, arg: str) -> Optional[SessionSummary]:
20
+ def _choose_session(ui: Any, arg: str) -> Optional[SessionSummary]:
21
21
  sessions = list_session_summaries(ui.project_path)
22
22
  if not sessions:
23
23
  ui.console.print("[yellow]No saved sessions found for this project.[/yellow]")
@@ -30,7 +30,7 @@ def _choose_session(ui, arg: str) -> Optional[SessionSummary]:
30
30
  if 0 <= idx < len(sessions):
31
31
  return sessions[idx]
32
32
  ui.console.print(
33
- f"[red]Invalid session index {escape(str(idx))}. Choose 0-{len(sessions)-1}.[/red]"
33
+ f"[red]Invalid session index {escape(str(idx))}. Choose 0-{len(sessions) - 1}.[/red]"
34
34
  )
35
35
  else:
36
36
  # Treat arg as session id if it matches.
@@ -60,7 +60,7 @@ def _choose_session(ui, arg: str) -> Optional[SessionSummary]:
60
60
  idx = int(choice_text)
61
61
  if idx < 0 or idx >= len(sessions):
62
62
  ui.console.print(
63
- f"[red]Invalid session index {escape(str(idx))}. Choose 0-{len(sessions)-1}.[/red]"
63
+ f"[red]Invalid session index {escape(str(idx))}. Choose 0-{len(sessions) - 1}.[/red]"
64
64
  )
65
65
  return None
66
66
  return sessions[idx]
@@ -22,8 +22,8 @@ def _auth_token_display(profile: Optional[ModelProfile]) -> Tuple[str, Optional[
22
22
  if not profile:
23
23
  return ("Not configured", None)
24
24
 
25
- provider_value = profile.provider.value if hasattr(profile.provider, "value") else str(
26
- profile.provider
25
+ provider_value = (
26
+ profile.provider.value if hasattr(profile.provider, "value") else str(profile.provider)
27
27
  )
28
28
 
29
29
  env_candidates = api_key_env_candidates(profile.provider)
@@ -50,8 +50,8 @@ def _api_base_display(profile: Optional[ModelProfile]) -> str:
50
50
  ProviderType.GEMINI: "Gemini base URL",
51
51
  }
52
52
  label = label_map.get(profile.provider, "API base URL")
53
- provider_value = profile.provider.value if hasattr(profile.provider, "value") else str(
54
- profile.provider
53
+ provider_value = (
54
+ profile.provider.value if hasattr(profile.provider, "value") else str(profile.provider)
55
55
  )
56
56
 
57
57
  env_candidates = api_base_env_candidates(profile.provider)
@@ -81,7 +81,7 @@ def _memory_status_lines(memory_files: List[MemoryFile]) -> List[str]:
81
81
 
82
82
 
83
83
  def _setting_sources_summary(
84
- config,
84
+ config: Any,
85
85
  profile: Optional[ModelProfile],
86
86
  memory_files: List[MemoryFile],
87
87
  auth_env_var: Optional[str],
@@ -14,11 +14,11 @@ from ripperdoc.tools.background_shell import (
14
14
  list_background_tasks,
15
15
  )
16
16
 
17
- from typing import Any
17
+ from typing import Any, Optional
18
18
  from .base import SlashCommand
19
19
 
20
20
 
21
- def _format_duration(duration_ms) -> str:
21
+ def _format_duration(duration_ms: Optional[float]) -> str:
22
22
  """Render milliseconds into a short human-readable duration."""
23
23
  if duration_ms is None:
24
24
  return "-"
@@ -82,7 +82,7 @@ def _tail_lines(text: str, max_lines: int = 20, max_chars: int = 4000) -> str:
82
82
  return content
83
83
 
84
84
 
85
- def _list_tasks(ui) -> bool:
85
+ def _list_tasks(ui: Any) -> bool:
86
86
  console = ui.console
87
87
  task_ids = list_background_tasks()
88
88
 
@@ -128,7 +128,7 @@ def _list_tasks(ui) -> bool:
128
128
  return True
129
129
 
130
130
 
131
- def _kill_task(ui, task_id: str) -> bool:
131
+ def _kill_task(ui: Any, task_id: str) -> bool:
132
132
  console = ui.console
133
133
  try:
134
134
  status = get_background_status(task_id, consume=False)
@@ -160,7 +160,7 @@ def _kill_task(ui, task_id: str) -> bool:
160
160
  return True
161
161
 
162
162
 
163
- def _show_task(ui, task_id: str) -> bool:
163
+ def _show_task(ui: Any, task_id: str) -> bool:
164
164
  console = ui.console
165
165
  try:
166
166
  status = get_background_status(task_id, consume=False)
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
- from typing import Any, Dict, List, Optional
6
+ from typing import Any, Dict, List, Optional, Tuple
7
7
 
8
8
  from ripperdoc.utils.message_compaction import ContextBreakdown
9
9
 
@@ -205,7 +205,8 @@ def make_segment_grid(
205
205
 
206
206
  rows: List[str] = []
207
207
  for start in range(0, total_slots, per_row):
208
- rows.append(" ".join(icons[start : start + per_row]))
208
+ row_icons = [icon for icon in icons[start : start + per_row] if icon is not None]
209
+ rows.append(" ".join(row_icons))
209
210
  return rows
210
211
 
211
212
 
@@ -229,7 +230,7 @@ def context_usage_lines(
229
230
  grid_lines.append(f" {row}")
230
231
 
231
232
  # Textual stats (without additional mini bars).
232
- stats = [
233
+ stats: List[Tuple[str, Optional[int], Optional[float]]] = [
233
234
  (
234
235
  f"{styled_symbol('⛁', 'grey58')} System prompt",
235
236
  breakdown.system_prompt_tokens,
@@ -6,7 +6,7 @@ This module provides a clean, minimal terminal UI using Rich for the Ripperdoc a
6
6
  import asyncio
7
7
  import sys
8
8
  import uuid
9
- from typing import List, Dict, Any, Optional
9
+ from typing import List, Dict, Any, Optional, Union, Iterable
10
10
  from pathlib import Path
11
11
 
12
12
  from rich.console import Console
@@ -32,10 +32,9 @@ from ripperdoc.cli.commands import (
32
32
  slash_command_completions,
33
33
  )
34
34
  from ripperdoc.cli.ui.helpers import get_profile_for_pointer
35
- from ripperdoc.core.permissions import make_permission_checker, PermissionResult
35
+ from ripperdoc.core.permissions import make_permission_checker
36
36
  from ripperdoc.cli.ui.spinner import Spinner
37
37
  from ripperdoc.cli.ui.context_display import context_usage_lines
38
- from ripperdoc.utils.messages import create_user_message, create_assistant_message
39
38
  from ripperdoc.utils.message_compaction import (
40
39
  compact_messages,
41
40
  estimate_conversation_tokens,
@@ -53,6 +52,16 @@ from ripperdoc.tools.mcp_tools import load_dynamic_mcp_tools_async, merge_tools_
53
52
  from ripperdoc.utils.session_history import SessionHistory
54
53
  from ripperdoc.utils.memory import build_memory_instructions
55
54
  from ripperdoc.core.query import query_llm
55
+ from ripperdoc.utils.messages import (
56
+ UserMessage,
57
+ AssistantMessage,
58
+ ProgressMessage,
59
+ create_user_message,
60
+ create_assistant_message,
61
+ )
62
+
63
+ # Type alias for conversation messages
64
+ ConversationMessage = Union[UserMessage, AssistantMessage, ProgressMessage]
56
65
 
57
66
 
58
67
  console = Console()
@@ -105,8 +114,8 @@ class RichUI:
105
114
  self.console = console
106
115
  self.safe_mode = safe_mode
107
116
  self.verbose = verbose
108
- self.conversation_messages: List[Dict[str, Any]] = []
109
- self._saved_conversation: Optional[List[Dict[str, Any]]] = None
117
+ self.conversation_messages: List[ConversationMessage] = []
118
+ self._saved_conversation: Optional[List[ConversationMessage]] = None
110
119
  self.query_context: Optional[QueryContext] = None
111
120
  self._current_tool: Optional[str] = None
112
121
  self._should_exit: bool = False
@@ -122,7 +131,7 @@ class RichUI:
122
131
  )
123
132
 
124
133
  def _context_usage_lines(
125
- self, breakdown, model_label: str, auto_compact_enabled: bool
134
+ self, breakdown: Any, model_label: str, auto_compact_enabled: bool
126
135
  ) -> List[str]:
127
136
  return context_usage_lines(breakdown, model_label, auto_compact_enabled)
128
137
 
@@ -188,10 +197,10 @@ class RichUI:
188
197
  sender: str,
189
198
  content: str,
190
199
  is_tool: bool = False,
191
- tool_type: str = None,
192
- tool_args: dict = None,
200
+ tool_type: Optional[str] = None,
201
+ tool_args: Optional[dict] = None,
193
202
  tool_data: Any = None,
194
- ):
203
+ ) -> None:
195
204
  """Display a message in the conversation."""
196
205
  if not is_tool:
197
206
  self._print_human_or_assistant(sender, content)
@@ -426,12 +435,12 @@ class RichUI:
426
435
  if tool_data:
427
436
  timing = ""
428
437
  if duration_ms:
429
- timing = f" ({duration_ms/1000:.2f}s"
438
+ timing = f" ({duration_ms / 1000:.2f}s"
430
439
  if timeout_ms:
431
- timing += f" / timeout {timeout_ms/1000:.0f}s"
440
+ timing += f" / timeout {timeout_ms / 1000:.0f}s"
432
441
  timing += ")"
433
442
  elif timeout_ms:
434
- timing = f" (timeout {timeout_ms/1000:.0f}s)"
443
+ timing = f" (timeout {timeout_ms / 1000:.0f}s)"
435
444
  self.console.print(f" ⎿ [dim]Exit code {exit_code}{timing}[/]")
436
445
  else:
437
446
  self.console.print(" ⎿ [dim]Command executed[/]")
@@ -534,7 +543,7 @@ class RichUI:
534
543
  return "\n".join(parts)
535
544
  return ""
536
545
 
537
- def _render_transcript(self, messages: List[Dict[str, Any]]) -> str:
546
+ def _render_transcript(self, messages: List[ConversationMessage]) -> str:
538
547
  """Render a simple transcript for summarization."""
539
548
  lines: List[str] = []
540
549
  for msg in messages:
@@ -549,7 +558,7 @@ class RichUI:
549
558
  lines.append(f"{label}: {text}")
550
559
  return "\n".join(lines)
551
560
 
552
- def _extract_assistant_text(self, assistant_message) -> str:
561
+ def _extract_assistant_text(self, assistant_message: Any) -> str:
553
562
  """Extract plain text from an AssistantMessage."""
554
563
  if isinstance(assistant_message.message.content, str):
555
564
  return assistant_message.message.content
@@ -561,7 +570,7 @@ class RichUI:
561
570
  return "\n".join(parts)
562
571
  return ""
563
572
 
564
- async def process_query(self, user_input: str):
573
+ async def process_query(self, user_input: str) -> None:
565
574
  """Process a user query and display the response."""
566
575
  if not self.query_context:
567
576
  self.query_context = QueryContext(
@@ -598,11 +607,13 @@ class RichUI:
598
607
 
599
608
  config = get_global_config()
600
609
  model_profile = get_profile_for_pointer("main")
601
- max_context_tokens = get_remaining_context_tokens(model_profile, config.context_token_limit)
610
+ max_context_tokens = get_remaining_context_tokens(
611
+ model_profile, config.context_token_limit
612
+ )
602
613
  auto_compact_enabled = resolve_auto_compact_enabled(config)
603
614
  protocol = provider_protocol(model_profile.provider) if model_profile else "openai"
604
615
 
605
- used_tokens = estimate_used_tokens(messages, protocol=protocol)
616
+ used_tokens = estimate_used_tokens(messages, protocol=protocol) # type: ignore[arg-type]
606
617
  usage_status = get_context_usage_status(
607
618
  used_tokens, max_context_tokens, auto_compact_enabled
608
619
  )
@@ -619,11 +630,11 @@ class RichUI:
619
630
 
620
631
  if usage_status.should_auto_compact:
621
632
  original_messages = list(messages)
622
- compaction = compact_messages(messages, protocol=protocol)
633
+ compaction = compact_messages(messages, protocol=protocol) # type: ignore[arg-type]
623
634
  if compaction.was_compacted:
624
635
  if self._saved_conversation is None:
625
- self._saved_conversation = original_messages
626
- messages = compaction.messages
636
+ self._saved_conversation = original_messages # type: ignore[assignment]
637
+ messages = compaction.messages # type: ignore[assignment]
627
638
  console.print(
628
639
  f"[yellow]Auto-compacted conversation (saved ~{compaction.tokens_saved} tokens). "
629
640
  f"Estimated usage: {compaction.tokens_after}/{max_context_tokens} tokens.[/yellow]"
@@ -633,13 +644,14 @@ class RichUI:
633
644
  # Wrap permission checker to pause the spinner while waiting for user input.
634
645
  base_permission_checker = self._permission_checker
635
646
 
636
- async def permission_checker(tool, parsed_input):
647
+ async def permission_checker(tool: Any, parsed_input: Any) -> bool:
637
648
  if spinner:
638
649
  spinner.stop()
639
650
  try:
640
651
  if base_permission_checker is not None:
641
- return await base_permission_checker(tool, parsed_input)
642
- return PermissionResult(result=True)
652
+ result = await base_permission_checker(tool, parsed_input)
653
+ return result.result if hasattr(result, "result") else True
654
+ return True
643
655
  finally:
644
656
  if spinner:
645
657
  spinner.start()
@@ -652,9 +664,13 @@ class RichUI:
652
664
  try:
653
665
  spinner.start()
654
666
  async for message in query(
655
- messages, system_prompt, context, self.query_context, permission_checker
667
+ messages,
668
+ system_prompt,
669
+ context,
670
+ self.query_context,
671
+ permission_checker, # type: ignore[arg-type]
656
672
  ):
657
- if message.type == "assistant":
673
+ if message.type == "assistant" and isinstance(message, AssistantMessage):
658
674
  # Extract text content from assistant message
659
675
  if isinstance(message.message.content, str):
660
676
  self.display_message("Ripperdoc", message.message.content)
@@ -688,7 +704,7 @@ class RichUI:
688
704
  tool_registry[tool_use_id]["printed"] = True
689
705
  last_tool_name = tool_name
690
706
 
691
- elif message.type == "user":
707
+ elif message.type == "user" and isinstance(message, UserMessage):
692
708
  # Handle tool results - show summary instead of full content
693
709
  if isinstance(message.message.content, list):
694
710
  for block in message.message.content:
@@ -724,7 +740,7 @@ class RichUI:
724
740
  tool_data=tool_data,
725
741
  )
726
742
 
727
- elif message.type == "progress":
743
+ elif message.type == "progress" and isinstance(message, ProgressMessage):
728
744
  if self.verbose:
729
745
  self.display_message(
730
746
  "System", f"Progress: {message.content}", is_tool=True
@@ -740,7 +756,7 @@ class RichUI:
740
756
 
741
757
  # Add message to history
742
758
  self._log_message(message)
743
- messages.append(message)
759
+ messages.append(message) # type: ignore[arg-type]
744
760
  except Exception as e:
745
761
  self.display_message("System", f"Error: {str(e)}", is_tool=True)
746
762
  finally:
@@ -787,7 +803,7 @@ class RichUI:
787
803
  def __init__(self, completions: List):
788
804
  self.completions = completions
789
805
 
790
- def get_completions(self, document, complete_event):
806
+ def get_completions(self, document: Any, complete_event: Any) -> Iterable[Completion]:
791
807
  text = document.text_before_cursor
792
808
  if not text.startswith("/"):
793
809
  return
@@ -809,7 +825,7 @@ class RichUI:
809
825
  )
810
826
  return self._prompt_session
811
827
 
812
- def run(self):
828
+ def run(self) -> None:
813
829
  """Run the Rich-based interface."""
814
830
  # Display welcome panel
815
831
  console.print()
@@ -929,7 +945,7 @@ class RichUI:
929
945
 
930
946
  async def _summarize_conversation(
931
947
  self,
932
- messages: List[Dict[str, Any]],
948
+ messages: List[ConversationMessage],
933
949
  custom_instructions: str,
934
950
  ) -> str:
935
951
  """Summarize the given conversation using the configured model."""
@@ -949,12 +965,11 @@ class RichUI:
949
965
  instructions += f"\nCustom instructions: {custom_instructions.strip()}"
950
966
 
951
967
  user_content = (
952
- "Summarize the following conversation between a user and an assistant:\n\n"
953
- f"{transcript}"
968
+ f"Summarize the following conversation between a user and an assistant:\n\n{transcript}"
954
969
  )
955
970
 
956
971
  assistant_response = await query_llm(
957
- messages=[{"role": "user", "content": user_content}],
972
+ messages=[{"role": "user", "content": user_content}], # type: ignore[list-item]
958
973
  system_prompt=instructions,
959
974
  tools=[],
960
975
  max_thinking_tokens=0,
@@ -1,8 +1,7 @@
1
- from typing import Optional
2
-
3
- from typing import Any, Literal
1
+ from typing import Any, Literal, Optional
4
2
  from rich.console import Console
5
3
  from rich.markup import escape
4
+ from rich.status import Status
6
5
 
7
6
 
8
7
  class Spinner:
@@ -12,7 +11,7 @@ class Spinner:
12
11
  self.console = console
13
12
  self.text = text
14
13
  self.spinner = spinner
15
- self._status = None # type: Optional[object]
14
+ self._status: Optional[Status] = None
16
15
 
17
16
  def start(self) -> None:
18
17
  """Start the spinner if not already running."""
ripperdoc/core/agents.py CHANGED
@@ -6,7 +6,7 @@ from dataclasses import dataclass
6
6
  from enum import Enum
7
7
  from functools import lru_cache
8
8
  from pathlib import Path
9
- from typing import Dict, Iterable, List, Optional, Tuple
9
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
10
10
 
11
11
  import yaml
12
12
 
@@ -98,7 +98,7 @@ def _agent_dir_for_location(location: AgentLocation) -> Path:
98
98
  raise ValueError(f"Unsupported agent location: {location}")
99
99
 
100
100
 
101
- def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, object], str]:
101
+ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
102
102
  """Extract YAML frontmatter and body content."""
103
103
  lines = raw_text.splitlines()
104
104
  if len(lines) >= 3 and lines[0].strip() == "---":
@@ -151,8 +151,10 @@ def _parse_agent_file(
151
151
  return None, 'Missing required "description" field in frontmatter'
152
152
 
153
153
  tools = _normalize_tools(frontmatter.get("tools"))
154
- model = frontmatter.get("model") if isinstance(frontmatter.get("model"), str) else None
155
- color = frontmatter.get("color") if isinstance(frontmatter.get("color"), str) else None
154
+ model_value = frontmatter.get("model")
155
+ color_value = frontmatter.get("color")
156
+ model = model_value if isinstance(model_value, str) else None
157
+ color = color_value if isinstance(color_value, str) else None
156
158
 
157
159
  agent = AgentDefinition(
158
160
  agent_type=agent_name.strip(),
@@ -2,7 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import List
5
+ from typing import Any, List
6
+
7
+ from ripperdoc.core.tool import Tool
6
8
 
7
9
  from ripperdoc.tools.bash_tool import BashTool
8
10
  from ripperdoc.tools.bash_output_tool import BashOutputTool
@@ -26,9 +28,9 @@ from ripperdoc.tools.mcp_tools import (
26
28
  )
27
29
 
28
30
 
29
- def get_default_tools() -> List:
31
+ def get_default_tools() -> List[Tool[Any, Any]]:
30
32
  """Construct the default tool set (base tools + Task subagent launcher)."""
31
- base_tools = [
33
+ base_tools: List[Tool[Any, Any]] = [
32
34
  BashTool(),
33
35
  BashOutputTool(),
34
36
  KillBashTool(),
@@ -48,7 +50,11 @@ def get_default_tools() -> List:
48
50
  ReadMcpResourceTool(),
49
51
  ]
50
52
  try:
51
- base_tools.extend(load_dynamic_mcp_tools_sync())
53
+ mcp_tools = load_dynamic_mcp_tools_sync()
54
+ # Filter to ensure only Tool instances are added
55
+ for tool in mcp_tools:
56
+ if isinstance(tool, Tool):
57
+ base_tools.append(tool)
52
58
  except Exception:
53
59
  # If MCP runtime is not available, continue with base tools only.
54
60
  pass
ripperdoc/core/query.py CHANGED
@@ -307,7 +307,7 @@ async def query_llm(
307
307
  model=model_profile.model,
308
308
  max_tokens=model_profile.max_tokens,
309
309
  system=system_prompt,
310
- messages=normalized_messages,
310
+ messages=normalized_messages, # type: ignore[arg-type]
311
311
  tools=tool_schemas if tool_schemas else None, # type: ignore
312
312
  temperature=model_profile.temperature,
313
313
  )
@@ -331,7 +331,7 @@ async def query_llm(
331
331
  "type": "tool_use",
332
332
  "tool_use_id": block.id,
333
333
  "name": block.name,
334
- "input": block.input,
334
+ "input": block.input, # type: ignore[dict-item]
335
335
  }
336
336
  )
337
337
 
@@ -367,22 +367,22 @@ async def query_llm(
367
367
  ] + normalized_messages
368
368
 
369
369
  # Make the API call
370
- response = await client.chat.completions.create(
370
+ openai_response: Any = await client.chat.completions.create(
371
371
  model=model_profile.model,
372
372
  messages=openai_messages,
373
- tools=openai_tools if openai_tools else None,
373
+ tools=openai_tools if openai_tools else None, # type: ignore[arg-type]
374
374
  temperature=model_profile.temperature,
375
375
  max_tokens=model_profile.max_tokens,
376
376
  )
377
377
 
378
378
  duration_ms = (time.time() - start_time) * 1000
379
- usage_tokens = _openai_usage_tokens(getattr(response, "usage", None))
379
+ usage_tokens = _openai_usage_tokens(getattr(openai_response, "usage", None))
380
380
  record_usage(model_profile.model, duration_ms=duration_ms, **usage_tokens)
381
381
  cost_usd = 0.0 # TODO: Implement cost calculation
382
382
 
383
383
  # Convert OpenAI response to our format
384
384
  content_blocks = []
385
- choice = response.choices[0]
385
+ choice = openai_response.choices[0]
386
386
 
387
387
  if choice.message.content:
388
388
  content_blocks.append({"type": "text", "text": choice.message.content})
@@ -538,6 +538,8 @@ async def query(
538
538
 
539
539
  for tool_use in tool_use_blocks:
540
540
  tool_name = tool_use.name
541
+ if not tool_name:
542
+ continue
541
543
  tool_id = getattr(tool_use, "tool_use_id", None) or getattr(tool_use, "id", None) or ""
542
544
  tool_input = getattr(tool_use, "input", {}) or {}
543
545
 
@@ -545,7 +547,7 @@ async def query(
545
547
  tool = query_context.tool_registry.get(tool_name)
546
548
  # Auto-activate when used so subsequent rounds list it as active.
547
549
  if tool:
548
- query_context.activate_tools([tool_name])
550
+ query_context.activate_tools([tool_name]) # type: ignore[list-item]
549
551
 
550
552
  if not tool:
551
553
  # Tool not found
ripperdoc/core/tool.py CHANGED
@@ -57,7 +57,7 @@ class ToolUseExample(BaseModel):
57
57
  model_config = ConfigDict(
58
58
  validate_by_alias=True,
59
59
  validate_by_name=True,
60
- serialization_aliases={"example": "input"},
60
+ serialization_aliases={"example": "input"}, # type: ignore[typeddict-item]
61
61
  )
62
62
 
63
63
 
ripperdoc/sdk/client.py CHANGED
@@ -157,7 +157,7 @@ class RipperdocClient:
157
157
  await self.connect()
158
158
  return self
159
159
 
160
- async def __aexit__(self, exc_type, exc, tb) -> None: # type: ignore[override]
160
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None: # type: ignore[override]
161
161
  await self.disconnect()
162
162
 
163
163
  async def connect(self, prompt: Optional[str] = None) -> None: