ripperdoc 0.2.9__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +379 -51
  3. ripperdoc/cli/commands/__init__.py +6 -0
  4. ripperdoc/cli/commands/agents_cmd.py +128 -5
  5. ripperdoc/cli/commands/clear_cmd.py +8 -0
  6. ripperdoc/cli/commands/doctor_cmd.py +29 -0
  7. ripperdoc/cli/commands/exit_cmd.py +1 -0
  8. ripperdoc/cli/commands/memory_cmd.py +2 -1
  9. ripperdoc/cli/commands/models_cmd.py +63 -7
  10. ripperdoc/cli/commands/resume_cmd.py +5 -0
  11. ripperdoc/cli/commands/skills_cmd.py +103 -0
  12. ripperdoc/cli/commands/stats_cmd.py +244 -0
  13. ripperdoc/cli/commands/status_cmd.py +10 -0
  14. ripperdoc/cli/commands/tasks_cmd.py +6 -3
  15. ripperdoc/cli/commands/themes_cmd.py +139 -0
  16. ripperdoc/cli/ui/file_mention_completer.py +63 -13
  17. ripperdoc/cli/ui/helpers.py +6 -3
  18. ripperdoc/cli/ui/interrupt_handler.py +34 -0
  19. ripperdoc/cli/ui/panels.py +14 -8
  20. ripperdoc/cli/ui/rich_ui.py +737 -47
  21. ripperdoc/cli/ui/spinner.py +93 -18
  22. ripperdoc/cli/ui/thinking_spinner.py +1 -2
  23. ripperdoc/cli/ui/tool_renderers.py +10 -9
  24. ripperdoc/cli/ui/wizard.py +24 -19
  25. ripperdoc/core/agents.py +14 -3
  26. ripperdoc/core/config.py +238 -6
  27. ripperdoc/core/default_tools.py +91 -10
  28. ripperdoc/core/hooks/events.py +4 -0
  29. ripperdoc/core/hooks/llm_callback.py +58 -0
  30. ripperdoc/core/hooks/manager.py +6 -0
  31. ripperdoc/core/permissions.py +160 -9
  32. ripperdoc/core/providers/openai.py +84 -28
  33. ripperdoc/core/query.py +489 -87
  34. ripperdoc/core/query_utils.py +17 -14
  35. ripperdoc/core/skills.py +1 -0
  36. ripperdoc/core/theme.py +298 -0
  37. ripperdoc/core/tool.py +15 -5
  38. ripperdoc/protocol/__init__.py +14 -0
  39. ripperdoc/protocol/models.py +300 -0
  40. ripperdoc/protocol/stdio.py +1453 -0
  41. ripperdoc/tools/background_shell.py +354 -139
  42. ripperdoc/tools/bash_tool.py +117 -22
  43. ripperdoc/tools/file_edit_tool.py +228 -50
  44. ripperdoc/tools/file_read_tool.py +154 -3
  45. ripperdoc/tools/file_write_tool.py +53 -11
  46. ripperdoc/tools/grep_tool.py +98 -8
  47. ripperdoc/tools/lsp_tool.py +609 -0
  48. ripperdoc/tools/multi_edit_tool.py +26 -3
  49. ripperdoc/tools/skill_tool.py +52 -1
  50. ripperdoc/tools/task_tool.py +539 -65
  51. ripperdoc/utils/conversation_compaction.py +1 -1
  52. ripperdoc/utils/file_watch.py +216 -7
  53. ripperdoc/utils/image_utils.py +125 -0
  54. ripperdoc/utils/log.py +30 -3
  55. ripperdoc/utils/lsp.py +812 -0
  56. ripperdoc/utils/mcp.py +80 -18
  57. ripperdoc/utils/message_formatting.py +7 -4
  58. ripperdoc/utils/messages.py +198 -33
  59. ripperdoc/utils/pending_messages.py +50 -0
  60. ripperdoc/utils/permissions/shell_command_validation.py +3 -3
  61. ripperdoc/utils/permissions/tool_permission_utils.py +180 -15
  62. ripperdoc/utils/platform.py +198 -0
  63. ripperdoc/utils/session_heatmap.py +242 -0
  64. ripperdoc/utils/session_history.py +2 -2
  65. ripperdoc/utils/session_stats.py +294 -0
  66. ripperdoc/utils/shell_utils.py +8 -5
  67. ripperdoc/utils/todo.py +0 -6
  68. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/METADATA +55 -17
  69. ripperdoc-0.3.0.dist-info/RECORD +136 -0
  70. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/WHEEL +1 -1
  71. ripperdoc/sdk/__init__.py +0 -9
  72. ripperdoc/sdk/client.py +0 -333
  73. ripperdoc-0.2.9.dist-info/RECORD +0 -123
  74. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/entry_points.txt +0 -0
  75. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/licenses/LICENSE +0 -0
  76. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,7 @@
