ripperdoc 0.2.6__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 (107) hide show
  1. ripperdoc/__init__.py +3 -0
  2. ripperdoc/__main__.py +20 -0
  3. ripperdoc/cli/__init__.py +1 -0
  4. ripperdoc/cli/cli.py +405 -0
  5. ripperdoc/cli/commands/__init__.py +82 -0
  6. ripperdoc/cli/commands/agents_cmd.py +263 -0
  7. ripperdoc/cli/commands/base.py +19 -0
  8. ripperdoc/cli/commands/clear_cmd.py +18 -0
  9. ripperdoc/cli/commands/compact_cmd.py +23 -0
  10. ripperdoc/cli/commands/config_cmd.py +31 -0
  11. ripperdoc/cli/commands/context_cmd.py +144 -0
  12. ripperdoc/cli/commands/cost_cmd.py +82 -0
  13. ripperdoc/cli/commands/doctor_cmd.py +221 -0
  14. ripperdoc/cli/commands/exit_cmd.py +19 -0
  15. ripperdoc/cli/commands/help_cmd.py +20 -0
  16. ripperdoc/cli/commands/mcp_cmd.py +70 -0
  17. ripperdoc/cli/commands/memory_cmd.py +202 -0
  18. ripperdoc/cli/commands/models_cmd.py +413 -0
  19. ripperdoc/cli/commands/permissions_cmd.py +302 -0
  20. ripperdoc/cli/commands/resume_cmd.py +98 -0
  21. ripperdoc/cli/commands/status_cmd.py +167 -0
  22. ripperdoc/cli/commands/tasks_cmd.py +278 -0
  23. ripperdoc/cli/commands/todos_cmd.py +69 -0
  24. ripperdoc/cli/commands/tools_cmd.py +19 -0
  25. ripperdoc/cli/ui/__init__.py +1 -0
  26. ripperdoc/cli/ui/context_display.py +298 -0
  27. ripperdoc/cli/ui/helpers.py +22 -0
  28. ripperdoc/cli/ui/rich_ui.py +1557 -0
  29. ripperdoc/cli/ui/spinner.py +49 -0
  30. ripperdoc/cli/ui/thinking_spinner.py +128 -0
  31. ripperdoc/cli/ui/tool_renderers.py +298 -0
  32. ripperdoc/core/__init__.py +1 -0
  33. ripperdoc/core/agents.py +486 -0
  34. ripperdoc/core/commands.py +33 -0
  35. ripperdoc/core/config.py +559 -0
  36. ripperdoc/core/default_tools.py +88 -0
  37. ripperdoc/core/permissions.py +252 -0
  38. ripperdoc/core/providers/__init__.py +47 -0
  39. ripperdoc/core/providers/anthropic.py +250 -0
  40. ripperdoc/core/providers/base.py +265 -0
  41. ripperdoc/core/providers/gemini.py +615 -0
  42. ripperdoc/core/providers/openai.py +487 -0
  43. ripperdoc/core/query.py +1058 -0
  44. ripperdoc/core/query_utils.py +622 -0
  45. ripperdoc/core/skills.py +295 -0
  46. ripperdoc/core/system_prompt.py +431 -0
  47. ripperdoc/core/tool.py +240 -0
  48. ripperdoc/sdk/__init__.py +9 -0
  49. ripperdoc/sdk/client.py +333 -0
  50. ripperdoc/tools/__init__.py +1 -0
  51. ripperdoc/tools/ask_user_question_tool.py +431 -0
  52. ripperdoc/tools/background_shell.py +389 -0
  53. ripperdoc/tools/bash_output_tool.py +98 -0
  54. ripperdoc/tools/bash_tool.py +1016 -0
  55. ripperdoc/tools/dynamic_mcp_tool.py +428 -0
  56. ripperdoc/tools/enter_plan_mode_tool.py +226 -0
  57. ripperdoc/tools/exit_plan_mode_tool.py +153 -0
  58. ripperdoc/tools/file_edit_tool.py +346 -0
  59. ripperdoc/tools/file_read_tool.py +203 -0
  60. ripperdoc/tools/file_write_tool.py +205 -0
  61. ripperdoc/tools/glob_tool.py +179 -0
  62. ripperdoc/tools/grep_tool.py +370 -0
  63. ripperdoc/tools/kill_bash_tool.py +136 -0
  64. ripperdoc/tools/ls_tool.py +471 -0
  65. ripperdoc/tools/mcp_tools.py +591 -0
  66. ripperdoc/tools/multi_edit_tool.py +456 -0
  67. ripperdoc/tools/notebook_edit_tool.py +386 -0
  68. ripperdoc/tools/skill_tool.py +205 -0
  69. ripperdoc/tools/task_tool.py +379 -0
  70. ripperdoc/tools/todo_tool.py +494 -0
  71. ripperdoc/tools/tool_search_tool.py +380 -0
  72. ripperdoc/utils/__init__.py +1 -0
  73. ripperdoc/utils/bash_constants.py +51 -0
  74. ripperdoc/utils/bash_output_utils.py +43 -0
  75. ripperdoc/utils/coerce.py +34 -0
  76. ripperdoc/utils/context_length_errors.py +252 -0
  77. ripperdoc/utils/exit_code_handlers.py +241 -0
  78. ripperdoc/utils/file_watch.py +135 -0
  79. ripperdoc/utils/git_utils.py +274 -0
  80. ripperdoc/utils/json_utils.py +27 -0
  81. ripperdoc/utils/log.py +176 -0
  82. ripperdoc/utils/mcp.py +560 -0
  83. ripperdoc/utils/memory.py +253 -0
  84. ripperdoc/utils/message_compaction.py +676 -0
  85. ripperdoc/utils/messages.py +519 -0
  86. ripperdoc/utils/output_utils.py +258 -0
  87. ripperdoc/utils/path_ignore.py +677 -0
  88. ripperdoc/utils/path_utils.py +46 -0
  89. ripperdoc/utils/permissions/__init__.py +27 -0
  90. ripperdoc/utils/permissions/path_validation_utils.py +174 -0
  91. ripperdoc/utils/permissions/shell_command_validation.py +552 -0
  92. ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
  93. ripperdoc/utils/prompt.py +17 -0
  94. ripperdoc/utils/safe_get_cwd.py +31 -0
  95. ripperdoc/utils/sandbox_utils.py +38 -0
  96. ripperdoc/utils/session_history.py +260 -0
  97. ripperdoc/utils/session_usage.py +117 -0
  98. ripperdoc/utils/shell_token_utils.py +95 -0
  99. ripperdoc/utils/shell_utils.py +159 -0
  100. ripperdoc/utils/todo.py +203 -0
  101. ripperdoc/utils/token_estimation.py +34 -0
  102. ripperdoc-0.2.6.dist-info/METADATA +193 -0
  103. ripperdoc-0.2.6.dist-info/RECORD +107 -0
  104. ripperdoc-0.2.6.dist-info/WHEEL +5 -0
  105. ripperdoc-0.2.6.dist-info/entry_points.txt +3 -0
  106. ripperdoc-0.2.6.dist-info/licenses/LICENSE +53 -0
  107. ripperdoc-0.2.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,486 @@
