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.
Files changed (36) hide show
  1. ripperdoc/cli/cli.py +9 -7
  2. ripperdoc/cli/commands/agents_cmd.py +1 -1
  3. ripperdoc/cli/commands/context_cmd.py +2 -2
  4. ripperdoc/cli/commands/cost_cmd.py +1 -1
  5. ripperdoc/cli/commands/resume_cmd.py +3 -3
  6. ripperdoc/cli/commands/status_cmd.py +5 -5
  7. ripperdoc/cli/commands/tasks_cmd.py +5 -5
  8. ripperdoc/cli/ui/context_display.py +4 -3
  9. ripperdoc/cli/ui/rich_ui.py +49 -34
  10. ripperdoc/cli/ui/spinner.py +3 -4
  11. ripperdoc/core/agents.py +6 -4
  12. ripperdoc/core/default_tools.py +10 -4
  13. ripperdoc/core/query.py +9 -7
  14. ripperdoc/core/tool.py +1 -1
  15. ripperdoc/sdk/client.py +1 -1
  16. ripperdoc/tools/bash_tool.py +4 -4
  17. ripperdoc/tools/file_edit_tool.py +2 -2
  18. ripperdoc/tools/file_read_tool.py +2 -2
  19. ripperdoc/tools/file_write_tool.py +8 -2
  20. ripperdoc/tools/glob_tool.py +2 -2
  21. ripperdoc/tools/grep_tool.py +2 -2
  22. ripperdoc/tools/ls_tool.py +3 -3
  23. ripperdoc/tools/mcp_tools.py +15 -9
  24. ripperdoc/tools/multi_edit_tool.py +2 -2
  25. ripperdoc/tools/notebook_edit_tool.py +3 -3
  26. ripperdoc/tools/task_tool.py +13 -5
  27. ripperdoc/tools/todo_tool.py +4 -4
  28. ripperdoc/tools/tool_search_tool.py +6 -4
  29. ripperdoc/utils/mcp.py +12 -4
  30. ripperdoc/utils/message_compaction.py +25 -9
  31. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/METADATA +1 -1
  32. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/RECORD +36 -36
  33. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/WHEEL +0 -0
  34. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/entry_points.txt +0 -0
  35. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/licenses/LICENSE +0 -0
  36. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.0.dist-info}/top_level.txt +0 -0
@@ -134,11 +134,11 @@ build projects, run tests, and interact with the file system."""
134
134
  return [
135
135
  ToolUseExample(
136
136
  description="Run a read-only listing in sandboxed mode",
137
- input={"command": "ls -la", "sandbox": True, "timeout": 10000},
137
+ example={"command": "ls -la", "sandbox": True, "timeout": 10000},
138
138
  ),
139
139
  ToolUseExample(
140
140
  description="Start a long task in the background with a timeout",
141
- input={
141
+ example={
142
142
  "command": "npm test",
143
143
  "run_in_background": True,
144
144
  "timeout": 600000,
@@ -395,10 +395,10 @@ build projects, run tests, and interact with the file system."""
395
395
  if output.duration_ms:
396
396
  timing = f" ({format_duration(output.duration_ms)}"
397
397
  if output.timeout_ms:
398
- timing += f" / timeout {output.timeout_ms/1000:.0f}s"
398
+ timing += f" / timeout {output.timeout_ms / 1000:.0f}s"
399
399
  timing += ")"
400
400
  elif output.timeout_ms:
401
- timing = f" (timeout {output.timeout_ms/1000:.0f}s)"
401
+ timing = f" (timeout {output.timeout_ms / 1000:.0f}s)"
402
402
 
403
403
  result_parts.append(f"{exit_code_text}{timing}")
404
404
 
@@ -60,7 +60,7 @@ match exactly (including whitespace and indentation)."""
60
60
  return [
61
61
  ToolUseExample(
62
62
  description="Rename a function definition once",
63
- input={
63
+ example={
64
64
  "file_path": "/repo/src/app.py",
65
65
  "old_string": "def old_name(",
66
66
  "new_string": "def new_name(",
@@ -69,7 +69,7 @@ match exactly (including whitespace and indentation)."""
69
69
  ),