1
1
  from rich.markup import escape
2
+ from rich import box
3
+ from rich.panel import Panel
4
+ from rich.table import Table
2
5
 
3
6
  from ripperdoc.core.agents import (
4
7
  AGENT_DIR_NAME,
@@ -8,14 +11,38 @@ from ripperdoc.core.agents import (
8
11
  save_agent_definition,
9
12
  )
10
13
  from ripperdoc.core.config import get_global_config
14
+ from ripperdoc.tools.task_tool import (
15
+ list_agent_runs,
16
+ get_agent_run_snapshot,
17
+ cancel_agent_run,
18
+ )
11
19
  from ripperdoc.utils.log import get_logger
12
20
 
13
- from typing import Any
21
+ from typing import Any, Dict
14
22
  from .base import SlashCommand
15
23
 
16
24
  logger = get_logger()
17
25
 
18
26
 
27
+ def _format_duration(duration_ms: float | None) -> str:
28
+ if duration_ms is None:
29
+ return "-"
30
+ try:
31
+ duration = float(duration_ms)
32
+ except (TypeError, ValueError):
33
+ return "-"
34
+ if duration < 1000:
35
+ return f"{int(duration)} ms"
36
+ seconds = duration / 1000.0
37
+ if seconds < 60:
38
+ return f"{seconds:.1f}s"
39
+ minutes, secs = divmod(int(seconds), 60)
40
+ if minutes < 60:
41
+ return f"{minutes}m {secs}s"
42
+ hours, mins = divmod(minutes, 60)
43
+ return f"{hours}h {mins}m"
44
+
45
+
19
46
  def _handle(ui: Any, trimmed_arg: str) -> bool:
20
47
  console = ui.console
21
48
  tokens = trimmed_arg.split()
@@ -39,18 +66,114 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
39
66
  "[bold]/agents delete <name> [location][/bold] — "
40
67
  "delete agent (location: user|project, default user)"
41
68
  )
69
+ console.print("[bold]/agents runs[/bold] — list subagent runs")
70
+ console.print("[bold]/agents show <id>[/bold] — show subagent run details")
71
+ console.print("[bold]/agents cancel <id>[/bold] — cancel a background subagent run")
42
72
  console.print(
43
73
  f"[dim]Agent files live in ~/.ripperdoc/{AGENT_DIR_NAME} "
44
74
  f"or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
45
75
  )
46
76
  console.print(
47
- "[dim]Model can be a profile name or pointer (task/main/etc). Defaults to 'task'.[/dim]"
77
+ "[dim]Model can be a profile name or pointer (main/quick). Defaults to 'main'.[/dim]"
48
78
  )
49
79
 
50
80
  if subcmd in ("help", "-h", "--help"):
51
81
  print_agents_usage()
52
82
  return True
53
83
 