1
+ """Agent definitions and helpers for Ripperdoc subagents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from functools import lru_cache
8
+ from pathlib import Path
9
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
10
+
11
+ import yaml
12
+
13
+ from ripperdoc.utils.log import get_logger
14
+ from ripperdoc.tools.ask_user_question_tool import AskUserQuestionTool
15
+ from ripperdoc.tools.bash_output_tool import BashOutputTool
16
+ from ripperdoc.tools.bash_tool import BashTool
17
+ from ripperdoc.tools.enter_plan_mode_tool import EnterPlanModeTool
18
+ from ripperdoc.tools.exit_plan_mode_tool import ExitPlanModeTool
19
+ from ripperdoc.tools.file_edit_tool import FileEditTool
20
+ from ripperdoc.tools.file_read_tool import FileReadTool
21
+ from ripperdoc.tools.file_write_tool import FileWriteTool
22
+ from ripperdoc.tools.glob_tool import GlobTool
23
+ from ripperdoc.tools.grep_tool import GrepTool
24
+ from ripperdoc.tools.kill_bash_tool import KillBashTool
25
+ from ripperdoc.tools.ls_tool import LSTool
26
+ from ripperdoc.tools.multi_edit_tool import MultiEditTool
27
+ from ripperdoc.tools.notebook_edit_tool import NotebookEditTool
28
+ from ripperdoc.tools.todo_tool import TodoReadTool, TodoWriteTool
29
+ from ripperdoc.tools.tool_search_tool import ToolSearchTool
30
+ from ripperdoc.tools.mcp_tools import (
31
+ ListMcpResourcesTool,
32
+ ListMcpServersTool,
33
+ ReadMcpResourceTool,
34
+ )
35
+
36
+
37
+ logger = get_logger()
38
+
39
+
40
+ def _safe_tool_name(factory: Any, fallback: str) -> str:
41
+ try:
42
+ name = getattr(factory(), "name", None)
43
+ return str(name) if name else fallback
44
+ except (TypeError, ValueError, RuntimeError, AttributeError):
45
+ return fallback
46
+
47
+
48
+ GLOB_TOOL_NAME = _safe_tool_name(GlobTool, "Glob")
49
+ GREP_TOOL_NAME = _safe_tool_name(GrepTool, "Grep")
50
+ VIEW_TOOL_NAME = _safe_tool_name(FileReadTool, "View")
51
+ FILE_EDIT_TOOL_NAME = _safe_tool_name(FileEditTool, "FileEdit")
52
+ MULTI_EDIT_TOOL_NAME = _safe_tool_name(MultiEditTool, "MultiEdit")
53
+ NOTEBOOK_EDIT_TOOL_NAME = _safe_tool_name(NotebookEditTool, "NotebookEdit")
54
+ FILE_WRITE_TOOL_NAME = _safe_tool_name(FileWriteTool, "FileWrite")
55
+ LS_TOOL_NAME = _safe_tool_name(LSTool, "LS")
56
+ BASH_TOOL_NAME = _safe_tool_name(BashTool, "Bash")
57
+ BASH_OUTPUT_TOOL_NAME = _safe_tool_name(BashOutputTool, "BashOutput")
58
+ KILL_BASH_TOOL_NAME = _safe_tool_name(KillBashTool, "KillBash")
59
+ TODO_READ_TOOL_NAME = _safe_tool_name(TodoReadTool, "TodoRead")
60
+ TODO_WRITE_TOOL_NAME = _safe_tool_name(TodoWriteTool, "TodoWrite")
61
+ ASK_USER_QUESTION_TOOL_NAME = _safe_tool_name(AskUserQuestionTool, "AskUserQuestion")
62
+ ENTER_PLAN_MODE_TOOL_NAME = _safe_tool_name(EnterPlanModeTool, "EnterPlanMode")
63
+ EXIT_PLAN_MODE_TOOL_NAME = _safe_tool_name(ExitPlanModeTool, "ExitPlanMode")
64
+ TOOL_SEARCH_TOOL_NAME = _safe_tool_name(ToolSearchTool, "ToolSearch")
65
+ MCP_LIST_SERVERS_TOOL_NAME = _safe_tool_name(ListMcpServersTool, "ListMcpServers")
66
+ MCP_LIST_RESOURCES_TOOL_NAME = _safe_tool_name(ListMcpResourcesTool, "ListMcpResources")
67
+ MCP_READ_RESOURCE_TOOL_NAME = _safe_tool_name(ReadMcpResourceTool, "ReadMcpResource")
68
+ TASK_TOOL_NAME = "Task"
69
+
70
+
71
+ AGENT_DIR_NAME = "agents"
72
+
73
+
74
+ class AgentLocation(str, Enum):
75
+ """Where an agent definition is sourced from."""
76
+
77
+ BUILT_IN = "built-in"
78
+ USER = "user"
79
+ PROJECT = "project"
80
+
81
+
82
+ @dataclass
83
+ class AgentDefinition:
84
+ """A parsed agent definition."""
85
+
86
+ agent_type: str
87
+ when_to_use: str
88
+ tools: List[str]
89
+ system_prompt: str
90
+ location: AgentLocation
91
+ model: Optional[str] = None
92
+ color: Optional[str] = None
93
+ filename: Optional[str] = None
94
+
95
+
96
+ @dataclass
97
+ class AgentLoadResult:
98
+ """Result of loading agent definitions."""
99
+
100
+ active_agents: List[AgentDefinition]
101
+ all_agents: List[AgentDefinition]
102
+ failed_files: List[Tuple[Path, str]]
103
+
104
+
105
+ GENERAL_AGENT_PROMPT = (
106
+ "You are a general-purpose subagent for Ripperdoc. Work autonomously on the task "
107
+ "provided by the parent agent. Use the allowed tools to research, edit files, and "
108
+ "run commands as needed. When you finish, provide a concise report describing what "
109
+ "you changed, what you investigated, and any follow-ups the parent agent should "
110
+ "share with the user."
111
+ )
112
+
113
+ CODE_REVIEW_AGENT_PROMPT = (
114
+ "You are a code review subagent. Inspect the code and summarize risks, bugs, "
115
+ "missing tests, security concerns, and regressions. Do not make code changes. "
116
+ "Provide clear, actionable feedback that the parent agent can relay to the user."
117
+ )
118
+
119
+ EXPLORE_AGENT_PROMPT = (
120
+ "You are a file search specialist. "
121
+ "You excel at thoroughly navigating and exploring codebases.\n\n"
122
+ "=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===\n"
123
+ "This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:\n"
124
+ "- Creating new files (no Write, touch, or file creation of any kind)\n"
125
+ "- Modifying existing files (no Edit operations)\n"
126
+ "- Deleting files (no rm or deletion)\n"
127
+ "- Moving or copying files (no mv or cp)\n"
128
+ "- Creating temporary files anywhere, including /tmp\n"
129
+ "- Using redirect operators (>, >>, |) or heredocs to write to files\n"
130
+ "- Running ANY commands that change system state\n\n"
131
+ "Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access "
132
+ "to file editing tools - attempting to edit files will fail.\n\n"
133
+ "Your strengths:\n"
134
+ "- Rapidly finding files using glob patterns\n"
135
+ "- Searching code and text with powerful regex patterns\n"
136
+ "- Reading and analyzing file contents\n\n"
137
+ "Guidelines:\n"
138
+ f"- Use {GLOB_TOOL_NAME} for broad file pattern matching\n"
139
+ f"- Use {GREP_TOOL_NAME} for searching file contents with regex\n"
140
+ f"- Use {VIEW_TOOL_NAME} when you know the specific file path you need to read\n"
141
+ f"- Use {BASH_TOOL_NAME} ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)\n"
142
+ f"- NEVER use {BASH_TOOL_NAME} for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification\n"
143
+ "- Adapt your search approach based on the thoroughness level specified by the caller\n"
144
+ "- Return file paths as absolute paths in your final response\n"
145
+ "- For clear communication, avoid using emojis\n"
146
+ "- Communicate your final report directly as a regular message - do NOT attempt to create files\n\n"
147
+ "NOTE: You are meant to be a fast agent that returns output as quickly as possible. In order to achieve this you must:\n"
148
+ "- Make efficient use of the tools that you have at your disposal: be smart about how you search for files and implementations\n"
149
+ "- Wherever possible you should try to spawn multiple parallel tool calls for grepping and reading files\n\n"
150
+ "Complete the user's search request efficiently and report your findings clearly."
151
+ )
152
+
153
+ PLAN_AGENT_PROMPT = (
154
+ "You are a software architect and planning specialist. Your role is "
155
+ "to explore the codebase and design implementation plans.\n\n"
156
+ "=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===\n"
157
+ "This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from:\n"
158
+ "- Creating new files (no Write, touch, or file creation of any kind)\n"
159
+ "- Modifying existing files (no Edit operations)\n"
160
+ "- Deleting files (no rm or deletion)\n"
161
+ "- Moving or copying files (no mv or cp)\n"
162
+ "- Creating temporary files anywhere, including /tmp\n"
163
+ "- Using redirect operators (>, >>, |) or heredocs to write to files\n"
164
+ "- Running ANY commands that change system state\n\n"
165
+ "Your role is EXCLUSIVELY to explore the codebase and design implementation plans. "
166
+ "You do NOT have access to file editing tools - attempting to edit files will fail.\n\n"
167
+ "You will be provided with a set of requirements and optionally a perspective on how "
168
+ "to approach the design process.\n\n"
169
+ "## Your Process\n\n"
170
+ "1. **Understand Requirements**: Focus on the requirements provided and apply your "
171
+ "assigned perspective throughout the design process.\n\n"
172
+ "2. **Explore Thoroughly**:\n"
173
+ " - Read any files provided to you in the initial prompt\n"
174
+ f" - Find existing patterns and conventions using {GLOB_TOOL_NAME}, {GREP_TOOL_NAME}, and {VIEW_TOOL_NAME}\n"
175
+ " - Understand the current architecture\n"
176
+ " - Identify similar features as reference\n"
177
+ " - Trace through relevant code paths\n"
178
+ f" - Use {BASH_TOOL_NAME} ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)\n"
179
+ f" - NEVER use {BASH_TOOL_NAME} for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification\n\n"
180
+ "3. **Design Solution**:\n"
181
+ " - Create implementation approach based on your assigned perspective\n"
182
+ " - Consider trade-offs and architectural decisions\n"
183
+ " - Follow existing patterns where appropriate\n\n"
184
+ "4. **Detail the Plan**:\n"
185
+ " - Provide step-by-step implementation strategy\n"
186
+ " - Identify dependencies and sequencing\n"
187
+ " - Anticipate potential challenges\n\n"
188
+ "## Required Output\n\n"
189
+ "End your response with:\n\n"
190
+ "### Critical Files for Implementation\n"
191
+ "List 3-5 files most critical for implementing this plan:\n"
192
+ '- path/to/file1.ts - [Brief reason: e.g., "Core logic to modify"]\n'
193
+ '- path/to/file2.ts - [Brief reason: e.g., "Interfaces to implement"]\n'
194
+ '- path/to/file3.ts - [Brief reason: e.g., "Pattern to follow"]\n\n'
195
+ "REMEMBER: You can ONLY explore and plan. You CANNOT and MUST NOT write, edit, or "
196
+ "modify any files. You do NOT have access to file editing tools."
197
+ )
198
+
199
+
200
+ def _built_in_agents() -> List[AgentDefinition]:
201
+ return [
202
+ AgentDefinition(
203
+ agent_type="general-purpose",
204
+ when_to_use=(
205
+ "General-purpose agent for multi-step coding tasks, deep searches, and "
206
+ "investigations that need their own context window."
207
+ ),
208
+ tools=["*"],
209
+ system_prompt=GENERAL_AGENT_PROMPT,
210
+ location=AgentLocation.BUILT_IN,
211
+ color="cyan",
212
+ ),
213
+ AgentDefinition(
214
+ agent_type="code-reviewer",
215
+ when_to_use=(
216
+ "Run after implementing non-trivial code changes to review for correctness, "
217
+ "testing gaps, security issues, and regressions."
218
+ ),
219
+ tools=["View", "Glob", "Grep"],
220
+ system_prompt=CODE_REVIEW_AGENT_PROMPT,
221
+ location=AgentLocation.BUILT_IN,
222
+ color="yellow",
223
+ ),
224
+ AgentDefinition(
225
+ agent_type="explore",
226
+ when_to_use=(
227
+ "Fast agent specialized for exploring codebases. Use this when you need to quickly find "
228
+ 'files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), '
229
+ 'or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, '
230
+ 'specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, '
231
+ 'or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
232
+ ),
233
+ tools=["View", "Glob", "Grep"],
234
+ system_prompt=EXPLORE_AGENT_PROMPT,
235
+ location=AgentLocation.BUILT_IN,
236
+ color="green",
237
+ model="task",
238
+ ),
239
+ AgentDefinition(
240
+ agent_type="plan",
241
+ when_to_use=(
242
+ "Software architect agent for designing implementation plans. Use this when "
243
+ "you need to plan the implementation strategy for a task. Returns step-by-step "
244
+ "plans, identifies critical files, and considers architectural trade-offs."
245
+ ),
246
+ tools=["View", "Glob", "Grep"],
247
+ system_prompt=PLAN_AGENT_PROMPT,
248
+ location=AgentLocation.BUILT_IN,
249
+ color="blue",
250
+ model=None,
251
+ ),
252
+ ]
253
+
254
+
255
+ def _agent_dirs() -> List[Tuple[Path, AgentLocation]]:
256
+ home_dir = Path.home() / ".ripperdoc" / AGENT_DIR_NAME
257
+ project_dir = Path.cwd() / ".ripperdoc" / AGENT_DIR_NAME
258
+ return [
259
+ (home_dir, AgentLocation.USER),
260
+ (project_dir, AgentLocation.PROJECT),
261
+ ]
262
+
263
+
264
+ def _agent_dir_for_location(location: AgentLocation) -> Path:
265
+ for path, loc in _agent_dirs():
266
+ if loc == location:
267
+ return path
268
+ raise ValueError(f"Unsupported agent location: {location}")
269
+
270
+
271
+ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
272
+ """Extract YAML frontmatter and body content."""
273
+ lines = raw_text.splitlines()
274
+ if len(lines) >= 3 and lines[0].strip() == "---":
275
+ for idx in range(1, len(lines)):
276
+ if lines[idx].strip() == "---":
277
+ frontmatter_text = "\n".join(lines[1:idx])
278
+ body = "\n".join(lines[idx + 1 :])
279
+ try:
280
+ frontmatter = yaml.safe_load(frontmatter_text) or {}
281
+ except (yaml.YAMLError, ValueError, TypeError) as exc: # pragma: no cover - defensive
282
+ logger.warning(
283
+ "Invalid frontmatter in agent file: %s: %s",
284
+ type(exc).__name__, exc,
285
+ extra={"error": str(exc)},
286
+ )
287
+ return {"__error__": f"Invalid frontmatter: {exc}"}, body
288
+ return frontmatter, body
289
+ return {}, raw_text
290
+
291
+
292
+ def _normalize_tools(value: object) -> List[str]:
293
+ if value is None:
294
+ return ["*"]
295
+ if isinstance(value, str):
296
+ return [item.strip() for item in value.split(",") if item.strip()] or ["*"]
297
+ if isinstance(value, Iterable):
298
+ tools: List[str] = []
299
+ for item in value:
300
+ if isinstance(item, str) and item.strip():
301
+ tools.append(item.strip())
302
+ return tools or ["*"]
303
+ return ["*"]
304
+
305
+
306
+ def _parse_agent_file(
307
+ path: Path, location: AgentLocation
308
+ ) -> Tuple[Optional[AgentDefinition], Optional[str]]:
309
+ """Parse a single agent file."""
310
+ try:
311
+ text = path.read_text(encoding="utf-8")
312
+ except (OSError, IOError, UnicodeDecodeError) as exc:
313
+ logger.warning(
314
+ "Failed to read agent file: %s: %s",
315
+ type(exc).__name__, exc,
316
+ extra={"error": str(exc), "path": str(path)},
317
+ )
318
+ return None, f"Failed to read agent file {path}: {exc}"
319
+
320
+ frontmatter, body = _split_frontmatter(text)
321
+ if "__error__" in frontmatter:
322
+ return None, str(frontmatter["__error__"])
323
+
324
+ agent_name = frontmatter.get("name")
325
+ description = frontmatter.get("description")
326
+ if not isinstance(agent_name, str) or not agent_name.strip():
327
+ return None, 'Missing required "name" field in frontmatter'
328
+ if not isinstance(description, str) or not description.strip():
329
+ return None, 'Missing required "description" field in frontmatter'
330
+
331
+ tools = _normalize_tools(frontmatter.get("tools"))
332
+ model_value = frontmatter.get("model")
333
+ color_value = frontmatter.get("color")
334
+ model = model_value if isinstance(model_value, str) else None
335
+ color = color_value if isinstance(color_value, str) else None
336
+
337
+ agent = AgentDefinition(
338
+ agent_type=agent_name.strip(),
339
+ when_to_use=description.replace("\\n", "\n").strip(),
340
+ tools=tools,
341
+ system_prompt=body.strip(),
342
+ location=location,
343
+ model=model,
344
+ color=color,
345
+ filename=path.stem,
346
+ )
347
+ return agent, None
348
+
349
+
350
+ def _load_agent_dir(
351
+ path: Path, location: AgentLocation
352
+ ) -> Tuple[List[AgentDefinition], List[Tuple[Path, str]]]:
353
+ agents: List[AgentDefinition] = []
354
+ errors: List[Tuple[Path, str]] = []
355
+ if not path.exists():
356
+ return agents, errors
357
+
358
+ for file_path in sorted(path.glob("*.md")):
359
+ agent, error = _parse_agent_file(file_path, location)
360
+ if agent:
361
+ agents.append(agent)
362
+ elif error:
363
+ errors.append((file_path, error))
364
+ return agents, errors
365
+
366
+
367
+ @lru_cache(maxsize=1)
368
+ def load_agent_definitions() -> AgentLoadResult:
369
+ """Load built-in, user, and project agents."""
370
+ built_ins = _built_in_agents()
371
+ collected_agents = list(built_ins)
372
+ errors: List[Tuple[Path, str]] = []
373
+
374
+ for directory, location in _agent_dirs():
375
+ loaded, dir_errors = _load_agent_dir(directory, location)
376
+ collected_agents.extend(loaded)
377
+ errors.extend(dir_errors)
378
+
379
+ agent_map: Dict[str, AgentDefinition] = {}
380
+ for agent in collected_agents:
381
+ agent_map[agent.agent_type] = agent
382
+
383
+ active_agents = list(agent_map.values())
384
+ return AgentLoadResult(
385
+ active_agents=active_agents,
386
+ all_agents=collected_agents,
387
+ failed_files=errors,
388
+ )
389
+
390
+
391
+ def clear_agent_cache() -> None:
392
+ """Reset cached agent definitions."""
393
+ load_agent_definitions.cache_clear() # type: ignore[attr-defined]
394
+
395
+
396
+ def summarize_agent(agent: AgentDefinition) -> str:
397
+ """Short human-readable summary."""
398
+ tool_label = "all tools" if "*" in agent.tools else ", ".join(agent.tools)
399
+ location = getattr(agent.location, "value", agent.location)
400
+ details = [f"tools: {tool_label}"]
401
+ if agent.model:
402
+ details.append(f"model: {agent.model}")
403
+ return f"- {agent.agent_type} ({location}): {agent.when_to_use} [{'; '.join(details)}]"
404
+
405
+
406
+ def resolve_agent_tools(
407
+ agent: AgentDefinition, available_tools: Iterable[object], task_tool_name: str
408
+ ) -> Tuple[List[object], List[str]]:
409
+ """Map tool names from an agent to Tool instances, filtering out the task tool itself."""
410
+ tool_map: Dict[str, object] = {}
411
+ ordered_tools: List[object] = []
412
+ for tool in available_tools:
413
+ name = getattr(tool, "name", None)
414
+ if not name:
415
+ continue
416
+ if name == task_tool_name:
417
+ continue
418
+ tool_map[name] = tool
419
+ ordered_tools.append(tool)
420
+
421
+ if "*" in agent.tools:
422
+ return ordered_tools, []
423
+
424
+ resolved: List[object] = []
425
+ missing: List[str] = []
426
+ seen = set()
427
+ for tool_name in agent.tools:
428
+ if tool_name in seen:
429
+ continue
430
+ seen.add(tool_name)
431
+ tool = tool_map.get(tool_name)
432
+ if tool:
433
+ resolved.append(tool)
434
+ else:
435
+ missing.append(tool_name)
436
+ return resolved, missing
437
+
438
+
439
+ def save_agent_definition(
440
+ agent_type: str,
441
+ description: str,
442
+ tools: List[str],
443
+ system_prompt: str,
444
+ location: AgentLocation = AgentLocation.USER,
445
+ model: Optional[str] = None,
446
+ color: Optional[str] = None,
447
+ overwrite: bool = False,
448
+ ) -> Path:
449
+ """Persist an agent markdown file."""
450
+ agent_dir = _agent_dir_for_location(location)
451
+ agent_dir.mkdir(parents=True, exist_ok=True)
452
+ target_path = agent_dir / f"{agent_type}.md"
453
+ if target_path.exists() and not overwrite:
454
+ raise FileExistsError(f"Agent file already exists: {target_path}")
455
+
456
+ escaped_description = description.replace("\n", "\\n")
457
+ lines = [
458
+ "---",
459
+ f"name: {agent_type}",
460
+ f"description: {escaped_description}",
461
+ ]
462
+
463
+ if not (len(tools) == 1 and tools[0] == "*"):
464
+ joined_tools = ", ".join(tools)
465
+ lines.append(f"tools: {joined_tools}")
466
+ if model:
467
+ lines.append(f"model: {model}")
468
+ if color:
469
+ lines.append(f"color: {color}")
470
+ lines.append("---")
471
+ lines.append("")
472
+ lines.append(system_prompt.strip())
473
+ target_path.write_text("\n".join(lines), encoding="utf-8")
474
+ clear_agent_cache()
475
+ return target_path
476
+
477
+
478
+ def delete_agent_definition(agent_type: str, location: AgentLocation = AgentLocation.USER) -> Path:
479
+ """Delete an agent markdown file."""
480
+ agent_dir = _agent_dir_for_location(location)
481
+ target_path = agent_dir / f"{agent_type}.md"
482
+ if target_path.exists():
483
+ target_path.unlink()
484
+ clear_agent_cache()
485
+ return target_path
486
+ raise FileNotFoundError(f"Agent file not found: {target_path}")
@@ -0,0 +1,33 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, List, Optional
3
+
4
+ from ripperdoc.cli.commands import list_slash_commands
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class CommandDef:
9
+ """Simple definition of a slash command."""
10
+
11
+ name: str
12
+ description: str
13
+
14
+
15
+ DEFAULT_COMMANDS: List[CommandDef] = []
16
+ for cmd in list_slash_commands():
17
+ DEFAULT_COMMANDS.append(CommandDef(cmd.name, cmd.description))
18
+ for alias in cmd.aliases:
19
+ DEFAULT_COMMANDS.append(CommandDef(alias, cmd.description))
20
+
21
+ COMMAND_LOOKUP: Dict[str, CommandDef] = {cmd.name: cmd for cmd in DEFAULT_COMMANDS}
22
+
23
+
24
+ def get_command(name: str) -> Optional[CommandDef]:
25
+ """Return a command definition by name."""
26
+
27
+ return COMMAND_LOOKUP.get(name)
28
+
29
+
30
+ def list_commands() -> List[CommandDef]:
31
+ """Return all available commands."""
32
+
33
+ return DEFAULT_COMMANDS