70
70
  ToolUseExample(
71
71
  description="Replace every occurrence of a constant across a file",
72
- input={
72
+ example={
73
73
  "file_path": "/repo/src/config.ts",
74
74
  "old_string": 'API_BASE = "http://localhost"',
75
75
  "new_string": 'API_BASE = "https://api.example.com"',
@@ -56,11 +56,11 @@ and limit to read only a portion of the file."""
56
56
  return [
57
57
  ToolUseExample(
58
58
  description="Read the top of a file to understand structure",
59
- input={"file_path": "/repo/src/main.py", "limit": 50},
59
+ example={"file_path": "/repo/src/main.py", "limit": 50},
60
60
  ),
61
61
  ToolUseExample(
62
62
  description="Inspect a slice of a large log without loading everything",
63
- input={"file_path": "/repo/logs/server.log", "offset": 200, "limit": 40},
63
+ example={"file_path": "/repo/logs/server.log", "offset": 200, "limit": 40},
64
64
  ),
65
65
  ]
66
66
 
@@ -53,11 +53,17 @@ the file if it already exists."""
53
53
  return [
54
54
  ToolUseExample(
55
55
  description="Create a JSON fixture file",
56
- input={"file_path": "/repo/tests/fixtures/sample.json", "content": '{\n "items": []\n}\n'},
56
+ example={
57
+ "file_path": "/repo/tests/fixtures/sample.json",
58
+ "content": '{\n "items": []\n}\n',
59
+ },
57
60
  ),
58
61
  ToolUseExample(
59
62
  description="Write a short markdown note",
60
- input={"file_path": "/repo/docs/USAGE.md", "content": "# Usage\n\nRun `make test`.\n"},
63
+ example={
64
+ "file_path": "/repo/docs/USAGE.md",
65
+ "content": "# Usage\n\nRun `make test`.\n",
66
+ },
61
67
  ),
62
68
  ]
63
69
 
@@ -62,11 +62,11 @@ class GlobTool(Tool[GlobToolInput, GlobToolOutput]):
62
62
  return [
63
63
  ToolUseExample(
64
64
  description="Find Python sources inside src",
65
- input={"pattern": "src/**/*.py"},
65
+ example={"pattern": "src/**/*.py"},
66
66
  ),
67
67
  ToolUseExample(
68
68
  description="Locate snapshot files within tests",
69
- input={"pattern": "tests/**/__snapshots__/*.snap", "path": "/repo"},
69
+ example={"pattern": "tests/**/__snapshots__/*.snap", "path": "/repo"},
70
70
  ),
71
71
  ]
72
72
 
@@ -80,11 +80,11 @@ class GrepTool(Tool[GrepToolInput, GrepToolOutput]):
80
80
  return [
81
81
  ToolUseExample(
82
82
  description="Find TODO comments in TypeScript files",
83
- input={"pattern": "TODO", "glob": "**/*.ts", "output_mode": "content"},
83
+ example={"pattern": "TODO", "glob": "**/*.ts", "output_mode": "content"},
84
84
  ),
85
85
  ToolUseExample(
86
86
  description="List files referencing a function name",
87
- input={
87
+ example={
88
88
  "pattern": "fetchUserData",
89
89
  "output_mode": "files_with_matches",
90
90
  "path": "/repo/src",
@@ -224,11 +224,11 @@ class LSTool(Tool[LSToolInput, LSToolOutput]):
224
224
  return [
225
225
  ToolUseExample(
226
226
  description="List the repository root with defaults",
227
- input={"path": "/repo"},
227
+ example={"path": "/repo"},
228
228
  ),
229
229
  ToolUseExample(
230
230
  description="Inspect a package while skipping build outputs",
231
- input={"path": "/repo/packages/api", "ignore": ["dist/**", "node_modules/**"]},
231
+ example={"path": "/repo/packages/api", "ignore": ["dist/**", "node_modules/**"]},
232
232
  ),
233
233
  ]
234
234
 
@@ -270,7 +270,7 @@ class LSTool(Tool[LSToolInput, LSToolOutput]):
270
270
  def render_tool_use_message(self, input_data: LSToolInput, verbose: bool = False) -> str:
271
271
  ignore_display = ""
272
272
  if input_data.ignore:
273
- ignore_display = f", ignore: \"{', '.join(input_data.ignore)}\""
273
+ ignore_display = f', ignore: "{", ".join(input_data.ignore)}"'
274
274
  return f'path: "{input_data.path}"{ignore_display}'
275
275
 
276
276
  async def call(
@@ -34,7 +34,7 @@ from ripperdoc.utils.mcp import (
34
34
  try:
35
35
  import mcp.types as mcp_types # type: ignore
36
36
  except Exception: # pragma: no cover - SDK may be missing at runtime
37
- mcp_types = None
37
+ mcp_types = None # type: ignore[assignment]
38
38
 
39
39
 
40
40
  logger = get_logger()
@@ -467,7 +467,11 @@ class ReadMcpResourceTool(Tool[ReadMcpResourceInput, ReadMcpResourceOutput]):
467
467
 
468
468
  if session and mcp_types:
469
469
  try:
470
- result = await session.read_resource(input_data.uri)
470
+ # Convert string to AnyUrl
471
+ from mcp.types import AnyUrl
472
+
473
+ uri = AnyUrl(input_data.uri)
474
+ result = await session.read_resource(uri)
471
475
  for item in result.contents:
472
476
  if isinstance(item, mcp_types.TextResourceContents):
473
477
  parts.append(
@@ -535,12 +539,12 @@ class ReadMcpResourceTool(Tool[ReadMcpResourceInput, ReadMcpResourceOutput]):
535
539
  )
536
540
  )
537
541
 
538
- result = ReadMcpResourceOutput(
542
+ read_result: Any = ReadMcpResourceOutput(
539
543
  server=input_data.server, uri=input_data.uri, content=content_text, contents=parts
540
544
  )
541
545
  yield ToolResult(
542
- data=result,
543
- result_for_assistant=self.render_result_for_assistant(result),
546
+ data=read_result,
547
+ result_for_assistant=self.render_result_for_assistant(read_result), # type: ignore[arg-type]
544
548
  )
545
549
 
546
550
 
@@ -676,14 +680,16 @@ class DynamicMcpTool(Tool[BaseModel, McpToolCallOutput]):
676
680
 
677
681
  try:
678
682
  args = input_data.model_dump(exclude_none=True)
679
- result = await session.call_tool(
683
+ call_result = await session.call_tool(
680
684
  self.tool_info.name,
681
685
  args or {},
682
686
  )
683
- raw_blocks = getattr(result, "content", None)
687
+ raw_blocks = getattr(call_result, "content", None)
684
688
  content_blocks = _normalize_content_blocks(raw_blocks)
685
689
  content_text = _render_content_blocks(content_blocks) if content_blocks else None
686
- structured = result.structuredContent if hasattr(result, "structuredContent") else None
690
+ structured = (
691
+ call_result.structuredContent if hasattr(call_result, "structuredContent") else None
692
+ )
687
693
  assistant_text = content_text
688
694
  if structured:
689
695
  assistant_text = (assistant_text + "\n" if assistant_text else "") + json.dumps(
@@ -696,7 +702,7 @@ class DynamicMcpTool(Tool[BaseModel, McpToolCallOutput]):
696
702
  text=content_text,
697
703
  content_blocks=content_blocks,
698
704
  structured_content=structured,
699
- is_error=getattr(result, "isError", False),
705
+ is_error=getattr(call_result, "isError", False),
700
706
  )
701
707
  yield ToolResult(
702
708
  data=output,
@@ -124,7 +124,7 @@ class MultiEditTool(Tool[MultiEditToolInput, MultiEditToolOutput]):
124
124
  return [
125
125
  ToolUseExample(
126
126
  description="Apply multiple replacements in one pass",
127
- input={
127
+ example={
128
128
  "file_path": "/repo/src/app.py",
129
129
  "edits": [
130
130
  {"old_string": "DEBUG = True", "new_string": "DEBUG = False"},
@@ -134,7 +134,7 @@ class MultiEditTool(Tool[MultiEditToolInput, MultiEditToolOutput]):
134
134
  ),
135
135
  ToolUseExample(
136
136
  description="Create a new file then adjust content",
137
- input={
137
+ example={
138
138
  "file_path": "/repo/docs/notes.txt",
139
139
  "edits": [
140
140
  {"old_string": "", "new_string": "Line one\nLine two\n"},
@@ -101,7 +101,7 @@ class NotebookEditTool(Tool[NotebookEditInput, NotebookEditOutput]):
101
101
  return [
102
102
  ToolUseExample(
103
103
  description="Replace a markdown cell by id",
104
- input={
104
+ example={
105
105
  "notebook_path": "/repo/notebooks/analysis.ipynb",
106
106
  "cell_id": "abc123",
107
107
  "new_source": "# Updated overview\\nThis notebook analyzes revenue.",
@@ -111,7 +111,7 @@ class NotebookEditTool(Tool[NotebookEditInput, NotebookEditOutput]):
111
111
  ),
112
112
  ToolUseExample(
113
113
  description="Insert a new code cell at the beginning",
114
- input={
114
+ example={
115
115
  "notebook_path": "/repo/notebooks/analysis.ipynb",
116
116
  "cell_type": "code",
117
117
  "edit_mode": "insert",
@@ -246,7 +246,7 @@ class NotebookEditTool(Tool[NotebookEditInput, NotebookEditOutput]):
246
246
  "id": new_id,
247
247
  "source": new_source,
248
248
  "metadata": {},
249
- "execution_count": 0,
249
+ "execution_count": None, # type: ignore[dict-item]
250
250
  "outputs": [],
251
251
  }
252
252
  )
@@ -85,7 +85,7 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
85
85
  details: List[str] = []
86
86
  if output.tool_use_count:
87
87
  details.append(f"{output.tool_use_count} tool uses")
88
- details.append(f"{output.duration_ms/1000:.1f}s")
88
+ details.append(f"{output.duration_ms / 1000:.1f}s")
89
89
  if output.missing_tools:
90
90
  details.append(f"missing tools: {', '.join(output.missing_tools)}")
91
91
 
@@ -125,11 +125,18 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
125
125
  f"Missing or unknown tools: {', '.join(missing_tools) if missing_tools else 'none'}"
126
126
  )
127
127
 
128
- agent_system_prompt = self._build_agent_prompt(target_agent, agent_tools)
128
+ # Type conversion: List[object] -> List[Tool[Any, Any]]
129
+ from ripperdoc.core.tool import Tool
130
+
131
+ typed_agent_tools: List[Tool[Any, Any]] = [
132
+ tool for tool in agent_tools if isinstance(tool, Tool)
133
+ ]
134
+
135
+ agent_system_prompt = self._build_agent_prompt(target_agent, typed_agent_tools)
129
136
  subagent_messages = [create_user_message(input_data.prompt)]
130
137
 
131
138
  subagent_context = QueryContext(
132
- tools=agent_tools,
139
+ tools=typed_agent_tools,
133
140
  safe_mode=context.safe_mode,
134
141
  verbose=context.verbose,
135
142
  model=target_agent.model or "task",
@@ -142,7 +149,7 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
142
149
  yield ToolProgress(content=f"Launching subagent '{target_agent.agent_type}'")
143
150
 
144
151
  async for message in query(
145
- subagent_messages,
152
+ subagent_messages, # type: ignore[arg-type]
146
153
  agent_system_prompt,
147
154
  {},
148
155
  subagent_context,
@@ -183,7 +190,8 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
183
190
  )
184
191
  yield ToolProgress(content=f"Subagent: {short}")
185
192
  assistant_messages.append(message) # type: ignore[arg-type]
186
- tool_use_count += self._count_tool_uses(message)
193
+ if isinstance(message, AssistantMessage):
194
+ tool_use_count += self._count_tool_uses(message)
187
195
 
188
196
  duration_ms = (time.time() - start) * 1000
189
197
  result_text = (
@@ -140,7 +140,7 @@ class TodoWriteTool(Tool[TodoWriteToolInput, TodoToolOutput]):
140
140
  return [
141
141
  ToolUseExample(
142
142
  description="Seed a three-step plan",
143
- input={
143
+ example={
144
144
  "todos": [
145
145
  {
146
146
  "id": "plan",
@@ -165,7 +165,7 @@ class TodoWriteTool(Tool[TodoWriteToolInput, TodoToolOutput]):
165
165
  ),
166
166
  ToolUseExample(
167
167
  description="Update a single task already in progress",
168
- input={
168
+ example={
169
169
  "todos": [
170
170
  {
171
171
  "id": "bugfix-123",
@@ -263,11 +263,11 @@ class TodoReadTool(Tool[TodoReadToolInput, TodoToolOutput]):
263
263
  return [
264
264
  ToolUseExample(
265
265
  description="Get only the next actionable todo",
266
- input={"next_only": True},
266
+ example={"next_only": True},
267
267
  ),
268
268
  ToolUseExample(
269
269
  description="List recent completed tasks with a limit",
270
- input={"status": ["completed"], "limit": 5},
270
+ example={"status": ["completed"], "limit": 5},
271
271
  ),
272
272
  ]
273
273
 
@@ -94,11 +94,11 @@ class ToolSearchTool(Tool[ToolSearchInput, ToolSearchOutput]):
94
94
  return [
95
95
  ToolUseExample(
96
96
  description="Search for notebook-related tools and activate top results",
97
- input={"query": "notebook", "max_results": 3},
97
+ example={"query": "notebook", "max_results": 3},
98
98
  ),
99
99
  ToolUseExample(
100
100
  description="Activate a known tool by name",
101
- input={"names": ["mcp__search__query"], "auto_activate": True},
101
+ example={"names": ["mcp__search__query"], "auto_activate": True},
102
102
  ),
103
103
  ]
104
104
 
@@ -119,7 +119,9 @@ class ToolSearchTool(Tool[ToolSearchInput, ToolSearchOutput]):
119
119
  return False
120
120
 
121
121
  async def validate_input(
122
- self, input_data: ToolSearchInput, context: Optional[ToolUseContext] = None # noqa: ARG002
122
+ self,
123
+ input_data: ToolSearchInput,
124
+ context: Optional[ToolUseContext] = None, # noqa: ARG002
123
125
  ) -> ValidationResult:
124
126
  if not (input_data.query or input_data.names):
125
127
  return ValidationResult(
@@ -193,7 +195,7 @@ class ToolSearchTool(Tool[ToolSearchInput, ToolSearchOutput]):
193
195
 
194
196
  avg_len = sum(doc_len for _, _, _, doc_len, _ in corpus) / len(corpus)
195
197
  query_terms = _tokenize(normalized)
196
- df = defaultdict(int)
198
+ df: Dict[str, int] = defaultdict(int)
197
199
  for _, _, tokens, _, _ in corpus:
198
200
  seen_terms = set(tokens)
199
201
  for term in query_terms:
ripperdoc/utils/mcp.py CHANGED
@@ -72,7 +72,10 @@ def _load_json_file(path: Path) -> Dict[str, Any]:
72
72
  if not path.exists():
73
73
  return {}
74
74
  try:
75
- return json.loads(path.read_text())
75
+ data = json.loads(path.read_text())
76
+ if isinstance(data, dict):
77
+ return data
78
+ return {}
76
79
  except (OSError, json.JSONDecodeError) as exc:
77
80
  logger.error(f"Failed to load JSON from {path}: {exc}")
78
81
  return {}
@@ -181,11 +184,11 @@ class McpRuntime:
181
184
  self.servers.append(await self._connect_server(config))
182
185
  return self.servers
183
186
 
184
- async def _list_roots_callback(self, *_: Any, **__: Any):
187
+ async def _list_roots_callback(self, *_: Any, **__: Any) -> Optional[Any]:
185
188
  if not mcp_types:
186
189
  return None
187
190
  return mcp_types.ListRootsResult(
188
- roots=[mcp_types.Root(uri=Path(self.project_path).resolve().as_uri())]
191
+ roots=[mcp_types.Root(uri=Path(self.project_path).resolve().as_uri())] # type: ignore[arg-type]
189
192
  )
190
193
 
191
194
  async def _connect_server(self, config: McpServerInfo) -> McpServerInfo:
@@ -228,17 +231,22 @@ class McpRuntime:
228
231
  stdio_client(stdio_params)
229
232
  )
230
233
 
234
+ if read_stream is None or write_stream is None:
235
+ raise ValueError("Failed to create read/write streams for MCP server")
236
+
231
237
  session = await self._exit_stack.enter_async_context(
232
238
  ClientSession(
233
239
  read_stream,
234
240
  write_stream,
235
- list_roots_callback=self._list_roots_callback,
241
+ list_roots_callback=self._list_roots_callback, # type: ignore[arg-type]
236
242
  client_info=mcp_types.Implementation(name="ripperdoc", version=__version__),
237
243
  )
238
244
  )
239
245
 
240
246
  init_result = await session.initialize()
241
247
  capabilities = session.get_server_capabilities()
248
+ if capabilities is None:
249
+ capabilities = mcp_types.ServerCapabilities()
242
250
 
243
251
  info.status = "connected"
244
252
  info.instructions = init_result.instructions or info.instructions
@@ -281,7 +281,9 @@ def get_remaining_context_tokens(
281
281
  """Return the context window minus the model's configured output tokens."""
282
282
  context_limit = max(get_model_context_limit(model_profile, explicit_limit), MIN_CONTEXT_TOKENS)
283
283
  try:
284
- max_output_tokens = int(getattr(model_profile, "max_tokens", 0) or 0) if model_profile else 0
284
+ max_output_tokens = (
285
+ int(getattr(model_profile, "max_tokens", 0) or 0) if model_profile else 0
286
+ )
285
287
  except (TypeError, ValueError):
286
288
  max_output_tokens = 0
287
289
  return max(MIN_CONTEXT_TOKENS, context_limit - max(0, max_output_tokens))
@@ -304,11 +306,15 @@ def get_context_usage_status(
304
306
  """Compute context usage thresholds following claude-code semantics."""
305
307
  context_limit = max(max_context_tokens or DEFAULT_CONTEXT_TOKENS, MIN_CONTEXT_TOKENS)
306
308
  effective_limit = (
307
- max(MIN_CONTEXT_TOKENS, context_limit - AUTO_COMPACT_BUFFER) if auto_compact_enabled else context_limit
309
+ max(MIN_CONTEXT_TOKENS, context_limit - AUTO_COMPACT_BUFFER)
310
+ if auto_compact_enabled
311
+ else context_limit
308
312
  )
309
313
 
310
314
  tokens_left = max(effective_limit - used_tokens, 0)
311
- percent_left = 0.0 if effective_limit <= 0 else min(100.0, (tokens_left / effective_limit) * 100)
315
+ percent_left = (
316
+ 0.0 if effective_limit <= 0 else min(100.0, (tokens_left / effective_limit) * 100)
317
+ )
312
318
  percent_used = 100.0 - percent_left
313
319
 
314
320
  warning_limit = max(0, effective_limit - WARNING_THRESHOLD)
@@ -451,7 +457,9 @@ def _estimate_message_tokens(content_block: Any) -> int:
451
457
  if isinstance(content, list):
452
458
  total = 0
453
459
  for part in content:
454
- part_type = getattr(part, "type", None) or (part.get("type") if isinstance(part, dict) else None)
460
+ part_type = getattr(part, "type", None) or (
461
+ part.get("type") if isinstance(part, dict) else None
462
+ )
455
463
  if part_type == "text":
456
464
  text_val = getattr(part, "text", None) if hasattr(part, "text") else None
457
465
  if text_val is None and isinstance(part, dict):
@@ -531,7 +539,9 @@ def compact_messages(
531
539
  token_counts_by_tool_use_id[tool_use_id] = token_count
532
540
 
533
541
  latest_tool_use_ids = (
534
- tool_use_ids_to_compact[-MAX_TOOL_USES_TO_PRESERVE:] if MAX_TOOL_USES_TO_PRESERVE > 0 else []
542
+ tool_use_ids_to_compact[-MAX_TOOL_USES_TO_PRESERVE:]
543
+ if MAX_TOOL_USES_TO_PRESERVE > 0
544
+ else []
535
545
  )
536
546
  total_token_count = sum(token_counts_by_tool_use_id.values())
537
547
 
@@ -571,7 +581,7 @@ def compact_messages(
571
581
  compacted_messages.append(message)
572
582
  continue
573
583
 
574
- if msg_type == "assistant":
584
+ if msg_type == "assistant" and isinstance(message, AssistantMessage):
575
585
  # Copy content list to avoid mutating the original message.
576
586
  compacted_messages.append(
577
587
  AssistantMessage(
@@ -597,7 +607,11 @@ def compact_messages(
597
607
  new_block = content_item.model_copy()
598
608
  new_block.text = COMPACT_PLACEHOLDER
599
609
  else:
600
- block_dict = dict(content_item) if isinstance(content_item, dict) else {"type": "tool_result"}
610
+ block_dict = (
611
+ dict(content_item)
612
+ if isinstance(content_item, dict)
613
+ else {"type": "tool_result"}
614
+ )
601
615
  block_dict["text"] = COMPACT_PLACEHOLDER
602
616
  block_dict["tool_use_id"] = tool_use_id
603
617
  new_block = MessageContent(**block_dict)
@@ -608,9 +622,11 @@ def compact_messages(
608
622
  elif isinstance(content_item, dict):
609
623
  filtered_content.append(MessageContent(**content_item))
610
624
  else:
611
- filtered_content.append(MessageContent(type=str(block_type or "text"), text=str(content_item)))
625
+ filtered_content.append(
626
+ MessageContent(type=str(block_type or "text"), text=str(content_item))
627
+ )
612
628
 
613
- if modified:
629
+ if modified and isinstance(message, UserMessage):
614
630
  compacted_messages.append(
615
631
  UserMessage(
616
632
  message=message.message.model_copy(update={"content": filtered_content}),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripperdoc
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: AI-powered terminal assistant for coding tasks
5
5
  Author: Ripperdoc Team
6
6
  License: Apache-2.0
@@ -1,65 +1,65 @@
1
1
  ripperdoc/__init__.py,sha256=8uE0Wb2dqhd3j7g6jm1ObxZ2cn3yf5E3SoVpLzYcFJs,66
2
2
  ripperdoc/__main__.py,sha256=7oIFEXI2irIoZ_dhcMd3hCs4Dj8tmMBbwiVACAoeE-k,506
3
3
  ripperdoc/cli/__init__.py,sha256=03wf6gXBcEgXJrDJS-W_5BEG_DdJ_ep7CxQFPML-73g,35
4
- ripperdoc/cli/cli.py,sha256=aqQh6WUZ4m2hFlPCm1MYho9TUXv_YtALIu2vP__aCC8,10654
4
+ ripperdoc/cli/cli.py,sha256=ldVNPsZIEdwaNwbrwbgAZMfQFQ1tEXw5uz26fP7avGc,10928
5
5
  ripperdoc/cli/commands/__init__.py,sha256=2ymRdOj3EekyIioOxhJNMZyjWaSNVixNZZ6DONfTXtc,2189
6
- ripperdoc/cli/commands/agents_cmd.py,sha256=toFweHBztgyrOzM--mRklei_QOUS56HO2zhPcWk8XvQ,9280
6
+ ripperdoc/cli/commands/agents_cmd.py,sha256=_4Cf1onhtgVrT4PnVPqHLueiAEbJcM9VP-_usPs77xE,9277
7
7
  ripperdoc/cli/commands/base.py,sha256=4KUjxCM04MwbSMUKVNEBph_jeAKPI8b5MHsUFoz7l5g,386
8
8
  ripperdoc/cli/commands/clear_cmd.py,sha256=zSYT0Nn_htZzLWTTQ4E5KWHfRg0Q5CYvRO4e--7thBY,345
9
9
  ripperdoc/cli/commands/compact_cmd.py,sha256=C_qdPTPdg1cOHdmODkaYoRusosgiqRK6c6KID_Gwq0k,330
10
10
  ripperdoc/cli/commands/config_cmd.py,sha256=ebIQk7zUFv353liWfbBSSfPiOaaCR7rQsd_eTw7nsvY,884
11
- ripperdoc/cli/commands/context_cmd.py,sha256=qSNVFtf_g7FLmSvuyeSHBZOrEkDqYNMxAsgmVxuHTpA,3988
12
- ripperdoc/cli/commands/cost_cmd.py,sha256=Ywtyf0IpIkb8Z4rAOXoXGcqSxd-kWx1cIDMfE7Zs3wc,2641
11
+ ripperdoc/cli/commands/context_cmd.py,sha256=8PpTi4b0-tcxuxf4Qh59HOyH4eGmGe8TV7T_KrS_9yw,4007
12
+ ripperdoc/cli/commands/cost_cmd.py,sha256=AoGRDDu48qsMGuS5zgScg_YdNqkwoj33D2PTh-1uZ34,2637
13
13
  ripperdoc/cli/commands/exit_cmd.py,sha256=B0CNKQos2eRC4LSjizLdKsFYzFfwRkrUur6Afu3Fh9M,334
14
14
  ripperdoc/cli/commands/help_cmd.py,sha256=iz1vR-rmWsvvfzdebLiIWEWrcMZo5_Eb55_wLr4Ufno,508
15
15
  ripperdoc/cli/commands/mcp_cmd.py,sha256=S0iQxclxqgbIxbKcC9oFrckLalzk-1eyAYwkfEZQsGU,2307
16
16
  ripperdoc/cli/commands/models_cmd.py,sha256=fQOwb_5_vNPNKF2NHepvLrsE25qTETPC9zlkvnrRjxs,11915
17
- ripperdoc/cli/commands/resume_cmd.py,sha256=KFbvoQ0K8xo17x7AZHNki-PLnkH-ZKpswfPY12silSo,2968
18
- ripperdoc/cli/commands/status_cmd.py,sha256=BlbqUeKTGbMs4SZvlbUXOTGAEDMw8wJs0fem6e_EU2E,5485
19
- ripperdoc/cli/commands/tasks_cmd.py,sha256=YT50pipKRPka8dgqWs7CL_uTvA7l-Hnu2jiygM1IB9Q,7244
17
+ ripperdoc/cli/commands/resume_cmd.py,sha256=F99haT29dUHYlgrIYKk2cadSOTrwkLOEs_D2RotNZXI,2977
18
+ ripperdoc/cli/commands/status_cmd.py,sha256=rAhHksegqdLGy551Hh22oVwR6ZSfkDvAPOjNiNQJ9_s,5494
19
+ ripperdoc/cli/commands/tasks_cmd.py,sha256=oCGEdfjCqPSHJOAWGQUABl7X5ltYJWtRPB69b8d6m4w,7286
20
20
  ripperdoc/cli/commands/todos_cmd.py,sha256=7Q0B1NVqGtB3R29ndbn4m0VQQm-YQ7d4Wlk7vJ7dLQI,1848
21
21
  ripperdoc/cli/commands/tools_cmd.py,sha256=3cMi0vN4mAUhpKqJtRgNvZfcKzRPaMs_pkYYXlyvSSU,384
22
22
  ripperdoc/cli/ui/__init__.py,sha256=TxSzTYdITlrYmYVfins_w_jzPqqWRpqky5u1ikwvmtM,43
23
- ripperdoc/cli/ui/context_display.py,sha256=YEHTx9-DeHS2zv2UFJ9PzWOc8kXSJsnzaXB5S8EJ2kI,9894
23
+ ripperdoc/cli/ui/context_display.py,sha256=3ezdtHVwltkPQ5etYwfqUh-fjnpPu8B3P81UzrdHxZs,10020
24
24
  ripperdoc/cli/ui/helpers.py,sha256=TJCipP0neh-96ETQfGhusCJ4aWt5gLw1HZbI-3bWDpw,739
25
- ripperdoc/cli/ui/rich_ui.py,sha256=l9WxAvXVf-rVLroKoR4YM79UgEFLP6TjGfWhhLDKkxw,43123
26
- ripperdoc/cli/ui/spinner.py,sha256=KH2uwJcuAzqJa3nX4eKqmw67ykF7z0Phh5413APGXd8,1445
25
+ ripperdoc/cli/ui/rich_ui.py,sha256=jBTcJc5hTXEgeih7CtJUqo6sB7nVwEg3NGZ4DvJI9QA,43907
26
+ ripperdoc/cli/ui/spinner.py,sha256=XsPRwJ-70InLX9Qw50CEgSHn5oKA5PFIue8Un4edhUk,1449
27
27
  ripperdoc/core/__init__.py,sha256=UemJCA-Y8df1466AX-YbRFj071zKajmqO1mi40YVW2g,40
28
- ripperdoc/core/agents.py,sha256=N5FY2lFT3TLFDKPUtdlWU5R8SdNqak7TbgJqulYaXaM,10204
28
+ ripperdoc/core/agents.py,sha256=sLgMeoDYXciBszRLCdbVCZRfxzXjlwGpWcx8qFk1Xm4,10240
29
29
  ripperdoc/core/commands.py,sha256=NXCkljYbAP4dDoRy-_3semFNWxG4YAk9q82u8FTKH60,835
30
30
  ripperdoc/core/config.py,sha256=gxR0uOhX11nv9fUQpcnQ1fHRtUEpt6v1GHSItyYxQYA,13240
31
- ripperdoc/core/default_tools.py,sha256=0gxoGux69o32ft6i2y4by_ULL3BXvRkCZysuIGTrdCU,1845
31
+ ripperdoc/core/default_tools.py,sha256=PpZ49yjDzV5vtSkYq_quxY8u-nk5uJPp25RfMa9wZtA,2086
32
32
  ripperdoc/core/permissions.py,sha256=lEKAUX9ksQxOFrPUkwwQe_3PMAr2xKgGBwjTOz8a8hM,8398
33
- ripperdoc/core/query.py,sha256=MCOYbRWGyuCmhF4uXDzntmeeIrrtnpjTSG95pvJjMtI,26150
33
+ ripperdoc/core/query.py,sha256=f3sfzM9vSZJGbCUMg2wmT7iESCgFrNv2DqGYL38mJK0,26329
34
34
  ripperdoc/core/system_prompt.py,sha256=OG_GUm9E30FnvL4p2L3_1A7oG54V0Kw5xRJPRzJrx2o,24066
35
- ripperdoc/core/tool.py,sha256=XQWnqrHd13h8Vb26kW57lmw9k2V-Trn18IW6737Kwck,6414
35
+ ripperdoc/core/tool.py,sha256=GbmQSVc8XQXw4aDVwFpYGZV3OFbU5wIz7n3xNWlNeyI,6446
36
36
  ripperdoc/sdk/__init__.py,sha256=aDSgI4lcCDs9cV3bxNmEEV3SuYx2aCd4VnUjs6H-R7E,213
37
- ripperdoc/sdk/client.py,sha256=6Tu7oh0_hnD0OyKMkHiEe1run_yCuGn0qyWfPiHJGVk,10541
37
+ ripperdoc/sdk/client.py,sha256=21f8viIh3BhSjXcI_MvgzfAcy7r289Thyhc_qgnQrB0,10556
38
38
  ripperdoc/tools/__init__.py,sha256=RBFz0DDnztDXMqv_zRxFHVY-ez2HYcncx8zh_y-BX6w,42
39
39
  ripperdoc/tools/background_shell.py,sha256=zdea03hpSGHEsVreyApn2bWMLzMQYo5ZmaHDp_Q11SA,9235
40
40
  ripperdoc/tools/bash_output_tool.py,sha256=ljIOzTOnkbQfe3jExlhpUlMiLT6HpeD-1QI-D1CwHh8,3379
41
- ripperdoc/tools/bash_tool.py,sha256=vb_ONooKt7VE7UO3YOLHnYDWvMzFzEVLrlC2qbB0TdU,36063
42
- ripperdoc/tools/file_edit_tool.py,sha256=mybTI2KNGPc1Dwnm5CSz-amELLXBMCIYwKmar6Cka6A,11149
43
- ripperdoc/tools/file_read_tool.py,sha256=o-N3wol6vUHEq40Qk5ubQIkaqCpIa8i0n-WMZMYm0ZA,5864
44
- ripperdoc/tools/file_write_tool.py,sha256=gigkuAwLK5-PA3xHR-hd0qALo3LsRBL1vOL6xl9UFUY,4447
45
- ripperdoc/tools/glob_tool.py,sha256=Sm2yBp59OYWgDik9kcwJW8UbcFZjffwC_tWRcgzRXSc,4305
46
- ripperdoc/tools/grep_tool.py,sha256=ZrsjXD7BEuyFXeYsTbVWI0L_9P41alFtRQtPDVjfY9g,8094
41
+ ripperdoc/tools/bash_tool.py,sha256=Ww92WIybH_5fqCJTEyR6JcIL6OQgj283uqvkFa1paqU,36071
42
+ ripperdoc/tools/file_edit_tool.py,sha256=zJib_Lqh9MlgPbNat9-LK1TTt8GsAXIuBEusuGWw8v8,11153
43
+ ripperdoc/tools/file_read_tool.py,sha256=EKA-OvDgRRzIMeK4t5ZFavB5ZTk0TzOJwxg-vD5ggho,5868
44
+ ripperdoc/tools/file_write_tool.py,sha256=X4b8PDJszRtp_B6daCsUsQAwUc-4PsNbXpm3FUG5sR0,4569
45
+ ripperdoc/tools/glob_tool.py,sha256=WxTz-cOz-whMxbQGCLpNmXfLgnxH9dh-1EFh11id64Q,4309
46
+ ripperdoc/tools/grep_tool.py,sha256=KyR5abvzy780evWu6tuMv9v6xGyGrTlLliJHan1N3S4,8098
47
47
  ripperdoc/tools/kill_bash_tool.py,sha256=36F8w2Rm1IVQitwOAwS-D8NTnyQdWfKWIam44qlXErk,4625
48
- ripperdoc/tools/ls_tool.py,sha256=6f-lIdHo_LRTbUzYnkfaIzH86wrAVnQ7Ksbak0HnxtE,8926
49
- ripperdoc/tools/mcp_tools.py,sha256=hMxKhwnqgirRhaby_8U7Ag06cwZtoIcR4uylClwEz5E,30055
50
- ripperdoc/tools/multi_edit_tool.py,sha256=QlYr6nu58UCJ81wC3QD_HT3X-NtYssp54F5j1EK-9A0,14964
51
- ripperdoc/tools/notebook_edit_tool.py,sha256=_wvEFJhSfxifgGhypctbWQXLGP4DcttjkzgQQTMbiT0,12030
52
- ripperdoc/tools/task_tool.py,sha256=Z4T0FoVysvNbyGotdvNOyJeEA9jXmo4Emnbl4L86pew,11397
53
- ripperdoc/tools/todo_tool.py,sha256=fo8Kv_DRo9v5zjrwSz7qzbG9QfPtBpOOg9sBAuTOIUk,12847
54
- ripperdoc/tools/tool_search_tool.py,sha256=xJ9k5UIbBcVtNAAGIMha7fXq_f07CcQVsrSJ8f5JaLU,13296
48
+ ripperdoc/tools/ls_tool.py,sha256=H9pYsZKwx7j7Ow98eTx67KNKFbNO0yY-JUX34lAuhy8,8928
49
+ ripperdoc/tools/mcp_tools.py,sha256=msGNVKx8aDoLhs7Xvta1XMdLNP78W8Pi_CqLhwIjKiM,30309
50
+ ripperdoc/tools/multi_edit_tool.py,sha256=EpkCsjVTXTa4fOfETABjXNoKvQ6LRK8Xnv_t22N_Mcg,14968
51
+ ripperdoc/tools/notebook_edit_tool.py,sha256=y7_NuB7NrmUu-KkRQfkSegHRuiyPYdGxpjD9pFb_f5M,12064
52
+ ripperdoc/tools/task_tool.py,sha256=qp8jYLy8VMriny4fqmobYXh1dac2UyCBSAXZNk4iI0w,11739
53
+ ripperdoc/tools/todo_tool.py,sha256=aB0ejA_np3zLWTNUmC7yEVlx46tTtT_WgHEsqrbXIn4,12855
54
+ ripperdoc/tools/tool_search_tool.py,sha256=l3DU2UuCPU-XSvFDsdIrl0VkdwtR_E5dMk0idAP2p2w,13333
55
55
  ripperdoc/utils/__init__.py,sha256=gdso60znB2hsYZ_YZBKVcuOY3QVfoqD2wHQ4pvr5lSw,37
56
56
  ripperdoc/utils/bash_constants.py,sha256=KNn8bzB6nVU5jid9jvjiH4FAu8pP3DZONJ-OknJypAQ,1641
57
57
  ripperdoc/utils/bash_output_utils.py,sha256=3Cf5wKJzRbUesmCNy5HtXIBtv0Z2BxklTfFHJ9q1T3w,1210
58
58
  ripperdoc/utils/exit_code_handlers.py,sha256=QtO1iDxVAb8Xp03D6_QixPoJC-RQlcp3ssIo_rm4two,7973
59
59
  ripperdoc/utils/log.py,sha256=QoqIxTctArJ3L5dwL6v4AZkMTGQozhDlNdM5BAn3QL8,2361
60
- ripperdoc/utils/mcp.py,sha256=VEwNMnVb5peSx4N6imX-IkuWl6-qq7Su5UngP9jeRdc,14259
60
+ ripperdoc/utils/mcp.py,sha256=w2yHygSWNUKyM4AU2xZmfEMyfN3oZAHuswtEAD0v1Dc,14652
61
61
  ripperdoc/utils/memory.py,sha256=5Shv8O8VjlUz0JHbIYm6oPLP5GAzzhJ3ZJIf3-GHXnE,7336
62
- ripperdoc/utils/message_compaction.py,sha256=fll2iGzeozyGGmOuurGVAz3ifI7-gBQeDYDfcMQ6DPM,23789
62
+ ripperdoc/utils/message_compaction.py,sha256=NVyDU58wsFuW85XWemKJqvtA1BMR-J8qCx10BIYKwnw,24112
63
63
  ripperdoc/utils/messages.py,sha256=1vAYabUXbuywYQETH7GStQGehPnkxrZ7ZkzeXbv4JC4,14725
64
64
  ripperdoc/utils/output_utils.py,sha256=9vROdxdZ_8FO1I_OxuTuTeUPWcJoWy9laTUFzTCDths,6162
65
65
  ripperdoc/utils/path_utils.py,sha256=C45Q3OeXnj-0FVEtvf_tdG5922XB6HthUzlUCvfc17Y,1626
@@ -73,9 +73,9 @@ ripperdoc/utils/permissions/__init__.py,sha256=-1aKvRC05kuvLacbeu7w1W5ANamOTAho4
73
73
  ripperdoc/utils/permissions/path_validation_utils.py,sha256=0saGffF-IwCHc_GrDimU8Vz_SAczJb_Ui29ea_hkj4E,5482
74
74
  ripperdoc/utils/permissions/shell_command_validation.py,sha256=BkK-wEwAuJPoC4XIx2Zj6MF3gXcWReozIwigXgib0z0,2446
75
75
  ripperdoc/utils/permissions/tool_permission_utils.py,sha256=6Fdu9-dMKhLsUExjEjoS0EUeRpEVN5UkqyseIC05YmM,9207
76
- ripperdoc-0.1.0.dist-info/licenses/LICENSE,sha256=bRv9UhBor6GhnQDj12RciDcRfu0R7sB-lqCy1sWF75c,9242
77
- ripperdoc-0.1.0.dist-info/METADATA,sha256=qAeFy7Ot8555XrISwT9Z_cgbvHRd1mP_UY630P4w3Xc,5317
78
- ripperdoc-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
79
- ripperdoc-0.1.0.dist-info/entry_points.txt,sha256=79aohFxFPJmrQ3-Mhain04vb3EWpuc0EyzvDDUnwAu4,81
80
- ripperdoc-0.1.0.dist-info/top_level.txt,sha256=u8LbdTr1a-laHgCO0Utl_R3QGFUhLxWelCDnP2ZgpCU,10
81
- ripperdoc-0.1.0.dist-info/RECORD,,
76
+ ripperdoc-0.2.0.dist-info/licenses/LICENSE,sha256=bRv9UhBor6GhnQDj12RciDcRfu0R7sB-lqCy1sWF75c,9242
77
+ ripperdoc-0.2.0.dist-info/METADATA,sha256=dwxYrx56ASM7ZbKyF5n6ZfZq9DwGfHU8sJWnOVCTY7M,5317
78
+ ripperdoc-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
79
+ ripperdoc-0.2.0.dist-info/entry_points.txt,sha256=79aohFxFPJmrQ3-Mhain04vb3EWpuc0EyzvDDUnwAu4,81
80
+ ripperdoc-0.2.0.dist-info/top_level.txt,sha256=u8LbdTr1a-laHgCO0Utl_R3QGFUhLxWelCDnP2ZgpCU,10
81
+ ripperdoc-0.2.0.dist-info/RECORD,,