84
+ if subcmd in ("runs", "run", "tasks", "status"):
85
+ console = ui.console
86
+ run_ids = list_agent_runs()
87
+ if not run_ids:
88
+ console.print(
89
+ Panel("No subagent runs recorded", title="Subagent runs", box=box.ROUNDED)
90
+ )
91
+ return True
92
+
93
+ table = Table(box=box.SIMPLE_HEAVY, expand=True)
94
+ table.add_column("ID", style="cyan", no_wrap=True)
95
+ table.add_column("Status", style="magenta", no_wrap=True)
96
+ table.add_column("Agent", style="white", no_wrap=True)
97
+ table.add_column("Duration", style="dim", no_wrap=True)
98
+ table.add_column("Background", style="dim", no_wrap=True)
99
+ table.add_column("Result", style="white")
100
+
101
+ for run_id in sorted(run_ids):
102
+ snapshot: Dict[Any, Any] = get_agent_run_snapshot(run_id) or {}
103
+ result_text = snapshot.get("result_text") or snapshot.get("error") or ""
104
+ result_preview = result_text if len(result_text) <= 80 else result_text[:77] + "..."
105
+ table.add_row(
106
+ escape(run_id),
107
+ escape(snapshot.get("status") or "unknown"),
108
+ escape(snapshot.get("agent_type") or "unknown"),
109
+ _format_duration(snapshot.get("duration_ms")),
110
+ "yes" if snapshot.get("is_background") else "no",
111
+ escape(result_preview),
112
+ )
113
+
114
+ console.print(
115
+ Panel(table, title="Subagent runs", box=box.ROUNDED, padding=(1, 2)),
116
+ markup=False,
117
+ )
118
+ console.print(
119
+ "[dim]Use /agents show <id> for details or /agents cancel <id> to stop a background run.[/dim]"
120
+ )
121
+ return True
122
+
123
+ if subcmd in ("show", "info", "details"):
124
+ if len(tokens) < 2:
125
+ console.print("[red]Usage: /agents show <id>[/red]")
126
+ return True
127
+ run_id = tokens[1]
128
+ snapshot = get_agent_run_snapshot(run_id) # type: ignore[assignment]
129
+ if not snapshot:
130
+ console.print(f"[red]No subagent run found with id '{escape(run_id)}'.[/red]")
131
+ return True
132
+ details = Table(box=box.SIMPLE_HEAVY, show_header=False)
133
+ details.add_row("ID", escape(run_id))
134
+ details.add_row("Status", escape(snapshot.get("status") or "unknown"))
135
+ details.add_row("Agent", escape(snapshot.get("agent_type") or "unknown"))
136
+ details.add_row("Duration", _format_duration(snapshot.get("duration_ms")))
137
+ details.add_row("Background", "yes" if snapshot.get("is_background") else "no")
138
+ if snapshot.get("model_used"):
139
+ details.add_row("Model", escape(str(snapshot.get("model_used"))))
140
+ if snapshot.get("tool_use_count"):
141
+ details.add_row("Tool uses", str(snapshot.get("tool_use_count")))
142
+ if snapshot.get("missing_tools"):
143
+ details.add_row("Missing tools", escape(", ".join(snapshot["missing_tools"])))
144
+ if snapshot.get("error"):
145
+ details.add_row("Error", escape(str(snapshot.get("error"))))
146
+ console.print(
147
+ Panel(details, title=f"Subagent {escape(run_id)}", box=box.ROUNDED, padding=(1, 2)),
148
+ markup=False,
149
+ )
150
+ result_text = snapshot.get("result_text")
151
+ if result_text:
152
+ console.print(Panel(escape(result_text), title="Result", box=box.SIMPLE))
153
+ return True
154
+
155
+ if subcmd in ("cancel", "kill", "stop"):
156
+ if len(tokens) < 2:
157
+ console.print("[red]Usage: /agents cancel <id>[/red]")
158
+ return True
159
+ run_id = tokens[1]
160
+ runner = getattr(ui, "run_async", None)
161
+ try:
162
+ if callable(runner):
163
+ cancelled = runner(cancel_agent_run(run_id))
164
+ else:
165
+ import asyncio
166
+
167
+ cancelled = asyncio.run(cancel_agent_run(run_id))
168
+ except (OSError, RuntimeError, ValueError) as exc:
169
+ console.print(f"[red]Failed to cancel '{escape(run_id)}': {escape(str(exc))}[/red]")
170
+ return True
171
+ if cancelled:
172
+ console.print(f"[green]Cancelled subagent {escape(run_id)}[/green]")
173
+ else:
174
+ console.print(f"[yellow]No running subagent found for '{escape(run_id)}'.[/yellow]")
175
+ return True
176
+
54
177
  if subcmd in ("create", "add"):
55
178
  agent_name = tokens[1] if len(tokens) > 1 else console.input("Agent name: ").strip()
