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.
Files changed (57) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +75 -15
  3. ripperdoc/cli/commands/__init__.py +4 -0
  4. ripperdoc/cli/commands/agents_cmd.py +23 -1
  5. ripperdoc/cli/commands/context_cmd.py +13 -3
  6. ripperdoc/cli/commands/cost_cmd.py +1 -1
  7. ripperdoc/cli/commands/doctor_cmd.py +200 -0
  8. ripperdoc/cli/commands/memory_cmd.py +209 -0
  9. ripperdoc/cli/commands/models_cmd.py +25 -0
  10. ripperdoc/cli/commands/resume_cmd.py +3 -3
  11. ripperdoc/cli/commands/status_cmd.py +5 -5
  12. ripperdoc/cli/commands/tasks_cmd.py +32 -5
  13. ripperdoc/cli/ui/context_display.py +4 -3
  14. ripperdoc/cli/ui/rich_ui.py +205 -43
  15. ripperdoc/cli/ui/spinner.py +3 -4
  16. ripperdoc/core/agents.py +10 -6
  17. ripperdoc/core/config.py +48 -3
  18. ripperdoc/core/default_tools.py +26 -6
  19. ripperdoc/core/permissions.py +19 -0
  20. ripperdoc/core/query.py +238 -302
  21. ripperdoc/core/query_utils.py +537 -0
  22. ripperdoc/core/system_prompt.py +2 -1
  23. ripperdoc/core/tool.py +14 -1
  24. ripperdoc/sdk/client.py +1 -1
  25. ripperdoc/tools/background_shell.py +9 -3
  26. ripperdoc/tools/bash_tool.py +19 -4
  27. ripperdoc/tools/file_edit_tool.py +9 -2
  28. ripperdoc/tools/file_read_tool.py +9 -2
  29. ripperdoc/tools/file_write_tool.py +15 -2
  30. ripperdoc/tools/glob_tool.py +57 -17
  31. ripperdoc/tools/grep_tool.py +9 -2
  32. ripperdoc/tools/ls_tool.py +244 -75
  33. ripperdoc/tools/mcp_tools.py +47 -19
  34. ripperdoc/tools/multi_edit_tool.py +13 -2
  35. ripperdoc/tools/notebook_edit_tool.py +9 -6
  36. ripperdoc/tools/task_tool.py +20 -5
  37. ripperdoc/tools/todo_tool.py +163 -29
  38. ripperdoc/tools/tool_search_tool.py +15 -4
  39. ripperdoc/utils/git_utils.py +276 -0
  40. ripperdoc/utils/json_utils.py +28 -0
  41. ripperdoc/utils/log.py +130 -29
  42. ripperdoc/utils/mcp.py +83 -10
  43. ripperdoc/utils/memory.py +14 -1
  44. ripperdoc/utils/message_compaction.py +51 -14
  45. ripperdoc/utils/messages.py +63 -4
  46. ripperdoc/utils/output_utils.py +36 -9
  47. ripperdoc/utils/permissions/path_validation_utils.py +6 -0
  48. ripperdoc/utils/safe_get_cwd.py +4 -0
  49. ripperdoc/utils/session_history.py +27 -9
  50. ripperdoc/utils/todo.py +2 -2
  51. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/METADATA +4 -2
  52. ripperdoc-0.2.2.dist-info/RECORD +86 -0
  53. ripperdoc-0.1.0.dist-info/RECORD +0 -81
  54. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/WHEEL +0 -0
  55. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/entry_points.txt +0 -0
  56. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/licenses/LICENSE +0 -0
  57. {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.error(f"Error loading global config: {e}")
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.error(f"Error loading project config: {e}")
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."""
@@ -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
- base_tools.extend(load_dynamic_mcp_tools_sync())
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
- pass
65
+ logger.exception("[default_tools] Failed to load dynamic MCP tools")
55
66
 
56
67
  task_tool = TaskTool(lambda: base_tools)
57
- return base_tools + [task_tool]
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
@@ -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.",