ripperdoc 0.2.6__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 +3 -0
- ripperdoc/__main__.py +20 -0
- ripperdoc/cli/__init__.py +1 -0
- ripperdoc/cli/cli.py +405 -0
- ripperdoc/cli/commands/__init__.py +82 -0
- ripperdoc/cli/commands/agents_cmd.py +263 -0
- ripperdoc/cli/commands/base.py +19 -0
- ripperdoc/cli/commands/clear_cmd.py +18 -0
- ripperdoc/cli/commands/compact_cmd.py +23 -0
- ripperdoc/cli/commands/config_cmd.py +31 -0
- ripperdoc/cli/commands/context_cmd.py +144 -0
- ripperdoc/cli/commands/cost_cmd.py +82 -0
- ripperdoc/cli/commands/doctor_cmd.py +221 -0
- ripperdoc/cli/commands/exit_cmd.py +19 -0
- ripperdoc/cli/commands/help_cmd.py +20 -0
- ripperdoc/cli/commands/mcp_cmd.py +70 -0
- ripperdoc/cli/commands/memory_cmd.py +202 -0
- ripperdoc/cli/commands/models_cmd.py +413 -0
- ripperdoc/cli/commands/permissions_cmd.py +302 -0
- ripperdoc/cli/commands/resume_cmd.py +98 -0
- ripperdoc/cli/commands/status_cmd.py +167 -0
- ripperdoc/cli/commands/tasks_cmd.py +278 -0
- ripperdoc/cli/commands/todos_cmd.py +69 -0
- ripperdoc/cli/commands/tools_cmd.py +19 -0
- ripperdoc/cli/ui/__init__.py +1 -0
- ripperdoc/cli/ui/context_display.py +298 -0
- ripperdoc/cli/ui/helpers.py +22 -0
- ripperdoc/cli/ui/rich_ui.py +1557 -0
- ripperdoc/cli/ui/spinner.py +49 -0
- ripperdoc/cli/ui/thinking_spinner.py +128 -0
- ripperdoc/cli/ui/tool_renderers.py +298 -0
- ripperdoc/core/__init__.py +1 -0
- ripperdoc/core/agents.py +486 -0
- ripperdoc/core/commands.py +33 -0
- ripperdoc/core/config.py +559 -0
- ripperdoc/core/default_tools.py +88 -0
- ripperdoc/core/permissions.py +252 -0
- ripperdoc/core/providers/__init__.py +47 -0
- ripperdoc/core/providers/anthropic.py +250 -0
- ripperdoc/core/providers/base.py +265 -0
- ripperdoc/core/providers/gemini.py +615 -0
- ripperdoc/core/providers/openai.py +487 -0
- ripperdoc/core/query.py +1058 -0
- ripperdoc/core/query_utils.py +622 -0
- ripperdoc/core/skills.py +295 -0
- ripperdoc/core/system_prompt.py +431 -0
- ripperdoc/core/tool.py +240 -0
- ripperdoc/sdk/__init__.py +9 -0
- ripperdoc/sdk/client.py +333 -0
- ripperdoc/tools/__init__.py +1 -0
- ripperdoc/tools/ask_user_question_tool.py +431 -0
- ripperdoc/tools/background_shell.py +389 -0
- ripperdoc/tools/bash_output_tool.py +98 -0
- ripperdoc/tools/bash_tool.py +1016 -0
- ripperdoc/tools/dynamic_mcp_tool.py +428 -0
- ripperdoc/tools/enter_plan_mode_tool.py +226 -0
- ripperdoc/tools/exit_plan_mode_tool.py +153 -0
- ripperdoc/tools/file_edit_tool.py +346 -0
- ripperdoc/tools/file_read_tool.py +203 -0
- ripperdoc/tools/file_write_tool.py +205 -0
- ripperdoc/tools/glob_tool.py +179 -0
- ripperdoc/tools/grep_tool.py +370 -0
- ripperdoc/tools/kill_bash_tool.py +136 -0
- ripperdoc/tools/ls_tool.py +471 -0
- ripperdoc/tools/mcp_tools.py +591 -0
- ripperdoc/tools/multi_edit_tool.py +456 -0
- ripperdoc/tools/notebook_edit_tool.py +386 -0
- ripperdoc/tools/skill_tool.py +205 -0
- ripperdoc/tools/task_tool.py +379 -0
- ripperdoc/tools/todo_tool.py +494 -0
- ripperdoc/tools/tool_search_tool.py +380 -0
- ripperdoc/utils/__init__.py +1 -0
- ripperdoc/utils/bash_constants.py +51 -0
- ripperdoc/utils/bash_output_utils.py +43 -0
- ripperdoc/utils/coerce.py +34 -0
- ripperdoc/utils/context_length_errors.py +252 -0
- ripperdoc/utils/exit_code_handlers.py +241 -0
- ripperdoc/utils/file_watch.py +135 -0
- ripperdoc/utils/git_utils.py +274 -0
- ripperdoc/utils/json_utils.py +27 -0
- ripperdoc/utils/log.py +176 -0
- ripperdoc/utils/mcp.py +560 -0
- ripperdoc/utils/memory.py +253 -0
- ripperdoc/utils/message_compaction.py +676 -0
- ripperdoc/utils/messages.py +519 -0
- ripperdoc/utils/output_utils.py +258 -0
- ripperdoc/utils/path_ignore.py +677 -0
- ripperdoc/utils/path_utils.py +46 -0
- ripperdoc/utils/permissions/__init__.py +27 -0
- ripperdoc/utils/permissions/path_validation_utils.py +174 -0
- ripperdoc/utils/permissions/shell_command_validation.py +552 -0
- ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
- ripperdoc/utils/prompt.py +17 -0
- ripperdoc/utils/safe_get_cwd.py +31 -0
- ripperdoc/utils/sandbox_utils.py +38 -0
- ripperdoc/utils/session_history.py +260 -0
- ripperdoc/utils/session_usage.py +117 -0
- ripperdoc/utils/shell_token_utils.py +95 -0
- ripperdoc/utils/shell_utils.py +159 -0
- ripperdoc/utils/todo.py +203 -0
- ripperdoc/utils/token_estimation.py +34 -0
- ripperdoc-0.2.6.dist-info/METADATA +193 -0
- ripperdoc-0.2.6.dist-info/RECORD +107 -0
- ripperdoc-0.2.6.dist-info/WHEEL +5 -0
- ripperdoc-0.2.6.dist-info/entry_points.txt +3 -0
- ripperdoc-0.2.6.dist-info/licenses/LICENSE +53 -0
- ripperdoc-0.2.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
from rich.markup import escape
|
|
2
|
+
|
|
3
|
+
from ripperdoc.core.agents import (
|
|
4
|
+
AGENT_DIR_NAME,
|
|
5
|
+
AgentLocation,
|
|
6
|
+
delete_agent_definition,
|
|
7
|
+
load_agent_definitions,
|
|
8
|
+
save_agent_definition,
|
|
9
|
+
)
|
|
10
|
+
from ripperdoc.core.config import get_global_config
|
|
11
|
+
from ripperdoc.utils.log import get_logger
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
from .base import SlashCommand
|
|
15
|
+
|
|
16
|
+
logger = get_logger()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
20
|
+
console = ui.console
|
|
21
|
+
tokens = trimmed_arg.split()
|
|
22
|
+
subcmd = tokens[0].lower() if tokens else ""
|
|
23
|
+
logger.info(
|
|
24
|
+
"[agents_cmd] Handling /agents command",
|
|
25
|
+
extra={
|
|
26
|
+
"subcommand": subcmd or "list",
|
|
27
|
+
"session_id": getattr(ui, "session_id", None),
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def print_agents_usage() -> None:
|
|
32
|
+
console.print("[bold]/agents[/bold] — list configured agents")
|
|
33
|
+
console.print(
|
|
34
|
+
"[bold]/agents create <name> [location] [model][/bold] — "
|
|
35
|
+
"create agent (location: user|project, default user)"
|
|
36
|
+
)
|
|
37
|
+
console.print("[bold]/agents edit <name> [location][/bold] — edit an existing agent")
|
|
38
|
+
console.print(
|
|
39
|
+
"[bold]/agents delete <name> [location][/bold] — "
|
|
40
|
+
"delete agent (location: user|project, default user)"
|
|
41
|
+
)
|
|
42
|
+
console.print(
|
|
43
|
+
f"[dim]Agent files live in ~/.ripperdoc/{AGENT_DIR_NAME} "
|
|
44
|
+
f"or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
|
|
45
|
+
)
|
|
46
|
+
console.print(
|
|
47
|
+
"[dim]Model can be a profile name or pointer (task/main/etc). Defaults to 'task'.[/dim]"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if subcmd in ("help", "-h", "--help"):
|
|
51
|
+
print_agents_usage()
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
if subcmd in ("create", "add"):
|
|
55
|
+
agent_name = tokens[1] if len(tokens) > 1 else console.input("Agent name: ").strip()
|
|
56
|
+
if not agent_name:
|
|
57
|
+
console.print("[red]Agent name is required.[/red]")
|
|
58
|
+
print_agents_usage()
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
description = console.input("Description (when to use this agent): ").strip()
|
|
62
|
+
if not description:
|
|
63
|
+
console.print("[red]Description is required.[/red]")
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
tools_input = console.input("Tools (comma-separated, * for all) [*]: ").strip() or "*"
|
|
67
|
+
tools = [t.strip() for t in tools_input.split(",") if t.strip()] or ["*"]
|
|
68
|
+
|
|
69
|
+
system_prompt = console.input("System prompt (single line, use \\n for newlines): ").strip()
|
|
70
|
+
if not system_prompt:
|
|
71
|
+
console.print("[red]System prompt is required.[/red]")
|
|
72
|
+
print_agents_usage()
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
location_arg = tokens[2] if len(tokens) > 2 else ""
|
|
76
|
+
model_arg = tokens[3] if len(tokens) > 3 else ""
|
|
77
|
+
if location_arg and location_arg.lower() not in ("user", "project"):
|
|
78
|
+
model_arg, location_arg = location_arg, ""
|
|
79
|
+
|
|
80
|
+
location_raw = (
|
|
81
|
+
location_arg or console.input("Location [user/project, default user]: ").strip()
|
|
82
|
+
).lower()
|
|
83
|
+
location = AgentLocation.PROJECT if location_raw == "project" else AgentLocation.USER
|
|
84
|
+
|
|
85
|
+
config = get_global_config()
|
|
86
|
+
pointer_map = config.model_pointers.model_dump()
|
|
87
|
+
default_model_value = model_arg or pointer_map.get("task", "task")
|
|
88
|
+
model_input = (
|
|
89
|
+
console.input(f"Model profile or pointer [{default_model_value}]: ").strip()
|
|
90
|
+
or default_model_value
|
|
91
|
+
)
|
|
92
|
+
if (
|
|
93
|
+
model_input
|
|
94
|
+
and model_input not in config.model_profiles
|
|
95
|
+
and model_input not in pointer_map
|
|
96
|
+
):
|
|
97
|
+
console.print(
|
|
98
|
+
"[yellow]Model not found in profiles or pointers; "
|
|
99
|
+
"will fall back to main if unavailable.[/yellow]"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
path = save_agent_definition(
|
|
104
|
+
agent_type=agent_name,
|
|
105
|
+
description=description,
|
|
106
|
+
tools=tools,
|
|
107
|
+
system_prompt=system_prompt,
|
|
108
|
+
location=location,
|
|
109
|
+
model=model_input,
|
|
110
|
+
)
|
|
111
|
+
console.print(
|
|
112
|
+
f"[green]✓ Agent '{escape(agent_name)}' created at {escape(str(path))}[/green]"
|
|
113
|
+
)
|
|
114
|
+
except (OSError, IOError, PermissionError, ValueError) as exc:
|
|
115
|
+
console.print(f"[red]Failed to create agent: {escape(str(exc))}[/red]")
|
|
116
|
+
print_agents_usage()
|
|
117
|
+
logger.warning(
|
|
118
|
+
"[agents_cmd] Failed to create agent: %s: %s",
|
|
119
|
+
type(exc).__name__, exc,
|
|
120
|
+
extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
|
|
121
|
+
)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
if subcmd in ("delete", "del", "remove"):
|
|
125
|
+
agent_name = (
|
|
126
|
+
tokens[1] if len(tokens) > 1 else console.input("Agent name to delete: ").strip()
|
|
127
|
+
)
|
|
128
|
+
if not agent_name:
|
|
129
|
+
console.print("[red]Agent name is required.[/red]")
|
|
130
|
+
print_agents_usage()
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
location_raw = (
|
|
134
|
+
tokens[2]
|
|
135
|
+
if len(tokens) > 2
|
|
136
|
+
else console.input("Location to delete from [user/project, default user]: ").strip()
|
|
137
|
+
).lower()
|
|
138
|
+
location = AgentLocation.PROJECT if location_raw == "project" else AgentLocation.USER
|
|
139
|
+
try:
|
|
140
|
+
path = delete_agent_definition(agent_name, location)
|
|
141
|
+
console.print(
|
|
142
|
+
f"[green]✓ Deleted agent '{escape(agent_name)}' at {escape(str(path))}[/green]"
|
|
143
|
+
)
|
|
144
|
+
except FileNotFoundError as exc:
|
|
145
|
+
console.print(f"[yellow]{escape(str(exc))}[/yellow]")
|
|
146
|
+
except (OSError, IOError, PermissionError, ValueError) as exc:
|
|
147
|
+
console.print(f"[red]Failed to delete agent: {escape(str(exc))}[/red]")
|
|
148
|
+
print_agents_usage()
|
|
149
|
+
logger.warning(
|
|
150
|
+
"[agents_cmd] Failed to delete agent: %s: %s",
|
|
151
|
+
type(exc).__name__, exc,
|
|
152
|
+
extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
|
|
153
|
+
)
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
if subcmd in ("edit", "update"):
|
|
157
|
+
agent_name = tokens[1] if len(tokens) > 1 else console.input("Agent to edit: ").strip()
|
|
158
|
+
if not agent_name:
|
|
159
|
+
console.print("[red]Agent name is required.[/red]")
|
|
160
|
+
print_agents_usage()
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
agents = load_agent_definitions()
|
|
164
|
+
target_agent = next((a for a in agents.active_agents if a.agent_type == agent_name), None)
|
|
165
|
+
if not target_agent:
|
|
166
|
+
console.print(f"[red]Agent '{escape(agent_name)}' not found.[/red]")
|
|
167
|
+
print_agents_usage()
|
|
168
|
+
return True
|
|
169
|
+
if target_agent.location == AgentLocation.BUILT_IN:
|
|
170
|
+
console.print("[yellow]Built-in agents cannot be edited.[/yellow]")
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
default_location = target_agent.location
|
|
174
|
+
location_raw = (
|
|
175
|
+
tokens[2]
|
|
176
|
+
if len(tokens) > 2
|
|
177
|
+
else console.input(
|
|
178
|
+
f"Location to save [user/project, default {default_location.value}]: "
|
|
179
|
+
).strip()
|
|
180
|
+
).lower()
|
|
181
|
+
location = AgentLocation.PROJECT if location_raw == "project" else AgentLocation.USER
|
|
182
|
+
|
|
183
|
+
description = (
|
|
184
|
+
console.input(f"Description (when to use) [{target_agent.when_to_use}]: ").strip()
|
|
185
|
+
or target_agent.when_to_use
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
tools_default = "*" if "*" in target_agent.tools else ", ".join(target_agent.tools)
|
|
189
|
+
tools_input = (
|
|
190
|
+
console.input(f"Tools (comma-separated, * for all) [{tools_default}]: ").strip()
|
|
191
|
+
or tools_default
|
|
192
|
+
)
|
|
193
|
+
tools = [t.strip() for t in tools_input.split(",") if t.strip()] or ["*"]
|
|
194
|
+
|
|
195
|
+
console.print("[dim]Current system prompt:[/dim]")
|
|
196
|
+
console.print(escape(target_agent.system_prompt or "(empty)"), markup=False)
|
|
197
|
+
system_prompt = (
|
|
198
|
+
console.input(
|
|
199
|
+
"System prompt (single line, use \\n for newlines) [Enter to keep current]: "
|
|
200
|
+
).strip()
|
|
201
|
+
or target_agent.system_prompt
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
config = get_global_config()
|
|
205
|
+
pointer_map = config.model_pointers.model_dump()
|
|
206
|
+
model_default = target_agent.model or pointer_map.get("task", "task")
|
|
207
|
+
model_input = (
|
|
208
|
+
console.input(f"Model profile or pointer [{model_default}]: ").strip() or model_default
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
path = save_agent_definition(
|
|
213
|
+
agent_type=agent_name,
|
|
214
|
+
description=description,
|
|
215
|
+
tools=tools,
|
|
216
|
+
system_prompt=system_prompt,
|
|
217
|
+
location=location,
|
|
218
|
+
model=model_input,
|
|
219
|
+
overwrite=True,
|
|
220
|
+
)
|
|
221
|
+
console.print(
|
|
222
|
+
f"[green]✓ Agent '{escape(agent_name)}' updated at {escape(str(path))}[/green]"
|
|
223
|
+
)
|
|
224
|
+
except (OSError, IOError, PermissionError, ValueError) as exc:
|
|
225
|
+
console.print(f"[red]Failed to update agent: {escape(str(exc))}[/red]")
|
|
226
|
+
print_agents_usage()
|
|
227
|
+
logger.warning(
|
|
228
|
+
"[agents_cmd] Failed to update agent: %s: %s",
|
|
229
|
+
type(exc).__name__, exc,
|
|
230
|
+
extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
|
|
231
|
+
)
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
agents = load_agent_definitions()
|
|
235
|
+
console.print("\n[bold]Agents:[/bold]")
|
|
236
|
+
print_agents_usage()
|
|
237
|
+
if not agents.active_agents:
|
|
238
|
+
console.print(" • None configured")
|
|
239
|
+
for agent in agents.active_agents:
|
|
240
|
+
location = getattr(agent.location, "value", agent.location)
|
|
241
|
+
tools_str = "all tools" if "*" in agent.tools else ", ".join(agent.tools)
|
|
242
|
+
console.print(f" • {escape(agent.agent_type)} ({escape(str(location))})", markup=False)
|
|
243
|
+
console.print(f" {escape(agent.when_to_use)}", markup=False)
|
|
244
|
+
console.print(f" tools: {escape(tools_str)}", markup=False)
|
|
245
|
+
console.print(f" model: {escape(agent.model or 'task (default)')}", markup=False)
|
|
246
|
+
if agents.failed_files:
|
|
247
|
+
console.print("[yellow]Some agent files could not be loaded:[/yellow]")
|
|
248
|
+
for path, error in agents.failed_files:
|
|
249
|
+
console.print(f" - {escape(str(path))}: {escape(str(error))}", markup=False)
|
|
250
|
+
console.print(
|
|
251
|
+
f"[dim]Add agents in ~/.ripperdoc/{AGENT_DIR_NAME} or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
|
|
252
|
+
)
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
command = SlashCommand(
|
|
257
|
+
name="agents",
|
|
258
|
+
description="Manage subagents: list/create/delete",
|
|
259
|
+
handler=_handle,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
__all__ = ["command"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Shared types for slash command handlers."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Callable, Tuple
|
|
5
|
+
|
|
6
|
+
Handler = Callable[[Any, str], bool]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class SlashCommand:
|
|
11
|
+
"""A single slash command implementation."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
description: str
|
|
15
|
+
handler: Handler
|
|
16
|
+
aliases: Tuple[str, ...] = ()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["SlashCommand", "Handler"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from .base import SlashCommand
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _handle(ui: Any, _: str) -> bool:
|
|
6
|
+
ui.conversation_messages = []
|
|
7
|
+
ui.console.print("[green]✓ Conversation cleared[/green]")
|
|
8
|
+
return True
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
command = SlashCommand(
|
|
12
|
+
name="clear",
|
|
13
|
+
description="Clear conversation history",
|
|
14
|
+
handler=_handle,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["command"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from .base import SlashCommand
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
6
|
+
runner = getattr(ui, "run_async", None)
|
|
7
|
+
if callable(runner):
|
|
8
|
+
runner(ui._run_manual_compact(trimmed_arg))
|
|
9
|
+
else:
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
asyncio.run(ui._run_manual_compact(trimmed_arg))
|
|
13
|
+
return True
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
command = SlashCommand(
|
|
17
|
+
name="compact",
|
|
18
|
+
description="Compact conversation history",
|
|
19
|
+
handler=_handle,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ["command"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from rich.markup import escape
|
|
2
|
+
|
|
3
|
+
from ripperdoc.core.config import get_global_config
|
|
4
|
+
from ripperdoc.cli.ui.helpers import get_profile_for_pointer
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
from .base import SlashCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _handle(ui: Any, _: str) -> bool:
|
|
11
|
+
config = get_global_config()
|
|
12
|
+
profile = get_profile_for_pointer("main")
|
|
13
|
+
main_pointer = getattr(config.model_pointers, "main", "default")
|
|
14
|
+
model_label = profile.model if profile else "Not configured"
|
|
15
|
+
|
|
16
|
+
ui.console.print(
|
|
17
|
+
f"\n[bold]Model (main -> {escape(str(main_pointer))}):[/bold] {escape(str(model_label))}"
|
|
18
|
+
)
|
|
19
|
+
ui.console.print(f"[bold]Safe Mode:[/bold] {escape(str(ui.safe_mode))}")
|
|
20
|
+
ui.console.print(f"[bold]Verbose:[/bold] {escape(str(ui.verbose))}")
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
command = SlashCommand(
|
|
25
|
+
name="config",
|
|
26
|
+
description="Show current configuration",
|
|
27
|
+
handler=_handle,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ["command"]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import List, Any
|
|
3
|
+
|
|
4
|
+
from ripperdoc.cli.ui.helpers import get_profile_for_pointer
|
|
5
|
+
from ripperdoc.cli.ui.context_display import format_tokens
|
|
6
|
+
from ripperdoc.core.config import get_global_config, provider_protocol
|
|
7
|
+
from ripperdoc.core.query import QueryContext
|
|
8
|
+
from ripperdoc.core.system_prompt import build_system_prompt
|
|
9
|
+
from ripperdoc.core.skills import build_skill_summary, load_all_skills
|
|
10
|
+
from ripperdoc.utils.memory import build_memory_instructions
|
|
11
|
+
from ripperdoc.utils.message_compaction import (
|
|
12
|
+
get_remaining_context_tokens,
|
|
13
|
+
resolve_auto_compact_enabled,
|
|
14
|
+
summarize_context_usage,
|
|
15
|
+
)
|
|
16
|
+
from ripperdoc.utils.token_estimation import estimate_tokens
|
|
17
|
+
from ripperdoc.utils.mcp import (
|
|
18
|
+
estimate_mcp_tokens,
|
|
19
|
+
format_mcp_instructions,
|
|
20
|
+
load_mcp_servers_async,
|
|
21
|
+
)
|
|
22
|
+
from ripperdoc.utils.log import get_logger
|
|
23
|
+
|
|
24
|
+
from .base import SlashCommand
|
|
25
|
+
|
|
26
|
+
logger = get_logger()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _handle(ui: Any, _: str) -> bool:
|
|
30
|
+
logger.info(
|
|
31
|
+
"[context_cmd] Rendering context summary",
|
|
32
|
+
extra={"session_id": getattr(ui, "session_id", None)},
|
|
33
|
+
)
|
|
34
|
+
config = get_global_config()
|
|
35
|
+
model_profile = get_profile_for_pointer("main")
|
|
36
|
+
max_context_tokens = get_remaining_context_tokens(model_profile, config.context_token_limit)
|
|
37
|
+
auto_compact_enabled = resolve_auto_compact_enabled(config)
|
|
38
|
+
protocol = provider_protocol(model_profile.provider) if model_profile else "openai"
|
|
39
|
+
|
|
40
|
+
if not ui.query_context:
|
|
41
|
+
ui.query_context = QueryContext(
|
|
42
|
+
tools=ui.get_default_tools(),
|
|
43
|
+
safe_mode=ui.safe_mode,
|
|
44
|
+
verbose=ui.verbose,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def _load_servers() -> List[Any]:
|
|
48
|
+
return await load_mcp_servers_async(ui.project_path)
|
|
49
|
+
|
|
50
|
+
runner = getattr(ui, "run_async", None)
|
|
51
|
+
if callable(runner):
|
|
52
|
+
servers = runner(_load_servers())
|
|
53
|
+
else:
|
|
54
|
+
import asyncio
|
|
55
|
+
|
|
56
|
+
servers = asyncio.run(_load_servers())
|
|
57
|
+
mcp_instructions = format_mcp_instructions(servers)
|
|
58
|
+
skill_result = load_all_skills(ui.project_path)
|
|
59
|
+
for err in skill_result.errors:
|
|
60
|
+
logger.warning(
|
|
61
|
+
"[skills] Failed to load skill",
|
|
62
|
+
extra={
|
|
63
|
+
"path": str(err.path),
|
|
64
|
+
"reason": err.reason,
|
|
65
|
+
"session_id": getattr(ui, "session_id", None),
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
skill_instructions = build_skill_summary(skill_result.skills)
|
|
69
|
+
additional_instructions: List[str] = []
|
|
70
|
+
if skill_instructions:
|
|
71
|
+
additional_instructions.append(skill_instructions)
|
|
72
|
+
memory_instructions = build_memory_instructions()
|
|
73
|
+
if memory_instructions:
|
|
74
|
+
additional_instructions.append(memory_instructions)
|
|
75
|
+
base_system_prompt = build_system_prompt(
|
|
76
|
+
ui.query_context.tools,
|
|
77
|
+
"",
|
|
78
|
+
{},
|
|
79
|
+
additional_instructions=additional_instructions or None,
|
|
80
|
+
mcp_instructions=mcp_instructions,
|
|
81
|
+
)
|
|
82
|
+
memory_tokens = 0
|
|
83
|
+
mcp_tokens = estimate_mcp_tokens(servers) if mcp_instructions else 0
|
|
84
|
+
|
|
85
|
+
breakdown = summarize_context_usage(
|
|
86
|
+
ui.conversation_messages,
|
|
87
|
+
ui.query_context.tools,
|
|
88
|
+
base_system_prompt,
|
|
89
|
+
max_context_tokens,
|
|
90
|
+
auto_compact_enabled,
|
|
91
|
+
memory_tokens=memory_tokens,
|
|
92
|
+
mcp_tokens=mcp_tokens,
|
|
93
|
+
protocol=protocol,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
model_label = model_profile.model if model_profile else "Unknown model"
|
|
97
|
+
lines = ui._context_usage_lines(breakdown, model_label, auto_compact_enabled)
|
|
98
|
+
|
|
99
|
+
lines.append("")
|
|
100
|
+
# Append a brief tool listing so users can see which tools are currently loaded.
|
|
101
|
+
try:
|
|
102
|
+
# Detailed MCP tool listing with token estimates.
|
|
103
|
+
mcp_tools = [
|
|
104
|
+
tool
|
|
105
|
+
for tool in getattr(ui.query_context, "tool_registry", ui.query_context).all_tools
|
|
106
|
+
if getattr(tool, "is_mcp", False) or getattr(tool, "name", "").startswith("mcp__")
|
|
107
|
+
]
|
|
108
|
+
if mcp_tools:
|
|
109
|
+
lines.append(" MCP tools · /mcp")
|
|
110
|
+
for tool in mcp_tools[:20]:
|
|
111
|
+
name = getattr(tool, "name", "unknown")
|
|
112
|
+
display = name
|
|
113
|
+
parts = name.split("__")
|
|
114
|
+
if len(parts) >= 3 and parts[0] == "mcp":
|
|
115
|
+
server = parts[1]
|
|
116
|
+
display = "__".join(parts[2:])
|
|
117
|
+
display = f"{display} ({server})"
|
|
118
|
+
try:
|
|
119
|
+
schema = tool.input_schema.model_json_schema()
|
|
120
|
+
token_est = estimate_tokens(json.dumps(schema, sort_keys=True))
|
|
121
|
+
except (AttributeError, TypeError, ValueError):
|
|
122
|
+
token_est = 0
|
|
123
|
+
lines.append(f" └ {display}: {format_tokens(token_est)} tokens")
|
|
124
|
+
if len(mcp_tools) > 20:
|
|
125
|
+
lines.append(f" └ ... (+{len(mcp_tools) - 20} more)")
|
|
126
|
+
except (OSError, RuntimeError, AttributeError, TypeError) as exc:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"[context_cmd] Failed to summarize MCP tools: %s: %s",
|
|
129
|
+
type(exc).__name__, exc,
|
|
130
|
+
extra={"session_id": getattr(ui, "session_id", None)},
|
|
131
|
+
)
|
|
132
|
+
for line in lines:
|
|
133
|
+
ui.console.print(line)
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
command = SlashCommand(
|
|
138
|
+
name="context",
|
|
139
|
+
description="Show current conversation context summary",
|
|
140
|
+
handler=_handle,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
__all__ = ["command"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from ripperdoc.utils.session_usage import get_session_usage
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from .base import SlashCommand
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _fmt_tokens(value: int) -> str:
|
|
8
|
+
"""Format integers with thousand separators."""
|
|
9
|
+
return f"{int(value):,}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _format_duration(duration_ms: float) -> str:
|
|
13
|
+
"""Render milliseconds into a compact human-readable duration."""
|
|
14
|
+
seconds = int(duration_ms // 1000)
|
|
15
|
+
if seconds < 60:
|
|
16
|
+
return f"{duration_ms / 1000:.2f}s"
|
|
17
|
+
minutes, secs = divmod(seconds, 60)
|
|
18
|
+
if minutes < 60:
|
|
19
|
+
return f"{minutes}m {secs}s"
|
|
20
|
+
hours, mins = divmod(minutes, 60)
|
|
21
|
+
return f"{hours}h {mins}m {secs}s"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _handle(ui: Any, _: str) -> bool:
|
|
25
|
+
usage = get_session_usage()
|
|
26
|
+
if not usage.models:
|
|
27
|
+
ui.console.print("[yellow]No model usage recorded yet.[/yellow]")
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
total_input = usage.total_input_tokens
|
|
31
|
+
total_output = usage.total_output_tokens
|
|
32
|
+
total_cache_read = usage.total_cache_read_tokens
|
|
33
|
+
total_cache_creation = usage.total_cache_creation_tokens
|
|
34
|
+
total_tokens = total_input + total_output + total_cache_read + total_cache_creation
|
|
35
|
+
total_cost = usage.total_cost_usd
|
|
36
|
+
|
|
37
|
+
ui.console.print("\n[bold]Session token usage[/bold]")
|
|
38
|
+
ui.console.print(
|
|
39
|
+
f" Total: {_fmt_tokens(total_tokens)} tokens "
|
|
40
|
+
f"(input {_fmt_tokens(total_input)}, output {_fmt_tokens(total_output)})"
|
|
41
|
+
)
|
|
42
|
+
if total_cache_read or total_cache_creation:
|
|
43
|
+
ui.console.print(
|
|
44
|
+
f" Cache: {_fmt_tokens(total_cache_read)} read, "
|
|
45
|
+
f"{_fmt_tokens(total_cache_creation)} write"
|
|
46
|
+
)
|
|
47
|
+
ui.console.print(f" Requests: {usage.total_requests}")
|
|
48
|
+
if total_cost:
|
|
49
|
+
ui.console.print(f" Cost: ${total_cost:.4f}")
|
|
50
|
+
if usage.total_duration_ms:
|
|
51
|
+
ui.console.print(f" API time: {_format_duration(usage.total_duration_ms)}")
|
|
52
|
+
|
|
53
|
+
ui.console.print("\n[bold]By model:[/bold]")
|
|
54
|
+
for model_name, stats in usage.models.items():
|
|
55
|
+
line = (
|
|
56
|
+
f" {model_name}: "
|
|
57
|
+
f"{_fmt_tokens(stats.input_tokens)} in, "
|
|
58
|
+
f"{_fmt_tokens(stats.output_tokens)} out"
|
|
59
|
+
)
|
|
60
|
+
if stats.cache_read_input_tokens:
|
|
61
|
+
line += f", {_fmt_tokens(stats.cache_read_input_tokens)} cache read"
|
|
62
|
+
if stats.cache_creation_input_tokens:
|
|
63
|
+
line += f", {_fmt_tokens(stats.cache_creation_input_tokens)} cache write"
|
|
64
|
+
line += f" ({stats.requests} call{'' if stats.requests == 1 else 's'}"
|
|
65
|
+
if stats.duration_ms:
|
|
66
|
+
line += f", {_format_duration(stats.duration_ms)} total"
|
|
67
|
+
line += ")"
|
|
68
|
+
if stats.cost_usd:
|
|
69
|
+
line += f", ${stats.cost_usd:.4f}"
|
|
70
|
+
ui.console.print(line)
|
|
71
|
+
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
command = SlashCommand(
|
|
76
|
+
name="cost",
|
|
77
|
+
description="Show total tokens used in this session",
|
|
78
|
+
handler=_handle,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = ["command"]
|