56
179
  if not agent_name:
@@ -84,7 +207,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
84
207
 
85
208
  config = get_global_config()
86
209
  pointer_map = config.model_pointers.model_dump()
87
- default_model_value = model_arg or pointer_map.get("task", "task")
210
+ default_model_value = model_arg or pointer_map.get("main", "main")
88
211
  model_input = (
89
212
  console.input(f"Model profile or pointer [{default_model_value}]: ").strip()
90
213
  or default_model_value
@@ -205,7 +328,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
205
328
 
206
329
  config = get_global_config()
207
330
  pointer_map = config.model_pointers.model_dump()
208
- model_default = target_agent.model or pointer_map.get("task", "task")
331
+ model_default = target_agent.model or pointer_map.get("main", "main")
209
332
  model_input = (
210
333
  console.input(f"Model profile or pointer [{model_default}]: ").strip() or model_default
211
334
  )
@@ -245,7 +368,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
245
368
  console.print(f" • {escape(agent.agent_type)} ({escape(str(location))})", markup=False)
246
369
  console.print(f" {escape(agent.when_to_use)}", markup=False)
247
370
  console.print(f" tools: {escape(tools_str)}", markup=False)
248
- console.print(f" model: {escape(agent.model or 'task (default)')}", markup=False)
371
+ console.print(f" model: {escape(agent.model or 'main (default)')}", markup=False)
249
372
  if agents.failed_files:
250
373
  console.print("[yellow]Some agent files could not be loaded:[/yellow]")
251
374
  for path, error in agents.failed_files:
@@ -3,8 +3,16 @@ from .base import SlashCommand
3
3
 
4
4
 
5
5
  def _handle(ui: Any, _: str) -> bool:
6
+ try:
7
+ ui._run_session_end("clear")
8
+ except (AttributeError, RuntimeError, ValueError):
9
+ pass
6
10
  ui.conversation_messages = []
7
11
  ui.console.print("[green]✓ Conversation cleared[/green]")
12
+ try:
13
+ ui._run_session_start("clear")
14
+ except (AttributeError, RuntimeError, ValueError):
15
+ pass
8
16
  return True
9
17
 
10
18
 
@@ -15,6 +15,8 @@ from ripperdoc.core.config import (
15
15
  api_key_env_candidates,
16
16
  get_global_config,
17
17
  get_project_config,
18
+ get_ripperdoc_env_status,
19
+ has_ripperdoc_env_overrides,
18
20
  )
19
21
  from ripperdoc.cli.ui.helpers import get_profile_for_pointer
20
22
  from ripperdoc.utils.log import get_logger
@@ -41,6 +43,12 @@ def _api_key_status(provider: ProviderType, profile_key: Optional[str]) -> Tuple
41
43
  """Check API key presence and source."""
42
44
  import os
43
45
 
46
+ # 首先检查全局 RIPPERDOC_API_KEY
47
+ if ripperdoc_api_key := os.getenv("RIPPERDOC_API_KEY"):
48
+ masked = ripperdoc_api_key[:4] + "…" if len(ripperdoc_api_key) > 4 else "set"
49
+ return ("ok", f"Found in $RIPPERDOC_API_KEY ({masked})")
50
+
51
+ # 然后检查 provider 特定的环境变量
44
52
  for env_var in api_key_env_candidates(provider):
45
53
  if os.environ.get(env_var):
46
54
  masked = os.environ[env_var]
@@ -186,6 +194,23 @@ def _project_status(project_path: Path) -> Tuple[str, str, str]:
186
194
  )
187
195
 
188
196
 
197
+ def _ripperdoc_env_status() -> List[Tuple[str, str, str]]:
198
+ """Check RIPPERDOC_* environment variable overrides."""
199
+ rows: List[Tuple[str, str, str]] = []
200
+
201
+ if not has_ripperdoc_env_overrides():
202
+ rows.append(_status_row("Env overrides", "ok", "No RIPPERDOC_* overrides active"))
203
+ return rows
204
+
205
+ rows.append(_status_row("Env overrides", "ok", "RIPPERDOC_* variables detected"))
206
+
207
+ status = get_ripperdoc_env_status()
208
+ for key, value in status.items():
209
+ rows.append(_status_row("", "ok", f" {key}: {value}"))
210
+
211
+ return rows
212
+
213
+
189
214
  def _render_table(console: Any, rows: List[Tuple[str, str, str]]) -> None:
