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/utils/memory.py
CHANGED
|
@@ -6,6 +6,9 @@ import re
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import List, Optional, Set
|
|
9
|
+
from ripperdoc.utils.log import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger()
|
|
9
12
|
|
|
10
13
|
MEMORY_FILE_NAME = "AGENTS.md"
|
|
11
14
|
LOCAL_MEMORY_FILE_NAME = "AGENTS.local.md"
|
|
@@ -43,6 +46,10 @@ def _is_path_under_directory(path: Path, directory: Path) -> bool:
|
|
|
43
46
|
path.resolve().relative_to(directory.resolve())
|
|
44
47
|
return True
|
|
45
48
|
except Exception:
|
|
49
|
+
logger.exception(
|
|
50
|
+
"[memory] Failed to compare path containment",
|
|
51
|
+
extra={"path": str(path), "directory": str(directory)},
|
|
52
|
+
)
|
|
46
53
|
return False
|
|
47
54
|
|
|
48
55
|
|
|
@@ -65,8 +72,12 @@ def _read_file_with_type(file_path: Path, file_type: str) -> Optional[MemoryFile
|
|
|
65
72
|
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
66
73
|
return MemoryFile(path=str(file_path), type=file_type, content=content)
|
|
67
74
|
except PermissionError:
|
|
75
|
+
logger.exception(
|
|
76
|
+
"[memory] Permission error reading file", extra={"path": str(file_path)}
|
|
77
|
+
)
|
|
68
78
|
return None
|
|
69
79
|
except OSError:
|
|
80
|
+
logger.exception("[memory] OS error reading file", extra={"path": str(file_path)})
|
|
70
81
|
return None
|
|
71
82
|
|
|
72
83
|
|
|
@@ -114,7 +125,9 @@ def _collect_files(
|
|
|
114
125
|
try:
|
|
115
126
|
resolved_path = resolved_path.resolve()
|
|
116
127
|
except Exception:
|
|
117
|
-
|
|
128
|
+
logger.exception(
|
|
129
|
+
"[memory] Failed to resolve memory file path", extra={"path": str(resolved_path)}
|
|
130
|
+
)
|
|
118
131
|
|
|
119
132
|
resolved_key = str(resolved_path)
|
|
120
133
|
if resolved_key in visited:
|
|
@@ -22,7 +22,7 @@ logger = get_logger()
|
|
|
22
22
|
|
|
23
23
|
ConversationMessage = Union[UserMessage, AssistantMessage, ProgressMessage]
|
|
24
24
|
|
|
25
|
-
# Compaction thresholds
|
|
25
|
+
# Compaction thresholds.
|
|
26
26
|
MAX_TOKENS_SOFT = 20_000
|
|
27
27
|
MAX_TOKENS_HARD = 40_000
|
|
28
28
|
MAX_TOOL_USES_TO_PRESERVE = 3
|
|
@@ -172,6 +172,9 @@ def _stringify_content(content: Union[str, List[MessageContent], None]) -> str:
|
|
|
172
172
|
try:
|
|
173
173
|
parts.append(json.dumps(part.get("input"), ensure_ascii=False))
|
|
174
174
|
except Exception:
|
|
175
|
+
logger.exception(
|
|
176
|
+
"[message_compaction] Failed to serialize tool_use input for token estimate"
|
|
177
|
+
)
|
|
175
178
|
parts.append(str(part.get("input")))
|
|
176
179
|
|
|
177
180
|
# OpenAI-style arguments blocks
|
|
@@ -225,7 +228,10 @@ def _estimate_tool_schema_tokens(tools: Sequence[Any]) -> int:
|
|
|
225
228
|
schema_text = json.dumps(schema, sort_keys=True)
|
|
226
229
|
total += estimate_tokens_from_text(schema_text)
|
|
227
230
|
except Exception as exc:
|
|
228
|
-
logger.
|
|
231
|
+
logger.exception(
|
|
232
|
+
"Failed to estimate tokens for tool schema",
|
|
233
|
+
extra={"tool": getattr(tool, "name", None), "error": str(exc)},
|
|
234
|
+
)
|
|
229
235
|
continue
|
|
230
236
|
return total
|
|
231
237
|
|
|
@@ -281,7 +287,9 @@ def get_remaining_context_tokens(
|
|
|
281
287
|
"""Return the context window minus the model's configured output tokens."""
|
|
282
288
|
context_limit = max(get_model_context_limit(model_profile, explicit_limit), MIN_CONTEXT_TOKENS)
|
|
283
289
|
try:
|
|
284
|
-
max_output_tokens =
|
|
290
|
+
max_output_tokens = (
|
|
291
|
+
int(getattr(model_profile, "max_tokens", 0) or 0) if model_profile else 0
|
|
292
|
+
)
|
|
285
293
|
except (TypeError, ValueError):
|
|
286
294
|
max_output_tokens = 0
|
|
287
295
|
return max(MIN_CONTEXT_TOKENS, context_limit - max(0, max_output_tokens))
|
|
@@ -301,14 +309,18 @@ def get_context_usage_status(
|
|
|
301
309
|
max_context_tokens: Optional[int],
|
|
302
310
|
auto_compact_enabled: bool,
|
|
303
311
|
) -> ContextUsageStatus:
|
|
304
|
-
"""Compute context usage thresholds
|
|
312
|
+
"""Compute context usage thresholds using the compaction heuristics."""
|
|
305
313
|
context_limit = max(max_context_tokens or DEFAULT_CONTEXT_TOKENS, MIN_CONTEXT_TOKENS)
|
|
306
314
|
effective_limit = (
|
|
307
|
-
max(MIN_CONTEXT_TOKENS, context_limit - AUTO_COMPACT_BUFFER)
|
|
315
|
+
max(MIN_CONTEXT_TOKENS, context_limit - AUTO_COMPACT_BUFFER)
|
|
316
|
+
if auto_compact_enabled
|
|
317
|
+
else context_limit
|
|
308
318
|
)
|
|
309
319
|
|
|
310
320
|
tokens_left = max(effective_limit - used_tokens, 0)
|
|
311
|
-
percent_left =
|
|
321
|
+
percent_left = (
|
|
322
|
+
0.0 if effective_limit <= 0 else min(100.0, (tokens_left / effective_limit) * 100)
|
|
323
|
+
)
|
|
312
324
|
percent_used = 100.0 - percent_left
|
|
313
325
|
|
|
314
326
|
warning_limit = max(0, effective_limit - WARNING_THRESHOLD)
|
|
@@ -390,6 +402,9 @@ def find_latest_assistant_usage_tokens(
|
|
|
390
402
|
if tokens > 0:
|
|
391
403
|
return tokens
|
|
392
404
|
except Exception:
|
|
405
|
+
logger.debug(
|
|
406
|
+
"[message_compaction] Failed to parse usage tokens", exc_info=True
|
|
407
|
+
)
|
|
393
408
|
continue
|
|
394
409
|
return 0
|
|
395
410
|
|
|
@@ -426,7 +441,9 @@ def _run_cleanup_callbacks() -> None:
|
|
|
426
441
|
try:
|
|
427
442
|
callback()
|
|
428
443
|
except Exception as exc:
|
|
429
|
-
logger.debug(
|
|
444
|
+
logger.debug(
|
|
445
|
+
f"[message_compaction] Cleanup callback failed: {exc}", exc_info=True
|
|
446
|
+
)
|
|
430
447
|
|
|
431
448
|
|
|
432
449
|
def _normalize_tool_use_id(block: Any) -> str:
|
|
@@ -451,7 +468,9 @@ def _estimate_message_tokens(content_block: Any) -> int:
|
|
|
451
468
|
if isinstance(content, list):
|
|
452
469
|
total = 0
|
|
453
470
|
for part in content:
|
|
454
|
-
part_type = getattr(part, "type", None) or (
|
|
471
|
+
part_type = getattr(part, "type", None) or (
|
|
472
|
+
part.get("type") if isinstance(part, dict) else None
|
|
473
|
+
)
|
|
455
474
|
if part_type == "text":
|
|
456
475
|
text_val = getattr(part, "text", None) if hasattr(part, "text") else None
|
|
457
476
|
if text_val is None and isinstance(part, dict):
|
|
@@ -531,7 +550,9 @@ def compact_messages(
|
|
|
531
550
|
token_counts_by_tool_use_id[tool_use_id] = token_count
|
|
532
551
|
|
|
533
552
|
latest_tool_use_ids = (
|
|
534
|
-
tool_use_ids_to_compact[-MAX_TOOL_USES_TO_PRESERVE:]
|
|
553
|
+
tool_use_ids_to_compact[-MAX_TOOL_USES_TO_PRESERVE:]
|
|
554
|
+
if MAX_TOOL_USES_TO_PRESERVE > 0
|
|
555
|
+
else []
|
|
535
556
|
)
|
|
536
557
|
total_token_count = sum(token_counts_by_tool_use_id.values())
|
|
537
558
|
|
|
@@ -571,7 +592,7 @@ def compact_messages(
|
|
|
571
592
|
compacted_messages.append(message)
|
|
572
593
|
continue
|
|
573
594
|
|
|
574
|
-
if msg_type == "assistant":
|
|
595
|
+
if msg_type == "assistant" and isinstance(message, AssistantMessage):
|
|
575
596
|
# Copy content list to avoid mutating the original message.
|
|
576
597
|
compacted_messages.append(
|
|
577
598
|
AssistantMessage(
|
|
@@ -597,7 +618,11 @@ def compact_messages(
|
|
|
597
618
|
new_block = content_item.model_copy()
|
|
598
619
|
new_block.text = COMPACT_PLACEHOLDER
|
|
599
620
|
else:
|
|
600
|
-
block_dict =
|
|
621
|
+
block_dict = (
|
|
622
|
+
dict(content_item)
|
|
623
|
+
if isinstance(content_item, dict)
|
|
624
|
+
else {"type": "tool_result"}
|
|
625
|
+
)
|
|
601
626
|
block_dict["text"] = COMPACT_PLACEHOLDER
|
|
602
627
|
block_dict["tool_use_id"] = tool_use_id
|
|
603
628
|
new_block = MessageContent(**block_dict)
|
|
@@ -608,9 +633,11 @@ def compact_messages(
|
|
|
608
633
|
elif isinstance(content_item, dict):
|
|
609
634
|
filtered_content.append(MessageContent(**content_item))
|
|
610
635
|
else:
|
|
611
|
-
filtered_content.append(
|
|
636
|
+
filtered_content.append(
|
|
637
|
+
MessageContent(type=str(block_type or "text"), text=str(content_item))
|
|
638
|
+
)
|
|
612
639
|
|
|
613
|
-
if modified:
|
|
640
|
+
if modified and isinstance(message, UserMessage):
|
|
614
641
|
compacted_messages.append(
|
|
615
642
|
UserMessage(
|
|
616
643
|
message=message.message.model_copy(update={"content": filtered_content}),
|
|
@@ -625,16 +652,26 @@ def compact_messages(
|
|
|
625
652
|
_processed_tool_use_ids.add(id_to_remove)
|
|
626
653
|
|
|
627
654
|
tokens_after = estimate_conversation_tokens(compacted_messages, protocol=protocol)
|
|
655
|
+
tokens_saved = max(0, tokens_before - tokens_after)
|
|
628
656
|
|
|
629
657
|
if ids_to_remove:
|
|
630
658
|
_is_compacting = True
|
|
631
659
|
_run_cleanup_callbacks()
|
|
660
|
+
logger.debug(
|
|
661
|
+
"[message_compaction] Compacted conversation",
|
|
662
|
+
extra={
|
|
663
|
+
"tokens_before": tokens_before,
|
|
664
|
+
"tokens_after": tokens_after,
|
|
665
|
+
"tokens_saved": tokens_saved,
|
|
666
|
+
"cleared_tool_ids": list(ids_to_remove),
|
|
667
|
+
},
|
|
668
|
+
)
|
|
632
669
|
|
|
633
670
|
return CompactionResult(
|
|
634
671
|
messages=compacted_messages,
|
|
635
672
|
tokens_before=tokens_before,
|
|
636
673
|
tokens_after=tokens_after,
|
|
637
|
-
tokens_saved=
|
|
674
|
+
tokens_saved=tokens_saved,
|
|
638
675
|
cleared_tool_ids=ids_to_remove,
|
|
639
676
|
was_compacted=bool(ids_to_remove),
|
|
640
677
|
)
|
ripperdoc/utils/messages.py
CHANGED
|
@@ -4,6 +4,7 @@ This module provides utilities for creating and normalizing messages
|
|
|
4
4
|
for communication with AI models.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import json
|
|
7
8
|
from typing import Any, Dict, List, Optional, Union
|
|
8
9
|
from pydantic import BaseModel, ConfigDict
|
|
9
10
|
from uuid import uuid4
|
|
@@ -75,6 +76,7 @@ def _content_block_to_openai(block: MessageContent) -> Dict[str, Any]:
|
|
|
75
76
|
try:
|
|
76
77
|
args_str = json.dumps(args)
|
|
77
78
|
except Exception:
|
|
79
|
+
logger.exception("[_content_block_to_openai] Failed to serialize tool arguments")
|
|
78
80
|
args_str = "{}"
|
|
79
81
|
tool_call_id = (
|
|
80
82
|
getattr(block, "id", None) or getattr(block, "tool_use_id", "") or str(uuid4())
|
|
@@ -187,7 +189,7 @@ def create_user_message(
|
|
|
187
189
|
tool_use_result = tool_use_result.model_dump()
|
|
188
190
|
except Exception:
|
|
189
191
|
# Fallback: keep as-is if conversion fails
|
|
190
|
-
|
|
192
|
+
logger.exception("[create_user_message] Failed to normalize tool_use_result")
|
|
191
193
|
|
|
192
194
|
message = Message(role=MessageRole.USER, content=message_content)
|
|
193
195
|
|
|
@@ -237,6 +239,7 @@ def create_progress_message(
|
|
|
237
239
|
def normalize_messages_for_api(
|
|
238
240
|
messages: List[Union[UserMessage, AssistantMessage, ProgressMessage]],
|
|
239
241
|
protocol: str = "anthropic",
|
|
242
|
+
tool_mode: str = "native",
|
|
240
243
|
) -> List[Dict[str, Any]]:
|
|
241
244
|
"""Normalize messages for API submission.
|
|
242
245
|
|
|
@@ -261,6 +264,62 @@ def normalize_messages_for_api(
|
|
|
261
264
|
return msg.get("content")
|
|
262
265
|
return None
|
|
263
266
|
|
|
267
|
+
def _block_type(block: Any) -> Optional[str]:
|
|
268
|
+
if hasattr(block, "type"):
|
|
269
|
+
return getattr(block, "type", None)
|
|
270
|
+
if isinstance(block, dict):
|
|
271
|
+
return block.get("type")
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
def _block_attr(block: Any, attr: str, default: Any = None) -> Any:
|
|
275
|
+
if hasattr(block, attr):
|
|
276
|
+
return getattr(block, attr, default)
|
|
277
|
+
if isinstance(block, dict):
|
|
278
|
+
return block.get(attr, default)
|
|
279
|
+
return default
|
|
280
|
+
|
|
281
|
+
def _flatten_blocks_to_text(blocks: List[Any]) -> str:
|
|
282
|
+
parts: List[str] = []
|
|
283
|
+
for blk in blocks:
|
|
284
|
+
btype = _block_type(blk)
|
|
285
|
+
if btype == "text":
|
|
286
|
+
text = _block_attr(blk, "text") or _block_attr(blk, "content") or ""
|
|
287
|
+
if text:
|
|
288
|
+
parts.append(str(text))
|
|
289
|
+
elif btype == "tool_result":
|
|
290
|
+
text = _block_attr(blk, "text") or _block_attr(blk, "content") or ""
|
|
291
|
+
tool_id = _block_attr(blk, "tool_use_id") or _block_attr(blk, "id")
|
|
292
|
+
prefix = "Tool error" if _block_attr(blk, "is_error") else "Tool result"
|
|
293
|
+
label = f"{prefix}{f' ({tool_id})' if tool_id else ''}"
|
|
294
|
+
parts.append(f"{label}: {text}" if text else label)
|
|
295
|
+
elif btype == "tool_use":
|
|
296
|
+
name = _block_attr(blk, "name") or ""
|
|
297
|
+
input_data = _block_attr(blk, "input")
|
|
298
|
+
input_preview = ""
|
|
299
|
+
if input_data not in (None, {}):
|
|
300
|
+
try:
|
|
301
|
+
input_preview = json.dumps(input_data)
|
|
302
|
+
except Exception:
|
|
303
|
+
input_preview = str(input_data)
|
|
304
|
+
tool_id = _block_attr(blk, "tool_use_id") or _block_attr(blk, "id")
|
|
305
|
+
desc = "Tool call"
|
|
306
|
+
if name:
|
|
307
|
+
desc += f" {name}"
|
|
308
|
+
if tool_id:
|
|
309
|
+
desc += f" ({tool_id})"
|
|
310
|
+
if input_preview:
|
|
311
|
+
desc += f": {input_preview}"
|
|
312
|
+
parts.append(desc)
|
|
313
|
+
else:
|
|
314
|
+
text = _block_attr(blk, "text") or _block_attr(blk, "content") or ""
|
|
315
|
+
if text:
|
|
316
|
+
parts.append(str(text))
|
|
317
|
+
return "\n".join(p for p in parts if p)
|
|
318
|
+
|
|
319
|
+
effective_tool_mode = (tool_mode or "native").lower()
|
|
320
|
+
if effective_tool_mode not in {"native", "text"}:
|
|
321
|
+
effective_tool_mode = "native"
|
|
322
|
+
|
|
264
323
|
normalized: List[Dict[str, Any]] = []
|
|
265
324
|
tool_results_seen = 0
|
|
266
325
|
tool_uses_seen = 0
|
|
@@ -374,9 +433,9 @@ def normalize_messages_for_api(
|
|
|
374
433
|
)
|
|
375
434
|
|
|
376
435
|
logger.debug(
|
|
377
|
-
f"[normalize_messages_for_api] protocol={protocol}
|
|
378
|
-
f"
|
|
379
|
-
f"tool_uses_seen={tool_uses_seen} "
|
|
436
|
+
f"[normalize_messages_for_api] protocol={protocol} tool_mode={effective_tool_mode} "
|
|
437
|
+
f"input_msgs={len(messages)} normalized={len(normalized)} "
|
|
438
|
+
f"tool_results_seen={tool_results_seen} tool_uses_seen={tool_uses_seen} "
|
|
380
439
|
f"tool_result_positions={len(tool_result_positions)} "
|
|
381
440
|
f"skipped_tool_uses_no_result={skipped_tool_uses_no_result} "
|
|
382
441
|
f"skipped_tool_uses_no_id={skipped_tool_uses_no_id}"
|
ripperdoc/utils/output_utils.py
CHANGED
|
@@ -131,15 +131,42 @@ def truncate_output(text: str, max_chars: int = MAX_OUTPUT_CHARS) -> dict[str, A
|
|
|
131
131
|
"is_image": False,
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
134
|
+
marker_template = "\n\n... [Output truncated: {omitted} characters omitted] ...\n\n"
|
|
135
|
+
short_marker = "... [truncated] ..."
|
|
136
|
+
|
|
137
|
+
def _choose_marker(omitted: int, budget: int) -> str:
|
|
138
|
+
"""Pick the most informative marker that fits within the budget."""
|
|
139
|
+
full_marker = marker_template.format(omitted=omitted)
|
|
140
|
+
if len(full_marker) <= budget:
|
|
141
|
+
return full_marker
|
|
142
|
+
if len(short_marker) <= budget:
|
|
143
|
+
return short_marker
|
|
144
|
+
# Last resort: squeeze an ellipsis into the budget (may be empty for tiny budgets)
|
|
145
|
+
return "..."[: max(budget, 0)]
|
|
146
|
+
|
|
147
|
+
# Iteratively balance how much of the start/end to keep while ensuring we never exceed max_chars.
|
|
148
|
+
marker = _choose_marker(original_length - max_chars, max_chars)
|
|
149
|
+
keep_start = keep_end = 0
|
|
150
|
+
for _ in range(2):
|
|
151
|
+
available = max(0, max_chars - len(marker))
|
|
152
|
+
keep_start = min(TRUNCATE_KEEP_START, available // 2)
|
|
153
|
+
keep_end = min(TRUNCATE_KEEP_END, available - keep_start)
|
|
154
|
+
marker = _choose_marker(
|
|
155
|
+
max(0, original_length - (keep_start + keep_end)), max_chars
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
available = max(0, max_chars - len(marker))
|
|
159
|
+
# Ensure kept sections fit the final budget; trim end first, then start if needed.
|
|
160
|
+
if keep_start + keep_end > available:
|
|
161
|
+
overflow = keep_start + keep_end - available
|
|
162
|
+
trim_end = min(overflow, keep_end)
|
|
163
|
+
keep_end -= trim_end
|
|
164
|
+
overflow -= trim_end
|
|
165
|
+
keep_start = max(0, keep_start - overflow)
|
|
166
|
+
|
|
167
|
+
truncated = text[:keep_start] + marker + (text[-keep_end:] if keep_end else "")
|
|
168
|
+
if len(truncated) > max_chars:
|
|
169
|
+
truncated = truncated[:max_chars]
|
|
143
170
|
|
|
144
171
|
return {
|
|
145
172
|
"truncated_content": truncated,
|
|
@@ -10,6 +10,9 @@ from typing import Iterable, List, Set
|
|
|
10
10
|
|
|
11
11
|
from ripperdoc.utils.safe_get_cwd import safe_get_cwd
|
|
12
12
|
from ripperdoc.utils.shell_token_utils import parse_and_clean_shell_tokens
|
|
13
|
+
from ripperdoc.utils.log import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger()
|
|
13
16
|
|
|
14
17
|
_GLOB_PATTERN = re.compile(r"[*?\[\]{}]")
|
|
15
18
|
_MAX_VISIBLE_ITEMS = 5
|
|
@@ -46,6 +49,9 @@ def _resolve_path(raw_path: str, cwd: str) -> Path:
|
|
|
46
49
|
try:
|
|
47
50
|
return candidate.resolve()
|
|
48
51
|
except Exception:
|
|
52
|
+
logger.exception(
|
|
53
|
+
"[path_validation] Failed to resolve path", extra={"raw_path": raw_path, "cwd": cwd}
|
|
54
|
+
)
|
|
49
55
|
return candidate
|
|
50
56
|
|
|
51
57
|
|
ripperdoc/utils/safe_get_cwd.py
CHANGED
|
@@ -4,6 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from ripperdoc.utils.log import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger()
|
|
7
10
|
|
|
8
11
|
_ORIGINAL_CWD = Path(os.getcwd()).resolve()
|
|
9
12
|
|
|
@@ -18,6 +21,7 @@ def safe_get_cwd() -> str:
|
|
|
18
21
|
try:
|
|
19
22
|
return str(Path(os.getcwd()).resolve())
|
|
20
23
|
except Exception:
|
|
24
|
+
logger.exception("[safe_get_cwd] Failed to resolve cwd")
|
|
21
25
|
return get_original_cwd()
|
|
22
26
|
|
|
23
27
|
|
|
@@ -102,10 +102,16 @@ class SessionHistory:
|
|
|
102
102
|
if isinstance(msg_uuid, str):
|
|
103
103
|
self._seen_ids.add(msg_uuid)
|
|
104
104
|
except Exception as exc:
|
|
105
|
-
logger.debug(
|
|
105
|
+
logger.debug(
|
|
106
|
+
f"Failed to parse session history line: {exc}",
|
|
107
|
+
exc_info=True,
|
|
108
|
+
)
|
|
106
109
|
continue
|
|
107
|
-
except Exception
|
|
108
|
-
logger.
|
|
110
|
+
except Exception:
|
|
111
|
+
logger.exception(
|
|
112
|
+
"Failed to load seen IDs from session",
|
|
113
|
+
extra={"session_id": self.session_id, "path": str(self.path)},
|
|
114
|
+
)
|
|
109
115
|
return
|
|
110
116
|
|
|
111
117
|
def append(self, message: ConversationMessage) -> None:
|
|
@@ -128,9 +134,12 @@ class SessionHistory:
|
|
|
128
134
|
fh.write("\n")
|
|
129
135
|
if isinstance(msg_uuid, str):
|
|
130
136
|
self._seen_ids.add(msg_uuid)
|
|
131
|
-
except Exception
|
|
137
|
+
except Exception:
|
|
132
138
|
# Avoid crashing the UI if logging fails
|
|
133
|
-
logger.
|
|
139
|
+
logger.exception(
|
|
140
|
+
"Failed to append message to session log",
|
|
141
|
+
extra={"session_id": self.session_id, "path": str(self.path)},
|
|
142
|
+
)
|
|
134
143
|
return
|
|
135
144
|
|
|
136
145
|
|
|
@@ -146,7 +155,10 @@ def list_session_summaries(project_path: Path) -> List[SessionSummary]:
|
|
|
146
155
|
with jsonl_path.open("r", encoding="utf-8") as fh:
|
|
147
156
|
messages = [json.loads(line) for line in fh if line.strip()]
|
|
148
157
|
except Exception as exc:
|
|
149
|
-
logger.
|
|
158
|
+
logger.exception(
|
|
159
|
+
"Failed to load session summary",
|
|
160
|
+
extra={"path": str(jsonl_path), "error": str(exc)},
|
|
161
|
+
)
|
|
150
162
|
continue
|
|
151
163
|
|
|
152
164
|
payloads = [entry.get("payload") or {} for entry in messages]
|
|
@@ -206,10 +218,16 @@ def load_session_messages(project_path: Path, session_id: str) -> List[Conversat
|
|
|
206
218
|
if msg is not None and getattr(msg, "type", None) != "progress":
|
|
207
219
|
messages.append(msg)
|
|
208
220
|
except Exception as exc:
|
|
209
|
-
logger.debug(
|
|
221
|
+
logger.debug(
|
|
222
|
+
f"Failed to deserialize message in session {session_id}: {exc}",
|
|
223
|
+
exc_info=True,
|
|
224
|
+
)
|
|
210
225
|
continue
|
|
211
|
-
except Exception
|
|
212
|
-
logger.
|
|
226
|
+
except Exception:
|
|
227
|
+
logger.exception(
|
|
228
|
+
"Failed to load session messages",
|
|
229
|
+
extra={"session_id": session_id, "path": str(path)},
|
|
230
|
+
)
|
|
213
231
|
return []
|
|
214
232
|
|
|
215
233
|
return messages
|
ripperdoc/utils/todo.py
CHANGED
|
@@ -83,8 +83,8 @@ def load_todos(project_root: Optional[Path] = None) -> List[TodoItem]:
|
|
|
83
83
|
|
|
84
84
|
try:
|
|
85
85
|
raw = json.loads(path.read_text())
|
|
86
|
-
except Exception
|
|
87
|
-
logger.
|
|
86
|
+
except Exception:
|
|
87
|
+
logger.exception("Failed to load todos from disk", extra={"path": str(path)})
|
|
88
88
|
return []
|
|
89
89
|
|
|
90
90
|
todos: List[TodoItem] = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ripperdoc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: AI-powered terminal assistant for coding tasks
|
|
5
5
|
Author: Ripperdoc Team
|
|
6
6
|
License: Apache-2.0
|
|
@@ -24,6 +24,7 @@ Requires-Dist: aiofiles>=23.0.0
|
|
|
24
24
|
Requires-Dist: prompt-toolkit>=3.0.0
|
|
25
25
|
Requires-Dist: PyYAML>=6.0.0
|
|
26
26
|
Requires-Dist: mcp[cli]>=1.22.0
|
|
27
|
+
Requires-Dist: json_repair>=0.54.2
|
|
27
28
|
Provides-Extra: dev
|
|
28
29
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
29
30
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
@@ -37,10 +38,12 @@ Dynamic: license-file
|
|
|
37
38
|
Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an interactive interface for AI-assisted development, file management, and command execution.
|
|
38
39
|
|
|
39
40
|
[中文文档](README_CN.md) | [Contributing](CONTRIBUTING.md) | [Documentation](docs/)
|
|
41
|
+
|
|
40
42
|
## Features
|
|
41
43
|
|
|
42
44
|
- **AI-Powered Assistance** - Uses AI models to understand and respond to coding requests
|
|
43
45
|
- **Multi-Model Support** - Support for Anthropic Claude and OpenAI models
|
|
46
|
+
- **Rich UI** - Beautiful terminal interface with syntax highlighting
|
|
44
47
|
- **Code Editing** - Directly edit files with intelligent suggestions
|
|
45
48
|
- **Codebase Understanding** - Analyzes project structure and code relationships
|
|
46
49
|
- **Command Execution** - Run shell commands with real-time feedback
|
|
@@ -52,7 +55,6 @@ Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an int
|
|
|
52
55
|
- **Permission System** - Safe mode with permission prompts for operations
|
|
53
56
|
- **Multi-Edit Support** - Batch edit operations on files
|
|
54
57
|
- **MCP Server Support** - Integration with Model Context Protocol servers
|
|
55
|
-
- **Subagent System** - Delegate tasks to specialized agents
|
|
56
58
|
- **Session Management** - Persistent session history and usage tracking
|
|
57
59
|
- **Jupyter Notebook Support** - Edit .ipynb files directly
|
|
58
60
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
ripperdoc/__init__.py,sha256=VVORpI3UmHW0atP8i4ThIdElrO8xDwVHSHveq6sUOco,66
|
|
2
|
+
ripperdoc/__main__.py,sha256=7oIFEXI2irIoZ_dhcMd3hCs4Dj8tmMBbwiVACAoeE-k,506
|
|
3
|
+
ripperdoc/cli/__init__.py,sha256=03wf6gXBcEgXJrDJS-W_5BEG_DdJ_ep7CxQFPML-73g,35
|
|
4
|
+
ripperdoc/cli/cli.py,sha256=lm3d4JXaHoLxZ20HmkE38GfNChDyJMGR-5pSRPiZzEA,12843
|
|
5
|
+
ripperdoc/cli/commands/__init__.py,sha256=J13i7g-69PVLhO5IJH5OvVK0FJLIFj0b84mm33JvpcE,2329
|
|
6
|
+
ripperdoc/cli/commands/agents_cmd.py,sha256=nYB_JZI8YIuBflDFXkWZsNBGAMneBFf3IFM2gZLOKfI,10118
|
|
7
|
+
ripperdoc/cli/commands/base.py,sha256=4KUjxCM04MwbSMUKVNEBph_jeAKPI8b5MHsUFoz7l5g,386
|
|
8
|
+
ripperdoc/cli/commands/clear_cmd.py,sha256=zSYT0Nn_htZzLWTTQ4E5KWHfRg0Q5CYvRO4e--7thBY,345
|
|
9
|
+
ripperdoc/cli/commands/compact_cmd.py,sha256=C_qdPTPdg1cOHdmODkaYoRusosgiqRK6c6KID_Gwq0k,330
|
|
10
|
+
ripperdoc/cli/commands/config_cmd.py,sha256=ebIQk7zUFv353liWfbBSSfPiOaaCR7rQsd_eTw7nsvY,884
|
|
11
|
+
ripperdoc/cli/commands/context_cmd.py,sha256=d0KiJyjbuDNXYlfSzVTFmxmkLXJZe3pUDg_Tt67OWqs,4359
|
|
12
|
+
ripperdoc/cli/commands/cost_cmd.py,sha256=AoGRDDu48qsMGuS5zgScg_YdNqkwoj33D2PTh-1uZ34,2637
|
|
13
|
+
ripperdoc/cli/commands/doctor_cmd.py,sha256=SW2f9Z6dQzeBTATqdByWRc1UE_w-132KsoKj8d9VPBY,6577
|
|
14
|
+
ripperdoc/cli/commands/exit_cmd.py,sha256=B0CNKQos2eRC4LSjizLdKsFYzFfwRkrUur6Afu3Fh9M,334
|
|
15
|
+
ripperdoc/cli/commands/help_cmd.py,sha256=iz1vR-rmWsvvfzdebLiIWEWrcMZo5_Eb55_wLr4Ufno,508
|
|
16
|
+
ripperdoc/cli/commands/mcp_cmd.py,sha256=S0iQxclxqgbIxbKcC9oFrckLalzk-1eyAYwkfEZQsGU,2307
|
|
17
|
+
ripperdoc/cli/commands/memory_cmd.py,sha256=W6-jteI_aajw7RaQzwGPWiGOBwfsZcWXKh2HLKsq9B8,6558
|
|
18
|
+
ripperdoc/cli/commands/models_cmd.py,sha256=b6t8UUUTyu4Se_NhYbMDPvkDnWTephFEtOiFjY2oVrk,13080
|
|
19
|
+
ripperdoc/cli/commands/resume_cmd.py,sha256=F99haT29dUHYlgrIYKk2cadSOTrwkLOEs_D2RotNZXI,2977
|
|
20
|
+
ripperdoc/cli/commands/status_cmd.py,sha256=rAhHksegqdLGy551Hh22oVwR6ZSfkDvAPOjNiNQJ9_s,5494
|
|
21
|
+
ripperdoc/cli/commands/tasks_cmd.py,sha256=lgVzN6_KFQGltb7bgn4Wwmkuq91H_mgzCrga67hgjT4,8292
|
|
22
|
+
ripperdoc/cli/commands/todos_cmd.py,sha256=7Q0B1NVqGtB3R29ndbn4m0VQQm-YQ7d4Wlk7vJ7dLQI,1848
|
|
23
|
+
ripperdoc/cli/commands/tools_cmd.py,sha256=3cMi0vN4mAUhpKqJtRgNvZfcKzRPaMs_pkYYXlyvSSU,384
|
|
24
|
+
ripperdoc/cli/ui/__init__.py,sha256=TxSzTYdITlrYmYVfins_w_jzPqqWRpqky5u1ikwvmtM,43
|
|
25
|
+
ripperdoc/cli/ui/context_display.py,sha256=3ezdtHVwltkPQ5etYwfqUh-fjnpPu8B3P81UzrdHxZs,10020
|
|
26
|
+
ripperdoc/cli/ui/helpers.py,sha256=TJCipP0neh-96ETQfGhusCJ4aWt5gLw1HZbI-3bWDpw,739
|
|
27
|
+
ripperdoc/cli/ui/rich_ui.py,sha256=sG1mXHembvDyu_AaokKsoZ23MRnG-GIeVtwBH2RSxpM,49829
|
|
28
|
+
ripperdoc/cli/ui/spinner.py,sha256=XsPRwJ-70InLX9Qw50CEgSHn5oKA5PFIue8Un4edhUk,1449
|
|
29
|
+
ripperdoc/core/__init__.py,sha256=UemJCA-Y8df1466AX-YbRFj071zKajmqO1mi40YVW2g,40
|
|
30
|
+
ripperdoc/core/agents.py,sha256=-v2IkP3QbGtb5AwTT3WoWyWvlUCDieHFnG7vXNMQaZo,10320
|
|
31
|
+
ripperdoc/core/commands.py,sha256=NXCkljYbAP4dDoRy-_3semFNWxG4YAk9q82u8FTKH60,835
|
|
32
|
+
ripperdoc/core/config.py,sha256=L022fgD-yIUP3M-e3txZI1Qo3O1mOa9TP4a-zcXZU-I,15335
|
|
33
|
+
ripperdoc/core/default_tools.py,sha256=t8cLZBOVReF5hvmyhUTziUSnnDMyKDjS7YjuSd_yolw,2568
|
|
34
|
+
ripperdoc/core/permissions.py,sha256=qWaVIaps9Ht0M4jgDk9J0gDErptvf6jzEZ2t48lZ_1I,9187
|
|
35
|
+
ripperdoc/core/query.py,sha256=25qQj3h8G5ad9PVSmrQaRCZf701F9bJL0NdeHlJ6N-w,23299
|
|
36
|
+
ripperdoc/core/query_utils.py,sha256=dnBiYO0zvlsGDt09bb9ozItqLNGUDbI4JCH0pUDM8ns,21404
|
|
37
|
+
ripperdoc/core/system_prompt.py,sha256=QH7Wg8QqwzvAzjuBayr57w-BazqIQrHfuGvC2KqVkgI,24190
|
|
38
|
+
ripperdoc/core/tool.py,sha256=dIEBU4bvzD0oZMJ4fbvmTc3mMOWTjxfb12nRHRsalzg,6872
|
|
39
|
+
ripperdoc/sdk/__init__.py,sha256=aDSgI4lcCDs9cV3bxNmEEV3SuYx2aCd4VnUjs6H-R7E,213
|
|
40
|
+
ripperdoc/sdk/client.py,sha256=21f8viIh3BhSjXcI_MvgzfAcy7r289Thyhc_qgnQrB0,10556
|
|
41
|
+
ripperdoc/tools/__init__.py,sha256=RBFz0DDnztDXMqv_zRxFHVY-ez2HYcncx8zh_y-BX6w,42
|
|
42
|
+
ripperdoc/tools/background_shell.py,sha256=r0F67FoXGnT_QN-fQWQpsZCW-RoZmIaJGIuXX8Eu8nY,9352
|
|
43
|
+
ripperdoc/tools/bash_output_tool.py,sha256=ljIOzTOnkbQfe3jExlhpUlMiLT6HpeD-1QI-D1CwHh8,3379
|
|
44
|
+
ripperdoc/tools/bash_tool.py,sha256=XFvg66mU19OuH-h8wxOa_nu-ThtcwFCHFuO-rc_QXQ0,36698
|
|
45
|
+
ripperdoc/tools/file_edit_tool.py,sha256=YX6ijp8huEtAR9jJiJzLnDmrJ5QUW2beyTTlZsrAhbA,11394
|
|
46
|
+
ripperdoc/tools/file_read_tool.py,sha256=IZwRZACRMn8QNElN8T5quhrx1Wk7B1u4lglASR_WKKg,6109
|
|
47
|
+
ripperdoc/tools/file_write_tool.py,sha256=BQgjaCp2UTe3sgDukoJQRVAFVfZdK1WG2eHMD3NuupM,4811
|
|
48
|
+
ripperdoc/tools/glob_tool.py,sha256=2DtKo8WZgK9_X8WDTXcrfRPm1sCx89SOfnmwvInTsIM,5792
|
|
49
|
+
ripperdoc/tools/grep_tool.py,sha256=LfLpPmqD9CIPk6kFYDDmIcykj7uN0xoKtyodc2412FA,8340
|
|
50
|
+
ripperdoc/tools/kill_bash_tool.py,sha256=36F8w2Rm1IVQitwOAwS-D8NTnyQdWfKWIam44qlXErk,4625
|
|
51
|
+
ripperdoc/tools/ls_tool.py,sha256=-ijEXqNZXeibz5gDkCQV5emQKI0rAwDgYtQNum05iro,15262
|
|
52
|
+
ripperdoc/tools/mcp_tools.py,sha256=ejofb2UEwH0r-ZAEvvq9nWvCgRGTAqiOws_X2T0YBU0,31263
|
|
53
|
+
ripperdoc/tools/multi_edit_tool.py,sha256=cTQjTovOJOs1fTeavFz14akm0NzNt16dsnoIIuvRI64,15360
|
|
54
|
+
ripperdoc/tools/notebook_edit_tool.py,sha256=693wva_Y77Y43Z5OAORPHUs1AL3vzHV_SDYPVzyu37c,12150
|
|
55
|
+
ripperdoc/tools/task_tool.py,sha256=bKvJy0hMJcY4MEmWDXqYH0CKfgMKEtaCO9UF9xdngc0,11983
|
|
56
|
+
ripperdoc/tools/todo_tool.py,sha256=DgKiJAI3W5WflXqdB4R7bJRdvlg6zqEeZ6CZfH9Ohb8,19984
|
|
57
|
+
ripperdoc/tools/tool_search_tool.py,sha256=pj26xBYBThuPioVYNB65wypwPVbGwKURxpd4p918ScI,13693
|
|
58
|
+
ripperdoc/utils/__init__.py,sha256=gdso60znB2hsYZ_YZBKVcuOY3QVfoqD2wHQ4pvr5lSw,37
|
|
59
|
+
ripperdoc/utils/bash_constants.py,sha256=KNn8bzB6nVU5jid9jvjiH4FAu8pP3DZONJ-OknJypAQ,1641
|
|
60
|
+
ripperdoc/utils/bash_output_utils.py,sha256=3Cf5wKJzRbUesmCNy5HtXIBtv0Z2BxklTfFHJ9q1T3w,1210
|
|
61
|
+
ripperdoc/utils/exit_code_handlers.py,sha256=QtO1iDxVAb8Xp03D6_QixPoJC-RQlcp3ssIo_rm4two,7973
|
|
62
|
+
ripperdoc/utils/git_utils.py,sha256=o5ff99CQ5tqe8lgY0zd0f6I3Rb95LAvhv_yLKZCKrv0,9233
|
|
63
|
+
ripperdoc/utils/json_utils.py,sha256=ZA77cDDd0smsIpEtKJk0cfaI6ZD88DyXjDm9bCsEAGc,711
|
|
64
|
+
ripperdoc/utils/log.py,sha256=AT7EMl8Xh02aY3qRF2k1UpBhfAWbpXeNtuogcldBRPo,6099
|
|
65
|
+
ripperdoc/utils/mcp.py,sha256=aIGmJLErQ1xRSzTj9Hm3ns2gW8oTvfrFBIukyNXaURk,17037
|
|
66
|
+
ripperdoc/utils/memory.py,sha256=nBulG1KFTnmYBikcYruNV0qhNDi2gpI46dvB9URdi9I,7894
|
|
67
|
+
ripperdoc/utils/message_compaction.py,sha256=pZY9wLlY46ZQq9zx5rOTzKa6sdoxokxr4w2IQsNI6wg,24857
|
|
68
|
+
ripperdoc/utils/messages.py,sha256=yEvaO8O4FW2dy12qKWfY3u_jimkoS-nSUnfY5nC9gzw,17389
|
|
69
|
+
ripperdoc/utils/output_utils.py,sha256=gAOfw_vZYjhv8qXb5U-r-rRTcidqDQShYWUgwiwCkGM,7470
|
|
70
|
+
ripperdoc/utils/path_utils.py,sha256=C45Q3OeXnj-0FVEtvf_tdG5922XB6HthUzlUCvfc17Y,1626
|
|
71
|
+
ripperdoc/utils/safe_get_cwd.py,sha256=hZXQ1oJBCXuk1QBmmfqgDsr1icVOAfGSxABp032GauI,716
|
|
72
|
+
ripperdoc/utils/sandbox_utils.py,sha256=G91P8dw2VFcCiCpjXZ4LvzbAPiO8REqMhw39eI5Z4dU,1123
|
|
73
|
+
ripperdoc/utils/session_history.py,sha256=Z-stx88yaWzJ5iqOLCeS_z8gg1G4w6j4OMoff9r7JvA,7965
|
|
74
|
+
ripperdoc/utils/session_usage.py,sha256=iz4B9MRhPbwemo9-beeZ8axcpGEe0kEc-Jghlexd_xU,2961
|
|
75
|
+
ripperdoc/utils/shell_token_utils.py,sha256=SduoSU-RERJdM_7gBn0urr5UXtl4XOpPgydBd2fwzWg,2500
|
|
76
|
+
ripperdoc/utils/todo.py,sha256=ejWd42AAVT15GJvCqb21GERSIbsdY-bwZDNnst4xebQ,6903
|
|
77
|
+
ripperdoc/utils/permissions/__init__.py,sha256=-1aKvRC05kuvLacbeu7w1W5ANamOTAho4Y0lY2vz0W0,522
|
|
78
|
+
ripperdoc/utils/permissions/path_validation_utils.py,sha256=jWcvBWUOgiEOncn_b2Fz7FBJL3XiPlyPQhNT4ZJ7p10,5681
|
|
79
|
+
ripperdoc/utils/permissions/shell_command_validation.py,sha256=BkK-wEwAuJPoC4XIx2Zj6MF3gXcWReozIwigXgib0z0,2446
|
|
80
|
+
ripperdoc/utils/permissions/tool_permission_utils.py,sha256=6Fdu9-dMKhLsUExjEjoS0EUeRpEVN5UkqyseIC05YmM,9207
|
|
81
|
+
ripperdoc-0.2.2.dist-info/licenses/LICENSE,sha256=bRv9UhBor6GhnQDj12RciDcRfu0R7sB-lqCy1sWF75c,9242
|
|
82
|
+
ripperdoc-0.2.2.dist-info/METADATA,sha256=K73CYp36dc4bblx3lxqPaJkZK8BxxlATA1v0ARqACq8,5362
|
|
83
|
+
ripperdoc-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
84
|
+
ripperdoc-0.2.2.dist-info/entry_points.txt,sha256=79aohFxFPJmrQ3-Mhain04vb3EWpuc0EyzvDDUnwAu4,81
|
|
85
|
+
ripperdoc-0.2.2.dist-info/top_level.txt,sha256=u8LbdTr1a-laHgCO0Utl_R3QGFUhLxWelCDnP2ZgpCU,10
|
|
86
|
+
ripperdoc-0.2.2.dist-info/RECORD,,
|