ripperdoc 0.2.6__py3-none-any.whl → 0.2.8__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 +5 -0
- ripperdoc/cli/commands/__init__.py +71 -6
- ripperdoc/cli/commands/clear_cmd.py +1 -0
- ripperdoc/cli/commands/exit_cmd.py +1 -1
- ripperdoc/cli/commands/help_cmd.py +11 -1
- ripperdoc/cli/commands/hooks_cmd.py +636 -0
- ripperdoc/cli/commands/permissions_cmd.py +36 -34
- ripperdoc/cli/commands/resume_cmd.py +71 -37
- ripperdoc/cli/ui/file_mention_completer.py +276 -0
- ripperdoc/cli/ui/helpers.py +100 -3
- ripperdoc/cli/ui/interrupt_handler.py +175 -0
- ripperdoc/cli/ui/message_display.py +249 -0
- ripperdoc/cli/ui/panels.py +63 -0
- ripperdoc/cli/ui/rich_ui.py +233 -648
- ripperdoc/cli/ui/tool_renderers.py +2 -2
- ripperdoc/core/agents.py +4 -4
- ripperdoc/core/custom_commands.py +411 -0
- ripperdoc/core/hooks/__init__.py +99 -0
- ripperdoc/core/hooks/config.py +303 -0
- ripperdoc/core/hooks/events.py +540 -0
- ripperdoc/core/hooks/executor.py +498 -0
- ripperdoc/core/hooks/integration.py +353 -0
- ripperdoc/core/hooks/manager.py +720 -0
- ripperdoc/core/providers/anthropic.py +476 -69
- ripperdoc/core/query.py +61 -4
- ripperdoc/core/query_utils.py +1 -1
- ripperdoc/core/tool.py +1 -1
- ripperdoc/tools/bash_tool.py +5 -5
- ripperdoc/tools/file_edit_tool.py +2 -2
- ripperdoc/tools/file_read_tool.py +2 -2
- ripperdoc/tools/multi_edit_tool.py +1 -1
- ripperdoc/utils/conversation_compaction.py +476 -0
- ripperdoc/utils/message_compaction.py +109 -154
- ripperdoc/utils/message_formatting.py +216 -0
- ripperdoc/utils/messages.py +31 -9
- ripperdoc/utils/path_ignore.py +3 -4
- ripperdoc/utils/session_history.py +19 -7
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/METADATA +24 -3
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/RECORD +44 -30
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Message content formatting utilities.
|
|
2
|
+
|
|
3
|
+
This module provides functions for converting message content to plain text,
|
|
4
|
+
with support for detailed tool information extraction. Used primarily for
|
|
5
|
+
conversation summarization and compaction.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, List, Union
|
|
9
|
+
|
|
10
|
+
from ripperdoc.utils.messages import UserMessage, AssistantMessage, ProgressMessage
|
|
11
|
+
|
|
12
|
+
ConversationMessage = Union[UserMessage, AssistantMessage, ProgressMessage]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def stringify_message_content(
|
|
16
|
+
content: Any, *, include_tool_details: bool = False
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Convert message content to plain string.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
content: The message content to stringify.
|
|
22
|
+
include_tool_details: If True, include tool input/output details
|
|
23
|
+
instead of just placeholders. Useful for summarization.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Plain text representation of the message content.
|
|
27
|
+
"""
|
|
28
|
+
if content is None:
|
|
29
|
+
return ""
|
|
30
|
+
if isinstance(content, str):
|
|
31
|
+
return content
|
|
32
|
+
if isinstance(content, list):
|
|
33
|
+
parts: List[str] = []
|
|
34
|
+
for block in content:
|
|
35
|
+
block_type = getattr(block, "type", None)
|
|
36
|
+
if block_type == "text":
|
|
37
|
+
text = getattr(block, "text", None)
|
|
38
|
+
if text:
|
|
39
|
+
parts.append(str(text))
|
|
40
|
+
elif block_type == "tool_use":
|
|
41
|
+
name = getattr(block, "name", "tool")
|
|
42
|
+
if include_tool_details:
|
|
43
|
+
tool_input = getattr(block, "input", None)
|
|
44
|
+
parts.append(format_tool_use_detail(name, tool_input))
|
|
45
|
+
else:
|
|
46
|
+
parts.append(f"[Called {name}]")
|
|
47
|
+
elif block_type == "tool_result":
|
|
48
|
+
if include_tool_details:
|
|
49
|
+
result_text = getattr(block, "text", None) or ""
|
|
50
|
+
is_error = getattr(block, "is_error", False)
|
|
51
|
+
parts.append(format_tool_result_detail(result_text, is_error))
|
|
52
|
+
else:
|
|
53
|
+
parts.append("[Tool result]")
|
|
54
|
+
return "\n".join(parts)
|
|
55
|
+
return str(content)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def format_tool_use_detail(name: str, tool_input: Any) -> str:
|
|
59
|
+
"""Format tool_use block with input details for summarization.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
name: The tool name.
|
|
63
|
+
tool_input: The tool input dictionary.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Formatted string like "[Called Bash(command=ls -la)]"
|
|
67
|
+
"""
|
|
68
|
+
if not tool_input:
|
|
69
|
+
return f"[Called {name}]"
|
|
70
|
+
|
|
71
|
+
summary_parts: List[str] = []
|
|
72
|
+
if isinstance(tool_input, dict):
|
|
73
|
+
# Common patterns for different tools
|
|
74
|
+
if name == "Bash":
|
|
75
|
+
cmd = tool_input.get("command", "")
|
|
76
|
+
if cmd:
|
|
77
|
+
cmd_preview = cmd[:200] + "..." if len(cmd) > 200 else cmd
|
|
78
|
+
summary_parts.append(f"command={cmd_preview}")
|
|
79
|
+
elif name in ("Read", "Write", "Edit", "MultiEdit"):
|
|
80
|
+
path = tool_input.get("file_path", "")
|
|
81
|
+
if path:
|
|
82
|
+
summary_parts.append(f"file={path}")
|
|
83
|
+
elif name in ("Glob", "Grep"):
|
|
84
|
+
pattern = tool_input.get("pattern", "")
|
|
85
|
+
if pattern:
|
|
86
|
+
summary_parts.append(f"pattern={pattern}")
|
|
87
|
+
elif name == "Task":
|
|
88
|
+
desc = tool_input.get("description", "")
|
|
89
|
+
subagent = tool_input.get("subagent_type", "")
|
|
90
|
+
if subagent:
|
|
91
|
+
summary_parts.append(f"subagent={subagent}")
|
|
92
|
+
if desc:
|
|
93
|
+
summary_parts.append(f"desc={desc}")
|
|
94
|
+
else:
|
|
95
|
+
# Generic: show first few key-value pairs
|
|
96
|
+
for key, value in list(tool_input.items())[:3]:
|
|
97
|
+
val_str = str(value)
|
|
98
|
+
if len(val_str) > 50:
|
|
99
|
+
val_str = val_str[:47] + "..."
|
|
100
|
+
summary_parts.append(f"{key}={val_str}")
|
|
101
|
+
|
|
102
|
+
if summary_parts:
|
|
103
|
+
return f"[Called {name}({', '.join(summary_parts)})]"
|
|
104
|
+
return f"[Called {name}]"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def format_tool_result_detail(result_text: str, is_error: bool = False) -> str:
|
|
108
|
+
"""Format tool_result block with output details for summarization.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
result_text: The tool result text.
|
|
112
|
+
is_error: Whether this is an error result.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Formatted string like "[Tool result]: file contents..."
|
|
116
|
+
"""
|
|
117
|
+
prefix = "[Tool error]" if is_error else "[Tool result]"
|
|
118
|
+
if not result_text:
|
|
119
|
+
return prefix
|
|
120
|
+
|
|
121
|
+
# Truncate very long results but keep enough for context
|
|
122
|
+
max_len = 500
|
|
123
|
+
if len(result_text) > max_len:
|
|
124
|
+
result_preview = result_text[:max_len] + f"... (truncated, {len(result_text)} chars total)"
|
|
125
|
+
else:
|
|
126
|
+
result_preview = result_text
|
|
127
|
+
|
|
128
|
+
return f"{prefix}: {result_preview}"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def format_reasoning_preview(reasoning: Any) -> str:
|
|
132
|
+
"""Return a short preview of reasoning/thinking content.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
reasoning: The reasoning content (string, list, or other).
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
A short preview string (max ~80 chars with ellipsis).
|
|
139
|
+
"""
|
|
140
|
+
if reasoning is None:
|
|
141
|
+
return ""
|
|
142
|
+
if isinstance(reasoning, str):
|
|
143
|
+
text = reasoning
|
|
144
|
+
elif isinstance(reasoning, list):
|
|
145
|
+
parts = []
|
|
146
|
+
for block in reasoning:
|
|
147
|
+
if isinstance(block, dict):
|
|
148
|
+
parts.append(block.get("thinking") or block.get("summary") or "")
|
|
149
|
+
elif hasattr(block, "thinking"):
|
|
150
|
+
parts.append(getattr(block, "thinking", "") or "")
|
|
151
|
+
text = "\n".join(p for p in parts if p)
|
|
152
|
+
else:
|
|
153
|
+
text = str(reasoning)
|
|
154
|
+
lines = text.strip().splitlines()
|
|
155
|
+
if not lines:
|
|
156
|
+
return ""
|
|
157
|
+
preview = lines[0][:80]
|
|
158
|
+
if len(lines) > 1 or len(lines[0]) > 80:
|
|
159
|
+
preview += "..."
|
|
160
|
+
return preview
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def render_transcript(
|
|
164
|
+
messages: List[ConversationMessage], *, include_tool_details: bool = True
|
|
165
|
+
) -> str:
|
|
166
|
+
"""Render conversation messages into a plain-text transcript.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
messages: List of conversation messages to render.
|
|
170
|
+
include_tool_details: If True (default), include tool input/output
|
|
171
|
+
details for better summarization context.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Plain text transcript of the conversation.
|
|
175
|
+
"""
|
|
176
|
+
lines: List[str] = []
|
|
177
|
+
for msg in messages:
|
|
178
|
+
msg_type = getattr(msg, "type", "")
|
|
179
|
+
if msg_type == "progress":
|
|
180
|
+
continue
|
|
181
|
+
role = "User" if msg_type == "user" else "Assistant"
|
|
182
|
+
content = getattr(getattr(msg, "message", None), "content", None)
|
|
183
|
+
text = stringify_message_content(content, include_tool_details=include_tool_details)
|
|
184
|
+
if text.strip():
|
|
185
|
+
lines.append(f"{role}: {text}")
|
|
186
|
+
return "\n\n".join(lines)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def extract_assistant_text(assistant_message: Any) -> str:
|
|
190
|
+
"""Extract plain text from an assistant response object.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
assistant_message: An AssistantMessage or similar object.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Plain text content from the message.
|
|
197
|
+
"""
|
|
198
|
+
# AssistantMessage has .message.content structure
|
|
199
|
+
message = getattr(assistant_message, "message", None)
|
|
200
|
+
if message is not None:
|
|
201
|
+
content = getattr(message, "content", None)
|
|
202
|
+
else:
|
|
203
|
+
# Fallback: maybe it's a raw object with .content directly
|
|
204
|
+
content = getattr(assistant_message, "content", None)
|
|
205
|
+
|
|
206
|
+
if isinstance(content, str):
|
|
207
|
+
return content
|
|
208
|
+
if isinstance(content, list):
|
|
209
|
+
parts: List[str] = []
|
|
210
|
+
for block in content:
|
|
211
|
+
if getattr(block, "type", None) == "text":
|
|
212
|
+
text = getattr(block, "text", None)
|
|
213
|
+
if text:
|
|
214
|
+
parts.append(str(text))
|
|
215
|
+
return "\n".join(parts)
|
|
216
|
+
return ""
|
ripperdoc/utils/messages.py
CHANGED
|
@@ -381,20 +381,30 @@ def normalize_messages_for_api(
|
|
|
381
381
|
# Precompute tool_result positions so we can drop dangling tool_calls that
|
|
382
382
|
# lack a following tool response (which OpenAI rejects).
|
|
383
383
|
tool_result_positions: Dict[str, int] = {}
|
|
384
|
+
# Precompute tool_use positions so we can drop dangling tool_results that
|
|
385
|
+
# lack a preceding tool_call (which OpenAI also rejects).
|
|
386
|
+
tool_use_positions: Dict[str, int] = {}
|
|
384
387
|
skipped_tool_uses_no_result = 0
|
|
385
388
|
skipped_tool_uses_no_id = 0
|
|
389
|
+
skipped_tool_results_no_call = 0
|
|
386
390
|
if protocol == "openai":
|
|
387
391
|
for idx, msg in enumerate(messages):
|
|
388
|
-
|
|
389
|
-
continue
|
|
392
|
+
msg_type = _msg_type(msg)
|
|
390
393
|
content = _msg_content(msg)
|
|
391
394
|
if not isinstance(content, list):
|
|
392
395
|
continue
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
if msg_type == "user":
|
|
397
|
+
for block in content:
|
|
398
|
+
if getattr(block, "type", None) == "tool_result":
|
|
399
|
+
tool_id = getattr(block, "tool_use_id", None) or getattr(block, "id", None)
|
|
400
|
+
if tool_id and tool_id not in tool_result_positions:
|
|
401
|
+
tool_result_positions[tool_id] = idx
|
|
402
|
+
elif msg_type == "assistant":
|
|
403
|
+
for block in content:
|
|
404
|
+
if getattr(block, "type", None) == "tool_use":
|
|
405
|
+
tool_id = getattr(block, "id", None) or getattr(block, "tool_use_id", None)
|
|
406
|
+
if tool_id and tool_id not in tool_use_positions:
|
|
407
|
+
tool_use_positions[tool_id] = idx
|
|
398
408
|
|
|
399
409
|
for msg_index, msg in enumerate(messages):
|
|
400
410
|
msg_type = _msg_type(msg)
|
|
@@ -412,8 +422,18 @@ def normalize_messages_for_api(
|
|
|
412
422
|
# Map each block to an OpenAI-style message
|
|
413
423
|
openai_msgs: List[Dict[str, Any]] = []
|
|
414
424
|
for block in user_content:
|
|
415
|
-
|
|
425
|
+
block_type = getattr(block, "type", None)
|
|
426
|
+
if block_type == "tool_result":
|
|
416
427
|
tool_results_seen += 1
|
|
428
|
+
# Skip tool_result blocks that lack a preceding tool_use
|
|
429
|
+
tool_id = getattr(block, "tool_use_id", None) or getattr(block, "id", None)
|
|
430
|
+
if not tool_id:
|
|
431
|
+
skipped_tool_results_no_call += 1
|
|
432
|
+
continue
|
|
433
|
+
call_pos = tool_use_positions.get(tool_id)
|
|
434
|
+
if call_pos is None or call_pos >= msg_index:
|
|
435
|
+
skipped_tool_results_no_call += 1
|
|
436
|
+
continue
|
|
417
437
|
mapped = _content_block_to_openai(block)
|
|
418
438
|
if mapped:
|
|
419
439
|
openai_msgs.append(mapped)
|
|
@@ -498,8 +518,10 @@ def normalize_messages_for_api(
|
|
|
498
518
|
f"input_msgs={len(messages)} normalized={len(normalized)} "
|
|
499
519
|
f"tool_results_seen={tool_results_seen} tool_uses_seen={tool_uses_seen} "
|
|
500
520
|
f"tool_result_positions={len(tool_result_positions)} "
|
|
521
|
+
f"tool_use_positions={len(tool_use_positions)} "
|
|
501
522
|
f"skipped_tool_uses_no_result={skipped_tool_uses_no_result} "
|
|
502
|
-
f"skipped_tool_uses_no_id={skipped_tool_uses_no_id}"
|
|
523
|
+
f"skipped_tool_uses_no_id={skipped_tool_uses_no_id} "
|
|
524
|
+
f"skipped_tool_results_no_call={skipped_tool_results_no_call}"
|
|
503
525
|
)
|
|
504
526
|
return normalized
|
|
505
527
|
|
ripperdoc/utils/path_ignore.py
CHANGED
|
@@ -11,8 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Dict, List, Optional, Set, Tuple
|
|
15
|
-
from functools import lru_cache
|
|
14
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
16
15
|
|
|
17
16
|
from ripperdoc.utils.git_utils import (
|
|
18
17
|
get_git_root,
|
|
@@ -361,7 +360,7 @@ class IgnoreFilter:
|
|
|
361
360
|
|
|
362
361
|
return result
|
|
363
362
|
|
|
364
|
-
def test(self, path: str) -> Dict[str,
|
|
363
|
+
def test(self, path: str) -> Dict[str, Any]:
|
|
365
364
|
"""Check if a path should be ignored and return details.
|
|
366
365
|
|
|
367
366
|
Returns:
|
|
@@ -369,7 +368,7 @@ class IgnoreFilter:
|
|
|
369
368
|
"""
|
|
370
369
|
path = path.replace("\\", "/").strip("/")
|
|
371
370
|
|
|
372
|
-
result = {"ignored": False, "rule": None}
|
|
371
|
+
result: Dict[str, Any] = {"ignored": False, "rule": None}
|
|
373
372
|
|
|
374
373
|
for pattern, is_negation in self._patterns:
|
|
375
374
|
if pattern.search(path):
|
|
@@ -29,7 +29,7 @@ class SessionSummary:
|
|
|
29
29
|
message_count: int
|
|
30
30
|
created_at: datetime
|
|
31
31
|
updated_at: datetime
|
|
32
|
-
|
|
32
|
+
last_prompt: str
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def _sessions_root() -> Path:
|
|
@@ -132,7 +132,9 @@ class SessionHistory:
|
|
|
132
132
|
}
|
|
133
133
|
try:
|
|
134
134
|
with self.path.open("a", encoding="utf-8") as fh:
|
|
135
|
-
|
|
135
|
+
# ensure_ascii=False 避免中文等字符被转义为 \uXXXX
|
|
136
|
+
# separators 去掉多余空格,减小体积
|
|
137
|
+
json.dump(entry, fh, ensure_ascii=False, separators=(",", ":"))
|
|
136
138
|
fh.write("\n")
|
|
137
139
|
if isinstance(msg_uuid, str):
|
|
138
140
|
self._seen_ids.add(msg_uuid)
|
|
@@ -198,10 +200,20 @@ def list_session_summaries(project_path: Path) -> List[SessionSummary]:
|
|
|
198
200
|
if isinstance(updated_raw, str)
|
|
199
201
|
else datetime.fromtimestamp(jsonl_path.stat().st_mtime)
|
|
200
202
|
)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
# Extract last user prompt with more than 10 characters
|
|
204
|
+
# If not found, fall back to any user prompt
|
|
205
|
+
last_prompt = ""
|
|
206
|
+
fallback_prompt = ""
|
|
207
|
+
for payload in reversed(conversation_payloads):
|
|
208
|
+
if payload.get("type") != "user":
|
|
209
|
+
continue
|
|
210
|
+
prompt = _extract_prompt(payload)
|
|
211
|
+
if not prompt:
|
|
212
|
+
continue
|
|
213
|
+
if not fallback_prompt:
|
|
214
|
+
fallback_prompt = prompt
|
|
215
|
+
if len(prompt) > 10:
|
|
216
|
+
last_prompt = prompt
|
|
205
217
|
break
|
|
206
218
|
summaries.append(
|
|
207
219
|
SessionSummary(
|
|
@@ -210,7 +222,7 @@ def list_session_summaries(project_path: Path) -> List[SessionSummary]:
|
|
|
210
222
|
message_count=len(conversation_payloads),
|
|
211
223
|
created_at=created_at,
|
|
212
224
|
updated_at=updated_at,
|
|
213
|
-
|
|
225
|
+
last_prompt=last_prompt or fallback_prompt or "(no prompt)",
|
|
214
226
|
)
|
|
215
227
|
)
|
|
216
228
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ripperdoc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: AI-powered terminal assistant for coding tasks
|
|
5
5
|
Author: Ripperdoc Team
|
|
6
6
|
License: Apache-2.0
|
|
@@ -35,9 +35,28 @@ Requires-Dist: black>=23.0.0; extra == "dev"
|
|
|
35
35
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
<div align="center">
|
|
39
39
|
|
|
40
|
-
Ripperdoc
|
|
40
|
+
# Ripperdoc
|
|
41
|
+
|
|
42
|
+
_an open-source, extensible AI coding agent that runs in your terminal_
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<a href="https://opensource.org/licenses/Apache-2.0">
|
|
46
|
+
<img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg">
|
|
47
|
+
</a>
|
|
48
|
+
<a href="https://www.python.org/downloads/">
|
|
49
|
+
<img src="https://img.shields.io/badge/python-3.10+-blue.svg">
|
|
50
|
+
</a>
|
|
51
|
+
<a href="https://github.com/quantmew/ripperdoc/stargazers">
|
|
52
|
+
<img src="https://img.shields.io/github/stars/quantmew/ripperdoc.svg" alt="GitHub stars">
|
|
53
|
+
</a>
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
Ripperdoc is your on-machine AI coding assistant, similar to [Claude Code](https://claude.com/claude-code), [Codex](https://github.com/openai/codex), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Aider](https://github.com/paul-gauthier/aider), and [Goose](https://github.com/block/goose). It can write code, refactor projects, execute shell commands, and manage files - all through natural language conversations in your terminal.
|
|
58
|
+
|
|
59
|
+
Designed for maximum flexibility, Ripperdoc works with **any LLM** (Anthropic Claude, OpenAI, DeepSeek, local models via OpenAI-compatible APIs), supports **custom hooks** to intercept and control tool execution, and offers both an interactive CLI and a **Python SDK** for headless automation.
|
|
41
60
|
|
|
42
61
|
[中文文档](README_CN.md) | [Contributing](CONTRIBUTING.md) | [Documentation](docs/)
|
|
43
62
|
|
|
@@ -60,6 +79,8 @@ Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an int
|
|
|
60
79
|
- **MCP Server Support** - Integration with Model Context Protocol servers
|
|
61
80
|
- **Session Management** - Persistent session history and usage tracking
|
|
62
81
|
- **Jupyter Notebook Support** - Edit .ipynb files directly
|
|
82
|
+
- **Hooks System** - Execute custom scripts at lifecycle events with decision control
|
|
83
|
+
- **Custom Commands** - Define reusable slash commands with parameter substitution
|
|
63
84
|
|
|
64
85
|
## Installation
|
|
65
86
|
|
|
@@ -1,47 +1,59 @@
|
|
|
1
|
-
ripperdoc/__init__.py,sha256=
|
|
1
|
+
ripperdoc/__init__.py,sha256=lrY1tU8qp_EIUO-H5GAvKzGaBf8Z5xcFbY1i2_NBgjE,66
|
|
2
2
|
ripperdoc/__main__.py,sha256=1Avq2MceBfwUlNsfasC8n4dqVL_V56Bl3DRsnY4_Nxk,370
|
|
3
3
|
ripperdoc/cli/__init__.py,sha256=03wf6gXBcEgXJrDJS-W_5BEG_DdJ_ep7CxQFPML-73g,35
|
|
4
|
-
ripperdoc/cli/cli.py,sha256=
|
|
5
|
-
ripperdoc/cli/commands/__init__.py,sha256=
|
|
4
|
+
ripperdoc/cli/cli.py,sha256=5H_kIa-kPVyF_KyQA2QROLr6JrHg99RORyHKPrS82qk,14316
|
|
5
|
+
ripperdoc/cli/commands/__init__.py,sha256=Xs69l9O8VglF1z1JkxbmAy_9ylrfXZxFW9G5GiPQBZk,4676
|
|
6
6
|
ripperdoc/cli/commands/agents_cmd.py,sha256=YDE9oIXPmsPyvkHhq95aXxnneN3PqZ1ZOtcn26cXeO8,10438
|
|
7
7
|
ripperdoc/cli/commands/base.py,sha256=4KUjxCM04MwbSMUKVNEBph_jeAKPI8b5MHsUFoz7l5g,386
|
|
8
|
-
ripperdoc/cli/commands/clear_cmd.py,sha256=
|
|
8
|
+
ripperdoc/cli/commands/clear_cmd.py,sha256=FDZ0W34VxGyLhLiU4TzukHCyElqsnLwkCmfKJqLfFAQ,366
|
|
9
9
|
ripperdoc/cli/commands/compact_cmd.py,sha256=uR_nB1OX7cUL1TOJoefwdO31Qfyjd0nZSSttErqUxbA,473
|
|
10
10
|
ripperdoc/cli/commands/config_cmd.py,sha256=ebIQk7zUFv353liWfbBSSfPiOaaCR7rQsd_eTw7nsvY,884
|
|
11
11
|
ripperdoc/cli/commands/context_cmd.py,sha256=6Yrz3_Oa2NwEsZo4tLK_PFFYP0Vq-amQCMBomSVFmBo,5220
|
|
12
12
|
ripperdoc/cli/commands/cost_cmd.py,sha256=yD9LSqgxVvYNTDPnEHxugjyLWcmbtH5dXim7DIW9zXc,2822
|
|
13
13
|
ripperdoc/cli/commands/doctor_cmd.py,sha256=q_PO1mnknRysVG7uopiqDWvkIIcvRX2i-JWgfKN-0gQ,7052
|
|
14
|
-
ripperdoc/cli/commands/exit_cmd.py,sha256=
|
|
15
|
-
ripperdoc/cli/commands/help_cmd.py,sha256=
|
|
14
|
+
ripperdoc/cli/commands/exit_cmd.py,sha256=B0CNKQos2eRC4LSjizLdKsFYzFfwRkrUur6Afu3Fh9M,334
|
|
15
|
+
ripperdoc/cli/commands/help_cmd.py,sha256=jyK6U2bsGEIwFpu08slVHKfxRyS3oblnRXdqSgU_W4w,978
|
|
16
|
+
ripperdoc/cli/commands/hooks_cmd.py,sha256=9kTMXl-7vR9Y63Dm8iieuRK5jYnnlsqWKG5NvDsWUxU,21228
|
|
16
17
|
ripperdoc/cli/commands/mcp_cmd.py,sha256=ZCnswx0TIiaiUUsIX7NpHaLZLZtvlUhBnN12s_ZtPCA,2424
|
|
17
18
|
ripperdoc/cli/commands/memory_cmd.py,sha256=gDvRr_-U1gMrOdC3OvujYLL5_CUgyZpwaJdytRP5CBM,6549
|
|
18
19
|
ripperdoc/cli/commands/models_cmd.py,sha256=p6IeV_K9BjOahmtqmI2Gu7xsqRagVsIPYy7FEeuKQWQ,16135
|
|
19
|
-
ripperdoc/cli/commands/permissions_cmd.py,sha256=
|
|
20
|
-
ripperdoc/cli/commands/resume_cmd.py,sha256=
|
|
20
|
+
ripperdoc/cli/commands/permissions_cmd.py,sha256=k2n82VxlESxM7u5TUrUh85NM-n0JHjqnJxzeAaHpDL0,11325
|
|
21
|
+
ripperdoc/cli/commands/resume_cmd.py,sha256=pFuo3_S_6l3F8usQ9NfyJvc-nBliKW9ct8Rma8YXPlA,4121
|
|
21
22
|
ripperdoc/cli/commands/status_cmd.py,sha256=yM_c_GgoAL7CMH_ucGSwUhlbHggxYuvCEb4AXtpN-8s,5534
|
|
22
23
|
ripperdoc/cli/commands/tasks_cmd.py,sha256=QrRF9MKg6LIH9BQz5E39KKdrwMiI3HTvI-c14aM7BU0,8815
|
|
23
24
|
ripperdoc/cli/commands/todos_cmd.py,sha256=7Q0B1NVqGtB3R29ndbn4m0VQQm-YQ7d4Wlk7vJ7dLQI,1848
|
|
24
25
|
ripperdoc/cli/commands/tools_cmd.py,sha256=3cMi0vN4mAUhpKqJtRgNvZfcKzRPaMs_pkYYXlyvSSU,384
|
|
25
26
|
ripperdoc/cli/ui/__init__.py,sha256=TxSzTYdITlrYmYVfins_w_jzPqqWRpqky5u1ikwvmtM,43
|
|
26
27
|
ripperdoc/cli/ui/context_display.py,sha256=3ezdtHVwltkPQ5etYwfqUh-fjnpPu8B3P81UzrdHxZs,10020
|
|
27
|
-
ripperdoc/cli/ui/
|
|
28
|
-
ripperdoc/cli/ui/
|
|
28
|
+
ripperdoc/cli/ui/file_mention_completer.py,sha256=U6uZbhCamC-cJTVCbblcXvPqmkaevNgKotMC_ftssug,11648
|
|
29
|
+
ripperdoc/cli/ui/helpers.py,sha256=DmgMMouyQdesjQ5RsErwsRCKVdWiDJnpqJjv90a3neE,2545
|
|
30
|
+
ripperdoc/cli/ui/interrupt_handler.py,sha256=dg15njl4NsFQvwAtxKPETeubuYl5OwiV2VlPECW2aNI,5930
|
|
31
|
+
ripperdoc/cli/ui/message_display.py,sha256=FHvfBY9hvf-lroB6SDzHDMS6bxuh57BfK5iBIifs2Ks,10361
|
|
32
|
+
ripperdoc/cli/ui/panels.py,sha256=lzgg7kP8nzJKqGFjE-0UVbr9a1YZ0i2XlkUy6-LcLnk,1875
|
|
33
|
+
ripperdoc/cli/ui/rich_ui.py,sha256=_VOx1zdt_QV23cwwiV6P8FiAA_H-JLIbC6WqYBLgUwM,47066
|
|
29
34
|
ripperdoc/cli/ui/spinner.py,sha256=XsPRwJ-70InLX9Qw50CEgSHn5oKA5PFIue8Un4edhUk,1449
|
|
30
35
|
ripperdoc/cli/ui/thinking_spinner.py,sha256=9Et5EqPChfkmkiOO8w1OPs8t-sHaisgjn9A__kEYLyg,2824
|
|
31
|
-
ripperdoc/cli/ui/tool_renderers.py,sha256=
|
|
36
|
+
ripperdoc/cli/ui/tool_renderers.py,sha256=gVuZM083Nys9KWYAFTdmr1vpJm7ardqNhyUZx7KkL6s,11170
|
|
32
37
|
ripperdoc/core/__init__.py,sha256=UemJCA-Y8df1466AX-YbRFj071zKajmqO1mi40YVW2g,40
|
|
33
|
-
ripperdoc/core/agents.py,sha256=
|
|
38
|
+
ripperdoc/core/agents.py,sha256=3glBiVM8e9NruCQGYl4inQ6Lt7jWb7C4S44YQXRWWl8,19930
|
|
34
39
|
ripperdoc/core/commands.py,sha256=NXCkljYbAP4dDoRy-_3semFNWxG4YAk9q82u8FTKH60,835
|
|
35
40
|
ripperdoc/core/config.py,sha256=fuzXTSSpPFIkzgZJW-tOf18cNeemrot64ihO4cdM79g,20979
|
|
41
|
+
ripperdoc/core/custom_commands.py,sha256=2Voc1u6WSZ-nJYRvGKmUcpNvzLCotacgGupvXUv6RT8,14289
|
|
36
42
|
ripperdoc/core/default_tools.py,sha256=fHmqIlPIE9qGwmgeYYw-QepKRoQLMhclnCv6Qahews0,3090
|
|
37
43
|
ripperdoc/core/permissions.py,sha256=_WLWE7Kq-Z5j3zEDAPr8JqdT0fz2oFqNs18NL0qoeWQ,9768
|
|
38
|
-
ripperdoc/core/query.py,sha256=
|
|
39
|
-
ripperdoc/core/query_utils.py,sha256
|
|
44
|
+
ripperdoc/core/query.py,sha256=We15XbLl_rAdAgJ5NAufYLNes6kNNeQ7CMj8rcGgBsE,40939
|
|
45
|
+
ripperdoc/core/query_utils.py,sha256=-lBRL5oAV0p6p6LukpFfBZKEcRdztbzCNOt51pEsKBM,24776
|
|
40
46
|
ripperdoc/core/skills.py,sha256=XkMt3WPT2_0xfx2qQhEnBbwJ0121aRFmuXLckw3MtVU,10251
|
|
41
47
|
ripperdoc/core/system_prompt.py,sha256=smhuRfzbvhfzNsQ3D89Mucll16u_40VWpOzKS0kJPFQ,26724
|
|
42
|
-
ripperdoc/core/tool.py,sha256=
|
|
48
|
+
ripperdoc/core/tool.py,sha256=Hnnt7FYBGlD6lyAr9XhDgp_LfcP7o3yapzd7t6FPlBE,7813
|
|
49
|
+
ripperdoc/core/hooks/__init__.py,sha256=xw7VJQu1ZB0ENHVqL5xtruBnP3d0FNgrBH6NTL2xYgg,2735
|
|
50
|
+
ripperdoc/core/hooks/config.py,sha256=2KZXpkCGWecg3loQkYZbr5Xh68dDZv-XlTvjwQbl29o,10123
|
|
51
|
+
ripperdoc/core/hooks/events.py,sha256=ZDyP_YaRPKeDz23d6wKbfwlb64poRZDpiuPJaz8ZN9w,17957
|
|
52
|
+
ripperdoc/core/hooks/executor.py,sha256=inOAT2-xB_lXiz0io6c3QV4Wnw9ntl0unf-w0nXPMak,16818
|
|
53
|
+
ripperdoc/core/hooks/integration.py,sha256=Gb3ADGoTTYWDrCmkcgjlyLyldu3wRcji77_2VAyuYJw,11213
|
|
54
|
+
ripperdoc/core/hooks/manager.py,sha256=8YqvGPfOL3ianxFz_tnd7c_HbhHeLDHr5cLk1ayKicU,23706
|
|
43
55
|
ripperdoc/core/providers/__init__.py,sha256=yevsHF0AUI4b6Wiq_401NXewJ3dqe8LUUtQm0TLPPNQ,1911
|
|
44
|
-
ripperdoc/core/providers/anthropic.py,sha256=
|
|
56
|
+
ripperdoc/core/providers/anthropic.py,sha256=V5sofBIU1w7bo1FmrrZ_lqVvGRXt9ZkykDa_J0b1xtc,26309
|
|
45
57
|
ripperdoc/core/providers/base.py,sha256=HNOa3_XWszu6DbI8BYixxV0hnZb9qZ_FU4uinFVRHjU,9271
|
|
46
58
|
ripperdoc/core/providers/gemini.py,sha256=3U69Gh6hiL8QWsf-nawZJTfh5oeZP5DAmXYDfWtrEQE,24786
|
|
47
59
|
ripperdoc/core/providers/openai.py,sha256=FiNTCBtFpq0i0K6S0OyC-F-0HssSWJE6jrH3xRsPzD4,20981
|
|
@@ -51,19 +63,19 @@ ripperdoc/tools/__init__.py,sha256=RBFz0DDnztDXMqv_zRxFHVY-ez2HYcncx8zh_y-BX6w,4
|
|
|
51
63
|
ripperdoc/tools/ask_user_question_tool.py,sha256=QgzmIDVR-wdlLf9fSiVPbRm_8tSaIlGJhuuRYOCGiUU,15446
|
|
52
64
|
ripperdoc/tools/background_shell.py,sha256=HangGLwN4iy-udo02zUZF3QRPIqOa7sVDesbv2wL9Js,12854
|
|
53
65
|
ripperdoc/tools/bash_output_tool.py,sha256=ljIOzTOnkbQfe3jExlhpUlMiLT6HpeD-1QI-D1CwHh8,3379
|
|
54
|
-
ripperdoc/tools/bash_tool.py,sha256=
|
|
66
|
+
ripperdoc/tools/bash_tool.py,sha256=Wua9_R7S0fMfV8SmmI6_m4mpJ7gYdyku34dyTPIRA4o,42757
|
|
55
67
|
ripperdoc/tools/dynamic_mcp_tool.py,sha256=GERh7qT1mPVivFUIhlFxPNRUwOGNw5CmCnymEwQ-7vk,15662
|
|
56
68
|
ripperdoc/tools/enter_plan_mode_tool.py,sha256=FYjm_TmBL55pY4GdP7t0ISlqg-Qe3DwpIt-2weL1S4s,7976
|
|
57
69
|
ripperdoc/tools/exit_plan_mode_tool.py,sha256=3smkwGLTITem5fgA8catSSRay_a1OGQrjs8JF1zDdUQ,5756
|
|
58
|
-
ripperdoc/tools/file_edit_tool.py,sha256=
|
|
59
|
-
ripperdoc/tools/file_read_tool.py,sha256=
|
|
70
|
+
ripperdoc/tools/file_edit_tool.py,sha256=pF4ZCBFq3vy2DukxGZjBGoyVJIRH-7UY4A-TpFegzdA,13768
|
|
71
|
+
ripperdoc/tools/file_read_tool.py,sha256=CvjnYusjolTH-PoQ8CPUVVR37GMLAntT16ffRwqKBto,7396
|
|
60
72
|
ripperdoc/tools/file_write_tool.py,sha256=SUsFvLVvCwegxEDhL8xpppNRlSl_Hcc1xwNR35FbMqU,7044
|
|
61
73
|
ripperdoc/tools/glob_tool.py,sha256=oy1S-MrQl57X_wpNXcqXyE4oHI3kmpOQoTYavx3mzEg,5932
|
|
62
74
|
ripperdoc/tools/grep_tool.py,sha256=n_YNKg8w63JgVJVpg7Qakj75JrymeKByNoapl8IS05U,14125
|
|
63
75
|
ripperdoc/tools/kill_bash_tool.py,sha256=36F8w2Rm1IVQitwOAwS-D8NTnyQdWfKWIam44qlXErk,4625
|
|
64
76
|
ripperdoc/tools/ls_tool.py,sha256=x0nw7F5cWjKK7Vb66nqdg9djgupPpCTwPAqd5rZlMUs,15367
|
|
65
77
|
ripperdoc/tools/mcp_tools.py,sha256=P0wbk_WRUD_MVkLDpMjttjQ13nje8Zc-mR3ud94QqZ0,23018
|
|
66
|
-
ripperdoc/tools/multi_edit_tool.py,sha256=
|
|
78
|
+
ripperdoc/tools/multi_edit_tool.py,sha256=ndCb-OlHQRqlrGElZ_q5QOTibZqW9uDp6e4ctOPNLbU,17579
|
|
67
79
|
ripperdoc/tools/notebook_edit_tool.py,sha256=FOgx0RtZnD1zZCUv5vsXS531ep26ABXaNsRhh0ZU4Uc,14393
|
|
68
80
|
ripperdoc/tools/skill_tool.py,sha256=vquTDL8ZihGvgP6U6EswOm5IjQYFHwxIiLzlzCEWrVw,7701
|
|
69
81
|
ripperdoc/tools/task_tool.py,sha256=EcUAaWY3aTTZawE84wvk2LyHSNU7nXAbpeDoOjU4_K0,18325
|
|
@@ -74,6 +86,7 @@ ripperdoc/utils/bash_constants.py,sha256=KNn8bzB6nVU5jid9jvjiH4FAu8pP3DZONJ-OknJ
|
|
|
74
86
|
ripperdoc/utils/bash_output_utils.py,sha256=3Cf5wKJzRbUesmCNy5HtXIBtv0Z2BxklTfFHJ9q1T3w,1210
|
|
75
87
|
ripperdoc/utils/coerce.py,sha256=KOPb4KR4p32nwHWG_6GsGHeVZunJyYc2YhC5DLmEZO8,1015
|
|
76
88
|
ripperdoc/utils/context_length_errors.py,sha256=oyDVr_ME_6j97TLwVZ8bDMb6ISGQx6wEHrY7ckc0GuA,7714
|
|
89
|
+
ripperdoc/utils/conversation_compaction.py,sha256=m9AHZcRJwbZUHrNYl5Q1esjKQSBKFkARBkO9YNUjm5Q,18364
|
|
77
90
|
ripperdoc/utils/exit_code_handlers.py,sha256=QtO1iDxVAb8Xp03D6_QixPoJC-RQlcp3ssIo_rm4two,7973
|
|
78
91
|
ripperdoc/utils/file_watch.py,sha256=CoUIcLuS-VcfxotuxFkel5KpNluMmLGJKDzx26MG3yY,4039
|
|
79
92
|
ripperdoc/utils/git_utils.py,sha256=Hq-Zx-KPyX4lp_i8ozhic15LyYdX_IfCRm-EyoFu59A,9047
|
|
@@ -81,15 +94,16 @@ ripperdoc/utils/json_utils.py,sha256=e12eMpWlLDniHZVg7zdOkXw5wBZhnjhVtDm8tpBEOjk
|
|
|
81
94
|
ripperdoc/utils/log.py,sha256=mieFMPxiv-M87bB-dgiY8D5WMxQbjVKJdsrW8QvCui8,6138
|
|
82
95
|
ripperdoc/utils/mcp.py,sha256=xszG3kDrlctVVZvXOHr7wgndthpu-AwMySSwpnaHFGc,19445
|
|
83
96
|
ripperdoc/utils/memory.py,sha256=KNB8Eoobl0vgIEh6phXKtGmDUct7_DNQH-F6Il4KRDQ,8009
|
|
84
|
-
ripperdoc/utils/message_compaction.py,sha256=
|
|
85
|
-
ripperdoc/utils/
|
|
97
|
+
ripperdoc/utils/message_compaction.py,sha256=ydTtMqo09ds1qTneJrHaFroZ4UtmzzXlXHErPm7ZY5E,22370
|
|
98
|
+
ripperdoc/utils/message_formatting.py,sha256=M-2zEkjNEiLyda9x4S0-xyIBSsVOQS3eTzbIE3K7G_Q,7492
|
|
99
|
+
ripperdoc/utils/messages.py,sha256=OrUK758mmUx2_u_hE5RFLJMp8VeDy-crLGVADwgFO3c,21665
|
|
86
100
|
ripperdoc/utils/output_utils.py,sha256=R3wqFh9Dko_GK00Exx7XI0DnnldRWMsxZypYX5y6SJo,7448
|
|
87
|
-
ripperdoc/utils/path_ignore.py,sha256=
|
|
101
|
+
ripperdoc/utils/path_ignore.py,sha256=m-cbf5I_1HeV7mjwzpb4O9BXGMTFZ4EnFmmguzD7WeY,17596
|
|
88
102
|
ripperdoc/utils/path_utils.py,sha256=C45Q3OeXnj-0FVEtvf_tdG5922XB6HthUzlUCvfc17Y,1626
|
|
89
103
|
ripperdoc/utils/prompt.py,sha256=zICNEsA_OtKx8t3zo9tHLXXu6G5K8rPO3jFLKz4j5tg,560
|
|
90
104
|
ripperdoc/utils/safe_get_cwd.py,sha256=IvG8dIJd2tC5_glUsfeWXkpcF1EHzdkjFtuUGJd669w,815
|
|
91
105
|
ripperdoc/utils/sandbox_utils.py,sha256=G91P8dw2VFcCiCpjXZ4LvzbAPiO8REqMhw39eI5Z4dU,1123
|
|
92
|
-
ripperdoc/utils/session_history.py,sha256=
|
|
106
|
+
ripperdoc/utils/session_history.py,sha256=SQ3HusavKtbVyx6V-KPCXbQtTm_rcVuL-qjwv_iNS7Y,9568
|
|
93
107
|
ripperdoc/utils/session_usage.py,sha256=p8_s46zDTzV1qzP4HR4PuZmLeJvSfq9mG_Y5rCnRYyA,3213
|
|
94
108
|
ripperdoc/utils/shell_token_utils.py,sha256=SduoSU-RERJdM_7gBn0urr5UXtl4XOpPgydBd2fwzWg,2500
|
|
95
109
|
ripperdoc/utils/shell_utils.py,sha256=t-neFPy_VhEmWZ79J7hh1ULBEdX116Rb9_pnXvir1Jw,5235
|
|
@@ -99,9 +113,9 @@ ripperdoc/utils/permissions/__init__.py,sha256=33FfOaDLepxJSkp0RLvTdVu7qBXuEcnOo
|
|
|
99
113
|
ripperdoc/utils/permissions/path_validation_utils.py,sha256=FWbb21Hwgb9gUPu_WtA14w99UUojVQLVz5HByormoUs,5760
|
|
100
114
|
ripperdoc/utils/permissions/shell_command_validation.py,sha256=94ylqoDUiTF4v4wEiVS35jFJakyaSxdIFqYKumPTrGk,21205
|
|
101
115
|
ripperdoc/utils/permissions/tool_permission_utils.py,sha256=6Fdu9-dMKhLsUExjEjoS0EUeRpEVN5UkqyseIC05YmM,9207
|
|
102
|
-
ripperdoc-0.2.
|
|
103
|
-
ripperdoc-0.2.
|
|
104
|
-
ripperdoc-0.2.
|
|
105
|
-
ripperdoc-0.2.
|
|
106
|
-
ripperdoc-0.2.
|
|
107
|
-
ripperdoc-0.2.
|
|
116
|
+
ripperdoc-0.2.8.dist-info/licenses/LICENSE,sha256=bRv9UhBor6GhnQDj12RciDcRfu0R7sB-lqCy1sWF75c,9242
|
|
117
|
+
ripperdoc-0.2.8.dist-info/METADATA,sha256=aoAeFiQqkUDQpJEt4buP6G1wKlkCMXI88slI-tGSDGg,7474
|
|
118
|
+
ripperdoc-0.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
119
|
+
ripperdoc-0.2.8.dist-info/entry_points.txt,sha256=79aohFxFPJmrQ3-Mhain04vb3EWpuc0EyzvDDUnwAu4,81
|
|
120
|
+
ripperdoc-0.2.8.dist-info/top_level.txt,sha256=u8LbdTr1a-laHgCO0Utl_R3QGFUhLxWelCDnP2ZgpCU,10
|
|
121
|
+
ripperdoc-0.2.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|