190
215
  table = Table(show_header=True, header_style="bold cyan")
191
216
  table.add_column("Check")
@@ -202,6 +227,10 @@ def _handle(ui: Any, _: str) -> bool:
202
227
 
203
228
  results.append(_onboarding_status())
204
229
  results.extend(_model_status(project_path))
230
+
231
+ # 检查 RIPPERDOC_* 环境变量
232
+ results.extend(_ripperdoc_env_status())
233
+
205
234
  project_row = _project_status(project_path)
206
235
  results.append(project_row)
207
236
 
@@ -4,6 +4,7 @@ from .base import SlashCommand
4
4
 
5
5
  def _handle(ui: Any, _: str) -> bool:
6
6
  ui.console.print("[yellow]Goodbye![/yellow]")
7
+ ui._exit_reason = "prompt_input_exit"
7
8
  ui._should_exit = True
8
9
  return True
9
10
 
@@ -18,6 +18,7 @@ from ripperdoc.utils.memory import (
18
18
  MEMORY_FILE_NAME,
19
19
  collect_all_memory_files,
20
20
  )
21
+ from ripperdoc.utils.platform import is_windows
21
22
 
22
23
  from .base import SlashCommand
23
24
 
@@ -89,7 +90,7 @@ def _determine_editor_command() -> Optional[List[str]]:
89
90
  return shlex.split(value)
90
91
 
91
92
  candidates = ["code", "nano", "vim", "vi"]
92
- if os.name == "nt":
93
+ if is_windows():
93
94
  candidates.insert(0, "notepad")
94
95
 
95
96
  for candidate in candidates:
@@ -36,7 +36,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
36
36
  console.print("[bold]/models delete <name>[/bold] — delete a model profile")
37
37
  console.print("[bold]/models use <name>[/bold] — set the main model pointer")
38
38
  console.print(
39
- "[bold]/models use <pointer> <name>[/bold] — set a specific pointer (main/task/reasoning/quick)"
39
+ "[bold]/models use <pointer> <name>[/bold] — set a specific pointer (main/quick)"
40
40
  )
41
41
 
42
42
  def parse_int(prompt_text: str, default_value: Optional[int]) -> Optional[int]:
@@ -155,12 +155,34 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
155
155
  context_prompt += "): "
156
156
  context_window = parse_int(context_prompt, context_window_default)
157
157
 
158
+ # Vision support prompt
159
+ supports_vision_default = existing_profile.supports_vision if existing_profile else None
160
+ supports_vision = None
161
+ vision_default_display = (
162
+ "auto"
163
+ if supports_vision_default is None
164
+ else ("yes" if supports_vision_default else "no")
165
+ )
166
+ supports_vision_input = (
167
+ console.input(f"Supports vision (images)? [{vision_default_display}] (Y/n/auto): ")
168
+ .strip()
169
+ .lower()
170
+ )
171
+ if supports_vision_input in ("y", "yes"):
172
+ supports_vision = True
173
+ elif supports_vision_input in ("n", "no"):
174
+ supports_vision = False
175
+ elif supports_vision_input in ("auto", ""):
176
+ supports_vision = None
177
+ else:
178
+ supports_vision = supports_vision_default
179
+
158
180
  default_set_main = (
159
181
  not config.model_profiles
160
182
  or getattr(config.model_pointers, "main", "") not in config.model_profiles
161
183
  )
162
184
  set_main_input = (
163
- console.input(f"Set as main model? [{'Y' if default_set_main else 'y'}/N]: ")
185
+ console.input(f"Set as main model? ({'Y' if default_set_main else 'y'}/N): ")
164
186
  .strip()
165
187
  .lower()
166
188
  )
@@ -175,6 +197,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
175
197
  temperature=temperature,
176
198
  context_window=context_window,
177
199
  auth_token=auth_token,
200
+ supports_vision=supports_vision,
178
201
  )
179
202
 
