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.
- ripperdoc/cli/cli.py +9 -7
- ripperdoc/cli/commands/agents_cmd.py +1 -1
- ripperdoc/cli/commands/context_cmd.py +2 -2
- ripperdoc/cli/commands/cost_cmd.py +1 -1
- ripperdoc/cli/commands/resume_cmd.py +3 -3
- ripperdoc/cli/commands/status_cmd.py +5 -5
- ripperdoc/cli/commands/tasks_cmd.py +5 -5
- ripperdoc/cli/ui/context_display.py +4 -3
- ripperdoc/cli/ui/rich_ui.py +49 -34
- ripperdoc/cli/ui/spinner.py +3 -4
- ripperdoc/core/agents.py +6 -4
- ripperdoc/core/default_tools.py +10 -4
- ripperdoc/core/query.py +9 -7
- ripperdoc/core/tool.py +1 -1
- ripperdoc/sdk/client.py +1 -1
- ripperdoc/tools/bash_tool.py +4 -4
- ripperdoc/tools/file_edit_tool.py +2 -2
- ripperdoc/tools/file_read_tool.py +2 -2
- ripperdoc/tools/file_write_tool.py +8 -2
- ripperdoc/tools/glob_tool.py +2 -2
- ripperdoc/tools/grep_tool.py +2 -2
- ripperdoc/tools/ls_tool.py +3 -3
- ripperdoc/tools/mcp_tools.py +15 -9
- ripperdoc/tools/multi_edit_tool.py +2 -2
- ripperdoc/tools/notebook_edit_tool.py +3 -3
- ripperdoc/tools/task_tool.py +13 -5
- ripperdoc/tools/todo_tool.py +4 -4
- ripperdoc/tools/tool_search_tool.py +6 -4
- ripperdoc/utils/mcp.py +12 -4
- ripperdoc/utils/message_compaction.py +25 -9
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/METADATA +1 -1
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/RECORD +36 -36
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/WHEEL +0 -0
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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)
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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,
|
ripperdoc/cli/ui/rich_ui.py
CHANGED
|
@@ -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
|
|
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[
|
|
109
|
-
self._saved_conversation: Optional[List[
|
|
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[
|
|
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(
|
|
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
|
-
|
|
642
|
-
|
|
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,
|
|
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[
|
|
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,
|
ripperdoc/cli/ui/spinner.py
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
155
|
-
|
|
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(),
|
ripperdoc/core/default_tools.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
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:
|