ripperdoc 0.1.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +75 -15
- ripperdoc/cli/commands/__init__.py +4 -0
- ripperdoc/cli/commands/agents_cmd.py +23 -1
- ripperdoc/cli/commands/context_cmd.py +13 -3
- ripperdoc/cli/commands/cost_cmd.py +1 -1
- ripperdoc/cli/commands/doctor_cmd.py +200 -0
- ripperdoc/cli/commands/memory_cmd.py +209 -0
- ripperdoc/cli/commands/models_cmd.py +25 -0
- ripperdoc/cli/commands/resume_cmd.py +3 -3
- ripperdoc/cli/commands/status_cmd.py +5 -5
- ripperdoc/cli/commands/tasks_cmd.py +32 -5
- ripperdoc/cli/ui/context_display.py +4 -3
- ripperdoc/cli/ui/rich_ui.py +205 -43
- ripperdoc/cli/ui/spinner.py +3 -4
- ripperdoc/core/agents.py +10 -6
- ripperdoc/core/config.py +48 -3
- ripperdoc/core/default_tools.py +26 -6
- ripperdoc/core/permissions.py +19 -0
- ripperdoc/core/query.py +238 -302
- ripperdoc/core/query_utils.py +537 -0
- ripperdoc/core/system_prompt.py +2 -1
- ripperdoc/core/tool.py +14 -1
- ripperdoc/sdk/client.py +1 -1
- ripperdoc/tools/background_shell.py +9 -3
- ripperdoc/tools/bash_tool.py +19 -4
- ripperdoc/tools/file_edit_tool.py +9 -2
- ripperdoc/tools/file_read_tool.py +9 -2
- ripperdoc/tools/file_write_tool.py +15 -2
- ripperdoc/tools/glob_tool.py +57 -17
- ripperdoc/tools/grep_tool.py +9 -2
- ripperdoc/tools/ls_tool.py +244 -75
- ripperdoc/tools/mcp_tools.py +47 -19
- ripperdoc/tools/multi_edit_tool.py +13 -2
- ripperdoc/tools/notebook_edit_tool.py +9 -6
- ripperdoc/tools/task_tool.py +20 -5
- ripperdoc/tools/todo_tool.py +163 -29
- ripperdoc/tools/tool_search_tool.py +15 -4
- ripperdoc/utils/git_utils.py +276 -0
- ripperdoc/utils/json_utils.py +28 -0
- ripperdoc/utils/log.py +130 -29
- ripperdoc/utils/mcp.py +83 -10
- ripperdoc/utils/memory.py +14 -1
- ripperdoc/utils/message_compaction.py +51 -14
- ripperdoc/utils/messages.py +63 -4
- ripperdoc/utils/output_utils.py +36 -9
- ripperdoc/utils/permissions/path_validation_utils.py +6 -0
- ripperdoc/utils/safe_get_cwd.py +4 -0
- ripperdoc/utils/session_history.py +27 -9
- ripperdoc/utils/todo.py +2 -2
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/METADATA +4 -2
- ripperdoc-0.2.2.dist-info/RECORD +86 -0
- ripperdoc-0.1.0.dist-info/RECORD +0 -81
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/WHEEL +0 -0
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/top_level.txt +0 -0
ripperdoc/core/config.py
CHANGED
|
@@ -7,7 +7,7 @@ including API keys, model settings, and user preferences.
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Dict, Optional
|
|
10
|
+
from typing import Dict, Optional, Literal
|
|
11
11
|
from pydantic import BaseModel, Field
|
|
12
12
|
from enum import Enum
|
|
13
13
|
|
|
@@ -105,6 +105,9 @@ class ModelProfile(BaseModel):
|
|
|
105
105
|
temperature: float = 0.7
|
|
106
106
|
# Total context window in tokens (if known). Falls back to heuristics when unset.
|
|
107
107
|
context_window: Optional[int] = None
|
|
108
|
+
# Tool handling for OpenAI-compatible providers. "native" uses tool_calls, "text" flattens tool
|
|
109
|
+
# interactions into plain text to support providers that reject tool roles.
|
|
110
|
+
openai_tool_mode: Literal["native", "text"] = "native"
|
|
108
111
|
|
|
109
112
|
|
|
110
113
|
class ModelPointers(BaseModel):
|
|
@@ -185,17 +188,36 @@ class ConfigManager:
|
|
|
185
188
|
try:
|
|
186
189
|
data = json.loads(self.global_config_path.read_text())
|
|
187
190
|
self._global_config = GlobalConfig(**data)
|
|
191
|
+
logger.debug(
|
|
192
|
+
"[config] Loaded global configuration",
|
|
193
|
+
extra={
|
|
194
|
+
"path": str(self.global_config_path),
|
|
195
|
+
"profile_count": len(self._global_config.model_profiles),
|
|
196
|
+
},
|
|
197
|
+
)
|
|
188
198
|
except Exception as e:
|
|
189
|
-
logger.
|
|
199
|
+
logger.exception("Error loading global config", extra={"error": str(e)})
|
|
190
200
|
self._global_config = GlobalConfig()
|
|
191
201
|
else:
|
|
192
202
|
self._global_config = GlobalConfig()
|
|
203
|
+
logger.debug(
|
|
204
|
+
"[config] Global config not found; using defaults",
|
|
205
|
+
extra={"path": str(self.global_config_path)},
|
|
206
|
+
)
|
|
193
207
|
return self._global_config
|
|
194
208
|
|
|
195
209
|
def save_global_config(self, config: GlobalConfig) -> None:
|
|
196
210
|
"""Save global configuration."""
|
|
197
211
|
self._global_config = config
|
|
198
212
|
self.global_config_path.write_text(config.model_dump_json(indent=2))
|
|
213
|
+
logger.debug(
|
|
214
|
+
"[config] Saved global configuration",
|
|
215
|
+
extra={
|
|
216
|
+
"path": str(self.global_config_path),
|
|
217
|
+
"profile_count": len(config.model_profiles),
|
|
218
|
+
"pointers": config.model_pointers.model_dump(),
|
|
219
|
+
},
|
|
220
|
+
)
|
|
199
221
|
|
|
200
222
|
def get_project_config(self, project_path: Optional[Path] = None) -> ProjectConfig:
|
|
201
223
|
"""Load and return project configuration."""
|
|
@@ -215,11 +237,26 @@ class ConfigManager:
|
|
|
215
237
|
try:
|
|
216
238
|
data = json.loads(config_path.read_text())
|
|
217
239
|
self._project_config = ProjectConfig(**data)
|
|
240
|
+
logger.debug(
|
|
241
|
+
"[config] Loaded project config",
|
|
242
|
+
extra={
|
|
243
|
+
"path": str(config_path),
|
|
244
|
+
"project_path": str(self.current_project_path),
|
|
245
|
+
"allowed_tools": len(self._project_config.allowed_tools),
|
|
246
|
+
},
|
|
247
|
+
)
|
|
218
248
|
except Exception as e:
|
|
219
|
-
logger.
|
|
249
|
+
logger.exception(
|
|
250
|
+
"Error loading project config",
|
|
251
|
+
extra={"error": str(e), "path": str(config_path)},
|
|
252
|
+
)
|
|
220
253
|
self._project_config = ProjectConfig()
|
|
221
254
|
else:
|
|
222
255
|
self._project_config = ProjectConfig()
|
|
256
|
+
logger.debug(
|
|
257
|
+
"[config] Project config not found; using defaults",
|
|
258
|
+
extra={"path": str(config_path), "project_path": str(self.current_project_path)},
|
|
259
|
+
)
|
|
223
260
|
|
|
224
261
|
return self._project_config
|
|
225
262
|
|
|
@@ -239,6 +276,14 @@ class ConfigManager:
|
|
|
239
276
|
config_path = config_dir / "config.json"
|
|
240
277
|
self._project_config = config
|
|
241
278
|
config_path.write_text(config.model_dump_json(indent=2))
|
|
279
|
+
logger.debug(
|
|
280
|
+
"[config] Saved project config",
|
|
281
|
+
extra={
|
|
282
|
+
"path": str(config_path),
|
|
283
|
+
"project_path": str(self.current_project_path),
|
|
284
|
+
"allowed_tools": len(config.allowed_tools),
|
|
285
|
+
},
|
|
286
|
+
)
|
|
242
287
|
|
|
243
288
|
def get_api_key(self, provider: ProviderType) -> Optional[str]:
|
|
244
289
|
"""Get API key for a provider."""
|
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
|
|
@@ -24,11 +26,14 @@ from ripperdoc.tools.mcp_tools import (
|
|
|
24
26
|
ReadMcpResourceTool,
|
|
25
27
|
load_dynamic_mcp_tools_sync,
|
|
26
28
|
)
|
|
29
|
+
from ripperdoc.utils.log import get_logger
|
|
30
|
+
|
|
31
|
+
logger = get_logger()
|
|
27
32
|
|
|
28
33
|
|
|
29
|
-
def get_default_tools() -> List:
|
|
34
|
+
def get_default_tools() -> List[Tool[Any, Any]]:
|
|
30
35
|
"""Construct the default tool set (base tools + Task subagent launcher)."""
|
|
31
|
-
base_tools = [
|
|
36
|
+
base_tools: List[Tool[Any, Any]] = [
|
|
32
37
|
BashTool(),
|
|
33
38
|
BashOutputTool(),
|
|
34
39
|
KillBashTool(),
|
|
@@ -47,11 +52,26 @@ def get_default_tools() -> List:
|
|
|
47
52
|
ListMcpResourcesTool(),
|
|
48
53
|
ReadMcpResourceTool(),
|
|
49
54
|
]
|
|
55
|
+
dynamic_tools: List[Tool[Any, Any]] = []
|
|
50
56
|
try:
|
|
51
|
-
|
|
57
|
+
mcp_tools = load_dynamic_mcp_tools_sync()
|
|
58
|
+
# Filter to ensure only Tool instances are added
|
|
59
|
+
for tool in mcp_tools:
|
|
60
|
+
if isinstance(tool, Tool):
|
|
61
|
+
base_tools.append(tool)
|
|
62
|
+
dynamic_tools.append(tool)
|
|
52
63
|
except Exception:
|
|
53
64
|
# If MCP runtime is not available, continue with base tools only.
|
|
54
|
-
|
|
65
|
+
logger.exception("[default_tools] Failed to load dynamic MCP tools")
|
|
55
66
|
|
|
56
67
|
task_tool = TaskTool(lambda: base_tools)
|
|
57
|
-
|
|
68
|
+
all_tools = base_tools + [task_tool]
|
|
69
|
+
logger.debug(
|
|
70
|
+
"[default_tools] Built tool inventory",
|
|
71
|
+
extra={
|
|
72
|
+
"base_tools": len(base_tools),
|
|
73
|
+
"dynamic_mcp_tools": len(dynamic_tools),
|
|
74
|
+
"total_tools": len(all_tools),
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
return all_tools
|
ripperdoc/core/permissions.py
CHANGED
|
@@ -11,6 +11,9 @@ from typing import Any, Awaitable, Callable, Optional, Set
|
|
|
11
11
|
from ripperdoc.core.config import config_manager
|
|
12
12
|
from ripperdoc.core.tool import Tool
|
|
13
13
|
from ripperdoc.utils.permissions import PermissionDecision, ToolRule
|
|
14
|
+
from ripperdoc.utils.log import get_logger
|
|
15
|
+
|
|
16
|
+
logger = get_logger()
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
@dataclass
|
|
@@ -46,11 +49,19 @@ def permission_key(tool: Tool[Any, Any], parsed_input: Any) -> str:
|
|
|
46
49
|
try:
|
|
47
50
|
return f"{tool.name}::path::{Path(getattr(parsed_input, 'file_path')).resolve()}"
|
|
48
51
|
except Exception:
|
|
52
|
+
logger.exception(
|
|
53
|
+
"[permissions] Failed to resolve file_path for permission key",
|
|
54
|
+
extra={"tool": getattr(tool, "name", None)},
|
|
55
|
+
)
|
|
49
56
|
return f"{tool.name}::path::{getattr(parsed_input, 'file_path')}"
|
|
50
57
|
if hasattr(parsed_input, "path"):
|
|
51
58
|
try:
|
|
52
59
|
return f"{tool.name}::path::{Path(getattr(parsed_input, 'path')).resolve()}"
|
|
53
60
|
except Exception:
|
|
61
|
+
logger.exception(
|
|
62
|
+
"[permissions] Failed to resolve path for permission key",
|
|
63
|
+
extra={"tool": getattr(tool, "name", None)},
|
|
64
|
+
)
|
|
54
65
|
return f"{tool.name}::path::{getattr(parsed_input, 'path')}"
|
|
55
66
|
return tool.name
|
|
56
67
|
|
|
@@ -116,6 +127,10 @@ def make_permission_checker(
|
|
|
116
127
|
if hasattr(tool, "needs_permissions") and not tool.needs_permissions(parsed_input):
|
|
117
128
|
return PermissionResult(result=True)
|
|
118
129
|
except Exception:
|
|
130
|
+
logger.exception(
|
|
131
|
+
"[permissions] Tool needs_permissions check failed",
|
|
132
|
+
extra={"tool": getattr(tool, "name", None)},
|
|
133
|
+
)
|
|
119
134
|
return PermissionResult(
|
|
120
135
|
result=False,
|
|
121
136
|
message="Permission check failed for this tool invocation.",
|
|
@@ -153,6 +168,10 @@ def make_permission_checker(
|
|
|
153
168
|
if isinstance(decision, dict) and "behavior" in decision:
|
|
154
169
|
decision = PermissionDecision(**decision)
|
|
155
170
|
except Exception:
|
|
171
|
+
logger.exception(
|
|
172
|
+
"[permissions] Tool check_permissions failed",
|
|
173
|
+
extra={"tool": getattr(tool, "name", None)},
|
|
174
|
+
)
|
|
156
175
|
decision = PermissionDecision(
|
|
157
176
|
behavior="ask",
|
|
158
177
|
message="Error checking permissions for this tool.",
|