180
203
  try:
@@ -276,6 +299,31 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
276
299
  existing_profile.context_window,
277
300
  )
278
301
 
302
+ # Vision support prompt
303
+ vision_default_display = (
304
+ "auto"
305
+ if existing_profile.supports_vision is None
306
+ else ("yes" if existing_profile.supports_vision else "no")
307
+ )
308
+ supports_vision_input = (
309
+ console.input(
310
+ f"Supports vision (images)? [{vision_default_display}] (Y/n/auto/C=clear): "
311
+ )
312
+ .strip()
313
+ .lower()
314
+ )
315
+ supports_vision = None
316
+ if supports_vision_input in ("y", "yes"):
317
+ supports_vision = True
318
+ elif supports_vision_input in ("n", "no"):
319
+ supports_vision = False
320
+ elif supports_vision_input in ("c", "clear", "-"):
321
+ supports_vision = None
322
+ elif supports_vision_input in ("auto", ""):
323
+ supports_vision = existing_profile.supports_vision
324
+ else:
325
+ supports_vision = existing_profile.supports_vision
326
+
279
327
  updated_profile = ModelProfile(
280
328
  provider=provider,
281
329
  model=model_name,
@@ -285,6 +333,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
285
333
  temperature=temperature,
286
334
  context_window=context_window,
287
335
  auth_token=auth_token,
336
+ supports_vision=supports_vision,
288
337
  )
289
338
 
290
339
  try:
@@ -331,7 +380,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
331
380
 
332
381
  if subcmd in ("use", "main", "set-main"):
333
382
  # Support both "/models use <profile>" and "/models use <pointer> <profile>"
334
- valid_pointers = {"main", "task", "reasoning", "quick"}
383
+ valid_pointers = {"main", "quick"}
335
384
 
336
385
  if len(tokens) >= 3:
337
386
  # /models use <pointer> <profile>
@@ -353,10 +402,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
353
402
  pointer = "main"
354
403
  target = tokens[1]
355
404
  else:
356
- pointer = (
357
- console.input("Pointer (main/task/reasoning/quick) [main]: ").strip().lower()
358
- or "main"
359
- )
405
+ pointer = console.input("Pointer (main/quick) [main]: ").strip().lower() or "main"
360
406
  if pointer not in valid_pointers:
361
407
  console.print(
362
408
  f"[red]Invalid pointer '{escape(pointer)}'. Valid pointers: {', '.join(valid_pointers)}[/red]"
@@ -415,6 +461,16 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
415
461
  )
416
462
  if profile.openai_tool_mode:
417
463
  console.print(f" openai_tool_mode: {profile.openai_tool_mode}", markup=False)
464
+ if profile.thinking_mode:
465
+ console.print(f" thinking_mode: {profile.thinking_mode}", markup=False)
466
+ # Display vision support
467
+ if profile.supports_vision is None:
468
+ vision_display = "auto-detect"
469
+ elif profile.supports_vision:
470
+ vision_display = "yes"
471
+ else:
472
+ vision_display = "no"
473
+ console.print(f" supports_vision: {vision_display}", markup=False)
418
474
  pointer_labels = ", ".join(f"{p}->{v or '-'}" for p, v in pointer_map.items())
419
475
  console.print(f"[dim]Pointers: {escape(pointer_labels)}[/dim]")
420
476
  return True
@@ -116,6 +116,10 @@ def _handle(ui: Any, arg: str) -> bool:
116
116
  ui.conversation_messages = messages
117
117
  ui._saved_conversation = None
118
118
  ui._set_session(summary.session_id)
119
+ try:
120
+ ui._run_session_start("resume")
121
+ except (AttributeError, RuntimeError, ValueError):
122
+ pass
119
123
  ui.replay_conversation(messages)
