ripperdoc 0.2.9__py3-none-any.whl → 0.3.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/__init__.py +1 -1
- ripperdoc/cli/cli.py +379 -51
- ripperdoc/cli/commands/__init__.py +6 -0
- ripperdoc/cli/commands/agents_cmd.py +128 -5
- ripperdoc/cli/commands/clear_cmd.py +8 -0
- ripperdoc/cli/commands/doctor_cmd.py +29 -0
- ripperdoc/cli/commands/exit_cmd.py +1 -0
- ripperdoc/cli/commands/memory_cmd.py +2 -1
- ripperdoc/cli/commands/models_cmd.py +63 -7
- ripperdoc/cli/commands/resume_cmd.py +5 -0
- ripperdoc/cli/commands/skills_cmd.py +103 -0
- ripperdoc/cli/commands/stats_cmd.py +244 -0
- ripperdoc/cli/commands/status_cmd.py +10 -0
- ripperdoc/cli/commands/tasks_cmd.py +6 -3
- ripperdoc/cli/commands/themes_cmd.py +139 -0
- ripperdoc/cli/ui/file_mention_completer.py +63 -13
- ripperdoc/cli/ui/helpers.py +6 -3
- ripperdoc/cli/ui/interrupt_handler.py +34 -0
- ripperdoc/cli/ui/panels.py +14 -8
- ripperdoc/cli/ui/rich_ui.py +737 -47
- ripperdoc/cli/ui/spinner.py +93 -18
- ripperdoc/cli/ui/thinking_spinner.py +1 -2
- ripperdoc/cli/ui/tool_renderers.py +10 -9
- ripperdoc/cli/ui/wizard.py +24 -19
- ripperdoc/core/agents.py +14 -3
- ripperdoc/core/config.py +238 -6
- ripperdoc/core/default_tools.py +91 -10
- ripperdoc/core/hooks/events.py +4 -0
- ripperdoc/core/hooks/llm_callback.py +58 -0
- ripperdoc/core/hooks/manager.py +6 -0
- ripperdoc/core/permissions.py +160 -9
- ripperdoc/core/providers/openai.py +84 -28
- ripperdoc/core/query.py +489 -87
- ripperdoc/core/query_utils.py +17 -14
- ripperdoc/core/skills.py +1 -0
- ripperdoc/core/theme.py +298 -0
- ripperdoc/core/tool.py +15 -5
- ripperdoc/protocol/__init__.py +14 -0
- ripperdoc/protocol/models.py +300 -0
- ripperdoc/protocol/stdio.py +1453 -0
- ripperdoc/tools/background_shell.py +354 -139
- ripperdoc/tools/bash_tool.py +117 -22
- ripperdoc/tools/file_edit_tool.py +228 -50
- ripperdoc/tools/file_read_tool.py +154 -3
- ripperdoc/tools/file_write_tool.py +53 -11
- ripperdoc/tools/grep_tool.py +98 -8
- ripperdoc/tools/lsp_tool.py +609 -0
- ripperdoc/tools/multi_edit_tool.py +26 -3
- ripperdoc/tools/skill_tool.py +52 -1
- ripperdoc/tools/task_tool.py +539 -65
- ripperdoc/utils/conversation_compaction.py +1 -1
- ripperdoc/utils/file_watch.py +216 -7
- ripperdoc/utils/image_utils.py +125 -0
- ripperdoc/utils/log.py +30 -3
- ripperdoc/utils/lsp.py +812 -0
- ripperdoc/utils/mcp.py +80 -18
- ripperdoc/utils/message_formatting.py +7 -4
- ripperdoc/utils/messages.py +198 -33
- ripperdoc/utils/pending_messages.py +50 -0
- ripperdoc/utils/permissions/shell_command_validation.py +3 -3
- ripperdoc/utils/permissions/tool_permission_utils.py +180 -15
- ripperdoc/utils/platform.py +198 -0
- ripperdoc/utils/session_heatmap.py +242 -0
- ripperdoc/utils/session_history.py +2 -2
- ripperdoc/utils/session_stats.py +294 -0
- ripperdoc/utils/shell_utils.py +8 -5
- ripperdoc/utils/todo.py +0 -6
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/METADATA +55 -17
- ripperdoc-0.3.0.dist-info/RECORD +136 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/WHEEL +1 -1
- ripperdoc/sdk/__init__.py +0 -9
- ripperdoc/sdk/client.py +0 -333
- ripperdoc-0.2.9.dist-info/RECORD +0 -123
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/top_level.txt +0 -0
ripperdoc/cli/ui/spinner.py
CHANGED
|
@@ -1,45 +1,108 @@
|
|
|
1
1
|
from contextlib import contextmanager
|
|
2
|
+
import shutil
|
|
3
|
+
import sys
|
|
2
4
|
from typing import Any, Generator, Literal, Optional
|
|
3
5
|
|
|
4
6
|
from rich.console import Console
|
|
5
|
-
from rich.
|
|
6
|
-
from rich.
|
|
7
|
+
from rich.live import Live
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
from rich.spinner import Spinner as RichSpinner
|
|
10
|
+
|
|
11
|
+
from ripperdoc.core.theme import theme_color
|
|
12
|
+
|
|
13
|
+
# ANSI escape sequences for terminal control
|
|
14
|
+
_CLEAR_LINE = "\r\033[K" # Move to start of line and clear to end
|
|
7
15
|
|
|
8
16
|
|
|
9
17
|
class Spinner:
|
|
10
|
-
"""Lightweight spinner wrapper
|
|
18
|
+
"""Lightweight spinner wrapper that plays nicely with other console output."""
|
|
19
|
+
|
|
20
|
+
# Reserve space for spinner animation (e.g., "⠧ ") and safety margin
|
|
21
|
+
_SPINNER_MARGIN = 6
|
|
11
22
|
|
|
12
23
|
def __init__(self, console: Console, text: str = "Thinking...", spinner: str = "dots"):
|
|
13
24
|
self.console = console
|
|
14
25
|
self.text = text
|
|
15
26
|
self.spinner = spinner
|
|
16
|
-
self.
|
|
27
|
+
self._style = theme_color("spinner")
|
|
28
|
+
self._live: Optional[Live] = None
|
|
29
|
+
# Spinner color from theme for visual separation in the terminal
|
|
30
|
+
self._renderable: RichSpinner = RichSpinner(
|
|
31
|
+
spinner,
|
|
32
|
+
text=Text(self._fit_to_terminal(self.text), style=self._style),
|
|
33
|
+
style=self._style,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def _get_terminal_width(self) -> int:
|
|
37
|
+
"""Get current terminal width, with fallback."""
|
|
38
|
+
try:
|
|
39
|
+
return shutil.get_terminal_size().columns
|
|
40
|
+
except Exception:
|
|
41
|
+
return 80 # Reasonable default
|
|
42
|
+
|
|
43
|
+
def _fit_to_terminal(self, text: str) -> str:
|
|
44
|
+
"""Truncate text to fit within terminal width, preventing line wrap issues.
|
|
45
|
+
|
|
46
|
+
This ensures spinner text never causes terminal wrapping, which would
|
|
47
|
+
leave artifacts when the spinner refreshes or stops.
|
|
48
|
+
"""
|
|
49
|
+
max_width = self._get_terminal_width() - self._SPINNER_MARGIN
|
|
50
|
+
if max_width < 20:
|
|
51
|
+
max_width = 20 # Minimum usable width
|
|
52
|
+
|
|
53
|
+
if len(text) <= max_width:
|
|
54
|
+
return text
|
|
55
|
+
|
|
56
|
+
# Smart truncation: keep the structure intact
|
|
57
|
+
# Find the last complete parenthetical group if possible
|
|
58
|
+
truncated = text[: max_width - 1] + "…"
|
|
59
|
+
return truncated
|
|
60
|
+
|
|
61
|
+
def _clear_line(self) -> None:
|
|
62
|
+
"""Clear the current terminal line to prevent artifacts."""
|
|
63
|
+
if self.console.is_terminal:
|
|
64
|
+
try:
|
|
65
|
+
sys.stdout.write(_CLEAR_LINE)
|
|
66
|
+
sys.stdout.flush()
|
|
67
|
+
except Exception:
|
|
68
|
+
pass # Ignore errors in non-TTY environments
|
|
17
69
|
|
|
18
70
|
def start(self) -> None:
|
|
19
71
|
"""Start the spinner if not already running."""
|
|
20
|
-
|
|
21
|
-
if self._status is not None:
|
|
72
|
+
if self._live is not None:
|
|
22
73
|
return
|
|
23
|
-
|
|
24
|
-
|
|
74
|
+
# Clear any residual content on current line before starting
|
|
75
|
+
self._clear_line()
|
|
76
|
+
self._renderable.text = Text(self._fit_to_terminal(self.text), style=self._style)
|
|
77
|
+
self._live = Live(
|
|
78
|
+
self._renderable,
|
|
79
|
+
console=self.console,
|
|
80
|
+
transient=True, # Remove spinner line when stopped to avoid layout glitches
|
|
81
|
+
refresh_per_second=12,
|
|
82
|
+
vertical_overflow="ellipsis", # Prevent multi-line overflow issues
|
|
25
83
|
)
|
|
26
|
-
self.
|
|
84
|
+
self._live.start()
|
|
27
85
|
|
|
28
86
|
def update(self, text: Optional[str] = None) -> None:
|
|
29
87
|
"""Update spinner text."""
|
|
30
|
-
|
|
31
|
-
if self._status is None:
|
|
88
|
+
if self._live is None:
|
|
32
89
|
return
|
|
33
|
-
|
|
34
|
-
|
|
90
|
+
if text is not None:
|
|
91
|
+
self.text = text
|
|
92
|
+
self._renderable.text = Text(self._fit_to_terminal(self.text), style=self._style)
|
|
93
|
+
# Live.refresh() redraws the current renderable
|
|
94
|
+
self._live.refresh()
|
|
35
95
|
|
|
36
96
|
def stop(self) -> None:
|
|
37
97
|
"""Stop the spinner if running."""
|
|
38
|
-
|
|
39
|
-
if self._status is None:
|
|
98
|
+
if self._live is None:
|
|
40
99
|
return
|
|
41
|
-
|
|
42
|
-
|
|
100
|
+
try:
|
|
101
|
+
self._live.stop()
|
|
102
|
+
# Clear line to ensure no artifacts remain from long spinner text
|
|
103
|
+
self._clear_line()
|
|
104
|
+
finally:
|
|
105
|
+
self._live = None
|
|
43
106
|
|
|
44
107
|
def __enter__(self) -> "Spinner":
|
|
45
108
|
self.start()
|
|
@@ -53,7 +116,7 @@ class Spinner:
|
|
|
53
116
|
@property
|
|
54
117
|
def is_running(self) -> bool:
|
|
55
118
|
"""Check if spinner is currently running."""
|
|
56
|
-
return self.
|
|
119
|
+
return self._live is not None
|
|
57
120
|
|
|
58
121
|
@contextmanager
|
|
59
122
|
def paused(self) -> Generator[None, None, None]:
|
|
@@ -70,4 +133,16 @@ class Spinner:
|
|
|
70
133
|
yield
|
|
71
134
|
finally:
|
|
72
135
|
if was_running:
|
|
136
|
+
# Ensure all output is flushed and cursor is on a clean line
|
|
137
|
+
# before restarting the spinner
|
|
138
|
+
try:
|
|
139
|
+
# Flush console buffer
|
|
140
|
+
self.console.file.flush()
|
|
141
|
+
# Clear any partial line content to prevent spinner
|
|
142
|
+
# from appearing on the same line as previous output
|
|
143
|
+
if self.console.is_terminal:
|
|
144
|
+
sys.stdout.write(_CLEAR_LINE)
|
|
145
|
+
sys.stdout.flush()
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
73
148
|
self.start()
|
|
@@ -22,7 +22,6 @@ THINKING_WORDS: list[str] = [
|
|
|
22
22
|
"Cerebrating",
|
|
23
23
|
"Channelling",
|
|
24
24
|
"Churning",
|
|
25
|
-
"Clauding",
|
|
26
25
|
"Coalescing",
|
|
27
26
|
"Cogitating",
|
|
28
27
|
"Computing",
|
|
@@ -114,7 +113,7 @@ class ThinkingSpinner(Spinner):
|
|
|
114
113
|
|
|
115
114
|
def _format_text(self, suffix: Optional[str] = None) -> str:
|
|
116
115
|
elapsed = int(time.monotonic() - self.start_time)
|
|
117
|
-
base = f"
|
|
116
|
+
base = f" {self.thinking_word}… (esc to interrupt · {elapsed}s"
|
|
118
117
|
if self.out_tokens > 0:
|
|
119
118
|
base += f" · ↓ {self.out_tokens} tokens"
|
|
120
119
|
else:
|
|
@@ -43,7 +43,7 @@ class TodoResultRenderer(ToolResultRenderer):
|
|
|
43
43
|
if lines:
|
|
44
44
|
self.console.print(f" ⎿ [dim]{escape(lines[0])}[/]")
|
|
45
45
|
for line in lines[1:]:
|
|
46
|
-
self.console.print(f"
|
|
46
|
+
self.console.print(f" {line}", markup=False)
|
|
47
47
|
else:
|
|
48
48
|
self.console.print(" ⎿ [dim]Todo update[/]")
|
|
49
49
|
|
|
@@ -107,7 +107,7 @@ class GlobResultRenderer(ToolResultRenderer):
|
|
|
107
107
|
if self.verbose:
|
|
108
108
|
for line in files[:30]:
|
|
109
109
|
if line.strip():
|
|
110
|
-
self.console.print(f"
|
|
110
|
+
self.console.print(f" {line}", markup=False)
|
|
111
111
|
if file_count > 30:
|
|
112
112
|
self.console.print(f"[dim]... ({file_count - 30} more)[/]")
|
|
113
113
|
|
|
@@ -125,7 +125,7 @@ class GrepResultRenderer(ToolResultRenderer):
|
|
|
125
125
|
if self.verbose:
|
|
126
126
|
for line in matches[:30]:
|
|
127
127
|
if line.strip():
|
|
128
|
-
self.console.print(f"
|
|
128
|
+
self.console.print(f" {line}", markup=False)
|
|
129
129
|
if match_count > 30:
|
|
130
130
|
self.console.print(f"[dim]... ({match_count - 30} more)[/]")
|
|
131
131
|
|
|
@@ -142,7 +142,7 @@ class LSResultRenderer(ToolResultRenderer):
|
|
|
142
142
|
if self.verbose:
|
|
143
143
|
preview = tree_lines[:40]
|
|
144
144
|
for line in preview:
|
|
145
|
-
self.console.print(f"
|
|
145
|
+
self.console.print(f" {line}", markup=False)
|
|
146
146
|
if len(tree_lines) > len(preview):
|
|
147
147
|
self.console.print(f"[dim]... ({len(tree_lines) - len(preview)} more)[/]")
|
|
148
148
|
|
|
@@ -193,7 +193,8 @@ class BashResultRenderer(ToolResultRenderer):
|
|
|
193
193
|
preview = stdout_lines if self.verbose else stdout_lines[:5]
|
|
194
194
|
self.console.print(f" ⎿ {preview[0]}", markup=False)
|
|
195
195
|
for line in preview[1:]:
|
|
196
|
-
|
|
196
|
+
# Use consistent 4-space indent to match the ⎿ prefix width
|
|
197
|
+
self.console.print(f" {line}", markup=False)
|
|
197
198
|
if not self.verbose and len(stdout_lines) > len(preview):
|
|
198
199
|
self.console.print(f"[dim]... ({len(stdout_lines) - len(preview)} more lines)[/]")
|
|
199
200
|
else:
|
|
@@ -229,28 +230,28 @@ class BashResultRenderer(ToolResultRenderer):
|
|
|
229
230
|
preview = stdout_lines if self.verbose else stdout_lines[:5]
|
|
230
231
|
self.console.print("[dim]stdout:[/]")
|
|
231
232
|
for line in preview:
|
|
232
|
-
self.console.print(f"
|
|
233
|
+
self.console.print(f" {line}", markup=False)
|
|
233
234
|
if not self.verbose and len(stdout_lines) > len(preview):
|
|
234
235
|
self.console.print(
|
|
235
236
|
f"[dim]... ({len(stdout_lines) - len(preview)} more stdout lines)[/]"
|
|
236
237
|
)
|
|
237
238
|
else:
|
|
238
239
|
self.console.print("[dim]stdout:[/]")
|
|
239
|
-
self.console.print("
|
|
240
|
+
self.console.print(" [dim](no stdout)[/]")
|
|
240
241
|
|
|
241
242
|
# Render stderr
|
|
242
243
|
if stderr_lines:
|
|
243
244
|
preview = stderr_lines if self.verbose else stderr_lines[:5]
|
|
244
245
|
self.console.print("[dim]stderr:[/]")
|
|
245
246
|
for line in preview:
|
|
246
|
-
self.console.print(f"
|
|
247
|
+
self.console.print(f" {line}", markup=False)
|
|
247
248
|
if not self.verbose and len(stderr_lines) > len(preview):
|
|
248
249
|
self.console.print(
|
|
249
250
|
f"[dim]... ({len(stderr_lines) - len(preview)} more stderr lines)[/]"
|
|
250
251
|
)
|
|
251
252
|
else:
|
|
252
253
|
self.console.print("[dim]stderr:[/]")
|
|
253
|
-
self.console.print("
|
|
254
|
+
self.console.print(" [dim](no stderr)[/]")
|
|
254
255
|
|
|
255
256
|
|
|
256
257
|
class ToolResultRendererRegistry:
|
ripperdoc/cli/ui/wizard.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Interactive onboarding wizard for Ripperdoc.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
from typing import List, Optional, Tuple
|
|
6
7
|
|
|
7
8
|
import click
|
|
@@ -46,6 +47,17 @@ def check_onboarding() -> bool:
|
|
|
46
47
|
if config.has_completed_onboarding:
|
|
47
48
|
return True
|
|
48
49
|
|
|
50
|
+
# 检查是否有有效的 RIPPERDOC_* 环境变量配置
|
|
51
|
+
# 如果设置了 RIPPERDOC_BASE_URL,可以跳过 onboarding
|
|
52
|
+
# 不写入配置文件,只在内存中处理
|
|
53
|
+
if os.getenv("RIPPERDOC_BASE_URL"):
|
|
54
|
+
# 在内存中标记已完成 onboarding,但不保存到配置文件
|
|
55
|
+
# 这样下次启动时如果环境变量存在仍然可以工作
|
|
56
|
+
config.has_completed_onboarding = True
|
|
57
|
+
config.last_onboarding_version = get_version()
|
|
58
|
+
save_global_config(config)
|
|
59
|
+
return True
|
|
60
|
+
|
|
49
61
|
console.print("[bold cyan]Welcome to Ripperdoc![/bold cyan]\n")
|
|
50
62
|
console.print("Let's set up your AI model configuration.\n")
|
|
51
63
|
|
|
@@ -93,14 +105,12 @@ def run_onboarding_wizard(config: GlobalConfig) -> bool:
|
|
|
93
105
|
model_suggestions=(),
|
|
94
106
|
)
|
|
95
107
|
else:
|
|
96
|
-
provider_option = KNOWN_PROVIDERS.get(provider_choice)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
model_suggestions=(),
|
|
103
|
-
)
|
|
108
|
+
provider_option = KNOWN_PROVIDERS.get(provider_choice) or ProviderOption(
|
|
109
|
+
key=provider_choice,
|
|
110
|
+
protocol=ProviderType.OPENAI_COMPATIBLE,
|
|
111
|
+
default_model=default_model_for_protocol(ProviderType.OPENAI_COMPATIBLE),
|
|
112
|
+
model_suggestions=(),
|
|
113
|
+
)
|
|
104
114
|
|
|
105
115
|
api_key = ""
|
|
106
116
|
while not api_key:
|
|
@@ -137,7 +147,7 @@ def get_model_name_with_suggestions(
|
|
|
137
147
|
api_base_override: Optional[str],
|
|
138
148
|
) -> Tuple[str, Optional[str]]:
|
|
139
149
|
"""Get model name with provider-specific suggestions and default API base.
|
|
140
|
-
|
|
150
|
+
|
|
141
151
|
Returns:
|
|
142
152
|
Tuple of (model_name, api_base)
|
|
143
153
|
"""
|
|
@@ -154,7 +164,7 @@ def get_model_name_with_suggestions(
|
|
|
154
164
|
if suggestions:
|
|
155
165
|
console.print("\n[dim]Available models for this provider:[/dim]")
|
|
156
166
|
for i, model_name in enumerate(suggestions[:5]): # Show top 5
|
|
157
|
-
console.print(f" [dim]{i+1}. {model_name}[/dim]")
|
|
167
|
+
console.print(f" [dim]{i + 1}. {model_name}[/dim]")
|
|
158
168
|
console.print("")
|
|
159
169
|
|
|
160
170
|
# Prompt for model name
|
|
@@ -164,16 +174,12 @@ def get_model_name_with_suggestions(
|
|
|
164
174
|
model = click.prompt("Model name", default=default_model)
|
|
165
175
|
# Prompt for API base if still not set
|
|
166
176
|
if api_base is None:
|
|
167
|
-
api_base_input = click.prompt(
|
|
168
|
-
"API base URL (optional)", default="", show_default=False
|
|
169
|
-
)
|
|
177
|
+
api_base_input = click.prompt("API base URL (optional)", default="", show_default=False)
|
|
170
178
|
api_base = api_base_input or None
|
|
171
179
|
elif provider.protocol == ProviderType.GEMINI:
|
|
172
180
|
model = click.prompt("Model name", default=default_model)
|
|
173
181
|
if api_base is None:
|
|
174
|
-
api_base_input = click.prompt(
|
|
175
|
-
"API base URL (optional)", default="", show_default=False
|
|
176
|
-
)
|
|
182
|
+
api_base_input = click.prompt("API base URL (optional)", default="", show_default=False)
|
|
177
183
|
api_base = api_base_input or None
|
|
178
184
|
else:
|
|
179
185
|
model = click.prompt("Model name", default=default_model)
|
|
@@ -193,9 +199,7 @@ def get_context_window() -> Optional[int]:
|
|
|
193
199
|
try:
|
|
194
200
|
context_window = int(context_window_input.strip())
|
|
195
201
|
except ValueError:
|
|
196
|
-
console.print(
|
|
197
|
-
"[yellow]Invalid context window, using auto-detected defaults.[/yellow]"
|
|
198
|
-
)
|
|
202
|
+
console.print("[yellow]Invalid context window, using auto-detected defaults.[/yellow]")
|
|
199
203
|
return context_window
|
|
200
204
|
|
|
201
205
|
|
|
@@ -203,6 +207,7 @@ def get_version() -> str:
|
|
|
203
207
|
"""Get current version of Ripperdoc."""
|
|
204
208
|
try:
|
|
205
209
|
from ripperdoc import __version__
|
|
210
|
+
|
|
206
211
|
return __version__
|
|
207
212
|
except ImportError:
|
|
208
213
|
return "unknown"
|
ripperdoc/core/agents.py
CHANGED
|
@@ -10,6 +10,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
|
10
10
|
|
|
11
11
|
import yaml
|
|
12
12
|
|
|
13
|
+
from ripperdoc.utils.coerce import parse_boolish
|
|
13
14
|
from ripperdoc.utils.log import get_logger
|
|
14
15
|
from ripperdoc.tools.ask_user_question_tool import AskUserQuestionTool
|
|
15
16
|
from ripperdoc.tools.bash_output_tool import BashOutputTool
|
|
@@ -23,8 +24,10 @@ from ripperdoc.tools.glob_tool import GlobTool
|
|
|
23
24
|
from ripperdoc.tools.grep_tool import GrepTool
|
|
24
25
|
from ripperdoc.tools.kill_bash_tool import KillBashTool
|
|
25
26
|
from ripperdoc.tools.ls_tool import LSTool
|
|
27
|
+
from ripperdoc.tools.lsp_tool import LspTool
|
|
26
28
|
from ripperdoc.tools.multi_edit_tool import MultiEditTool
|
|
27
29
|
from ripperdoc.tools.notebook_edit_tool import NotebookEditTool
|
|
30
|
+
from ripperdoc.tools.skill_tool import SkillTool
|
|
28
31
|
from ripperdoc.tools.todo_tool import TodoReadTool, TodoWriteTool
|
|
29
32
|
from ripperdoc.tools.tool_search_tool import ToolSearchTool
|
|
30
33
|
from ripperdoc.tools.mcp_tools import (
|
|
@@ -65,6 +68,8 @@ TOOL_SEARCH_TOOL_NAME = _safe_tool_name(ToolSearchTool, "ToolSearch")
|
|
|
65
68
|
MCP_LIST_SERVERS_TOOL_NAME = _safe_tool_name(ListMcpServersTool, "ListMcpServers")
|
|
66
69
|
MCP_LIST_RESOURCES_TOOL_NAME = _safe_tool_name(ListMcpResourcesTool, "ListMcpResources")
|
|
67
70
|
MCP_READ_RESOURCE_TOOL_NAME = _safe_tool_name(ReadMcpResourceTool, "ReadMcpResource")
|
|
71
|
+
LSP_TOOL_NAME = _safe_tool_name(LspTool, "LSP")
|
|
72
|
+
SKILL_TOOL_NAME = _safe_tool_name(SkillTool, "Skill")
|
|
68
73
|
TASK_TOOL_NAME = "Task"
|
|
69
74
|
|
|
70
75
|
|
|
@@ -91,6 +96,7 @@ class AgentDefinition:
|
|
|
91
96
|
model: Optional[str] = None
|
|
92
97
|
color: Optional[str] = None
|
|
93
98
|
filename: Optional[str] = None
|
|
99
|
+
fork_context: bool = False
|
|
94
100
|
|
|
95
101
|
|
|
96
102
|
@dataclass
|
|
@@ -234,7 +240,7 @@ def _built_in_agents() -> List[AgentDefinition]:
|
|
|
234
240
|
system_prompt=EXPLORE_AGENT_PROMPT,
|
|
235
241
|
location=AgentLocation.BUILT_IN,
|
|
236
242
|
color="green",
|
|
237
|
-
model="
|
|
243
|
+
model="main",
|
|
238
244
|
),
|
|
239
245
|
AgentDefinition(
|
|
240
246
|
agent_type="plan",
|
|
@@ -324,8 +330,9 @@ def _parse_agent_file(
|
|
|
324
330
|
return None, f"Failed to read agent file {path}: {exc}"
|
|
325
331
|
|
|
326
332
|
frontmatter, body = _split_frontmatter(text)
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
error = frontmatter.get("__error__")
|
|
334
|
+
if error is not None:
|
|
335
|
+
return None, str(error)
|
|
329
336
|
|
|
330
337
|
agent_name = frontmatter.get("name")
|
|
331
338
|
description = frontmatter.get("description")
|
|
@@ -339,6 +346,7 @@ def _parse_agent_file(
|
|
|
339
346
|
color_value = frontmatter.get("color")
|
|
340
347
|
model = model_value if isinstance(model_value, str) else None
|
|
341
348
|
color = color_value if isinstance(color_value, str) else None
|
|
349
|
+
fork_context = parse_boolish(frontmatter.get("fork_context") or frontmatter.get("fork-context"))
|
|
342
350
|
|
|
343
351
|
agent = AgentDefinition(
|
|
344
352
|
agent_type=agent_name.strip(),
|
|
@@ -349,6 +357,7 @@ def _parse_agent_file(
|
|
|
349
357
|
model=model,
|
|
350
358
|
color=color,
|
|
351
359
|
filename=path.stem,
|
|
360
|
+
fork_context=fork_context,
|
|
352
361
|
)
|
|
353
362
|
return agent, None
|
|
354
363
|
|
|
@@ -404,6 +413,8 @@ def summarize_agent(agent: AgentDefinition) -> str:
|
|
|
404
413
|
tool_label = "all tools" if "*" in agent.tools else ", ".join(agent.tools)
|
|
405
414
|
location = getattr(agent.location, "value", agent.location)
|
|
406
415
|
details = [f"tools: {tool_label}"]
|
|
416
|
+
if agent.fork_context:
|
|
417
|
+
details.append("context: forked")
|
|
407
418
|
if agent.model:
|
|
408
419
|
details.append(f"model: {agent.model}")
|
|
409
420
|
return f"- {agent.agent_type} ({location}): {agent.when_to_use} [{'; '.join(details)}]"
|