120
124
  ui.console.print(
121
125
  f"[green]✓ Resumed session {escape(summary.session_id)} "
@@ -128,6 +132,7 @@ command = SlashCommand(
128
132
  name="resume",
129
133
  description="Resume a previous session conversation",
130
134
  handler=_handle,
135
+ aliases=("sessions",),
131
136
  )
132
137
 
133
138
 
@@ -0,0 +1,103 @@
1
+ from rich.markup import escape
2
+
3
+ from ripperdoc.core.skills import (
4
+ SkillDefinition,
5
+ SkillLoadResult,
6
+ SkillLocation,
7
+ load_all_skills,
8
+ skill_directories,
9
+ )
10
+
11
+ from typing import Any
12
+ from .base import SlashCommand
13
+
14
+
15
+ def _handle(ui: Any, _: str) -> bool:
16
+ console = ui.console
17
+ project_path = getattr(ui, "project_path", None)
18
+
19
+ result: SkillLoadResult = load_all_skills(project_path=project_path)
20
+
21
+ if not result.skills:
22
+ dirs = skill_directories(project_path=project_path)
23
+ dir_paths = [f"'{d}'" for d, _ in dirs]
24
+ console.print("[yellow]No skills found.[/yellow]")
25
+ console.print(
26
+ f"\n[bold]Create skills in:[/bold]\n"
27
+ f" • User: {dir_paths[0]}\n"
28
+ f" • Project: {dir_paths[1]}\n"
29
+ f"\n[dim]Each skill needs a SKILL.md file with YAML frontmatter:\n"
30
+ f"---\n"
31
+ f"name: my-skill\n"
32
+ f"description: A helpful skill\n"
33
+ f"---\n\n"
34
+ f"Skill content goes here...[/dim]"
35
+ )
36
+ if result.errors:
37
+ console.print("\n[bold red]Errors encountered while loading skills:[/bold red]")
38
+ for error in result.errors:
39
+ console.print(f" • {error.path}: {escape(error.reason)}", markup=False)
40
+ return True
41
+
42
+ console.print("\n[bold]Skills[/bold]")
43
+
44
+ # Group skills by location for better organization
45
+ user_skills = [s for s in result.skills if s.location == SkillLocation.USER]
46
+ project_skills = [s for s in result.skills if s.location == SkillLocation.PROJECT]
47
+ other_skills = [s for s in result.skills if s.location == SkillLocation.OTHER]
48
+
49
+ def print_skill(skill: SkillDefinition) -> None:
50
+ location_tag = f"[dim]({skill.location.value})[/dim]" if skill.location else ""
51
+ console.print(f"\n[bold cyan]{escape(skill.name)}[/bold cyan] {location_tag}")
52
+ console.print(f" {escape(skill.description)}")
53
+
54
+ if skill.allowed_tools:
55
+ tools_str = ", ".join(escape(t) for t in skill.allowed_tools)
56
+ console.print(f" Tools: {tools_str}")
57
+
58
+ if skill.model:
59
+ console.print(f" Model: {escape(skill.model)}")
60
+
61
+ if skill.max_thinking_tokens:
62
+ console.print(f" Max thinking tokens: {skill.max_thinking_tokens}")
63
+
64
+ if skill.skill_type != "prompt":
65
+ console.print(f" Type: {escape(skill.skill_type)}")
66
+
67
+ if skill.disable_model_invocation:
68
+ console.print(" [yellow]Model invocation disabled[/yellow]")
69
+
70
+ console.print(f" Path: {escape(str(skill.path))}", markup=False)
71
+
72
+ # Print project skills first (they have priority), then user skills
73
+ if project_skills:
74
+ console.print("\n[bold]Project skills:[/bold]")
75
+ for skill in project_skills:
76
+ print_skill(skill)
77
+
78
+ if user_skills:
79
+ console.print("\n[bold]User skills:[/bold]")
80
+ for skill in user_skills:
81
+ print_skill(skill)
82
+
83
+ if other_skills:
84
+ console.print("\n[bold]Other skills:[/bold]")
85
+ for skill in other_skills:
86
+ print_skill(skill)
87
+
88
+ if result.errors:
89
+ console.print("\n[bold red]Errors encountered while loading skills:[/bold red]")
90
+ for error in result.errors:
91
+ console.print(f" • {error.path}: {escape(error.reason)}", markup=False)
92
+
93
+ return True
94
+
95
+
96
+ command = SlashCommand(
97
+ name="skills",
98
+ description="List available skills",
99
+ handler=_handle,
100
+ )
101
+
102
+
103
+ __all__ = ["command"]