ripperdoc 0.2.0__tar.gz → 0.2.4__tar.gz

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 (123) hide show
  1. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/PKG-INFO +6 -2
  2. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/README.md +2 -1
  3. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/pyproject.toml +9 -1
  4. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/__init__.py +1 -1
  5. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/cli.py +74 -9
  6. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/__init__.py +4 -0
  7. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/agents_cmd.py +30 -4
  8. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/context_cmd.py +14 -4
  9. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/cost_cmd.py +5 -0
  10. ripperdoc-0.2.4/ripperdoc/cli/commands/doctor_cmd.py +208 -0
  11. ripperdoc-0.2.4/ripperdoc/cli/commands/memory_cmd.py +202 -0
  12. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/models_cmd.py +61 -6
  13. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/resume_cmd.py +4 -2
  14. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/status_cmd.py +1 -1
  15. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/tasks_cmd.py +27 -0
  16. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/ui/rich_ui.py +291 -11
  17. ripperdoc-0.2.4/ripperdoc/cli/ui/thinking_spinner.py +128 -0
  18. ripperdoc-0.2.4/ripperdoc/core/agents.py +478 -0
  19. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/core/config.py +56 -3
  20. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/core/default_tools.py +22 -2
  21. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/core/permissions.py +19 -0
  22. ripperdoc-0.2.4/ripperdoc/core/providers/__init__.py +47 -0
  23. ripperdoc-0.2.4/ripperdoc/core/providers/anthropic.py +147 -0
  24. ripperdoc-0.2.4/ripperdoc/core/providers/base.py +236 -0
  25. ripperdoc-0.2.4/ripperdoc/core/providers/gemini.py +496 -0
  26. ripperdoc-0.2.4/ripperdoc/core/providers/openai.py +253 -0
  27. ripperdoc-0.2.4/ripperdoc/core/query.py +814 -0
  28. ripperdoc-0.2.4/ripperdoc/core/query_utils.py +578 -0
  29. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/core/system_prompt.py +69 -62
  30. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/core/tool.py +23 -1
  31. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/sdk/client.py +12 -1
  32. ripperdoc-0.2.4/ripperdoc/tools/ask_user_question_tool.py +433 -0
  33. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/background_shell.py +113 -21
  34. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/bash_tool.py +48 -13
  35. ripperdoc-0.2.4/ripperdoc/tools/enter_plan_mode_tool.py +223 -0
  36. ripperdoc-0.2.4/ripperdoc/tools/exit_plan_mode_tool.py +150 -0
  37. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/file_edit_tool.py +20 -0
  38. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/file_read_tool.py +23 -0
  39. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/file_write_tool.py +20 -0
  40. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/glob_tool.py +59 -15
  41. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/grep_tool.py +7 -0
  42. ripperdoc-0.2.4/ripperdoc/tools/ls_tool.py +471 -0
  43. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/mcp_tools.py +143 -12
  44. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/multi_edit_tool.py +23 -0
  45. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/notebook_edit_tool.py +18 -3
  46. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/task_tool.py +95 -5
  47. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/todo_tool.py +157 -25
  48. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/tool_search_tool.py +17 -4
  49. ripperdoc-0.2.4/ripperdoc/utils/file_watch.py +134 -0
  50. ripperdoc-0.2.4/ripperdoc/utils/git_utils.py +274 -0
  51. ripperdoc-0.2.4/ripperdoc/utils/json_utils.py +27 -0
  52. ripperdoc-0.2.4/ripperdoc/utils/log.py +176 -0
  53. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/mcp.py +120 -16
  54. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/memory.py +12 -1
  55. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/message_compaction.py +25 -10
  56. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/messages.py +72 -17
  57. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/output_utils.py +34 -9
  58. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/permissions/path_validation_utils.py +6 -0
  59. ripperdoc-0.2.4/ripperdoc/utils/prompt.py +17 -0
  60. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/safe_get_cwd.py +4 -0
  61. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/session_history.py +27 -9
  62. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/session_usage.py +7 -0
  63. ripperdoc-0.2.4/ripperdoc/utils/shell_utils.py +159 -0
  64. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/todo.py +2 -2
  65. ripperdoc-0.2.4/ripperdoc/utils/token_estimation.py +33 -0
  66. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc.egg-info/PKG-INFO +6 -2
  67. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc.egg-info/SOURCES.txt +22 -0
  68. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc.egg-info/requires.txt +3 -0
  69. ripperdoc-0.2.4/tests/test_background_shell_shutdown.py +90 -0
  70. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/tests/test_config.py +10 -1
  71. ripperdoc-0.2.4/tests/test_mcp_config.py +18 -0
  72. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/tests/test_messages.py +53 -0
  73. ripperdoc-0.2.4/tests/test_output_utils.py +23 -0
  74. ripperdoc-0.2.4/tests/test_query_abort.py +155 -0
  75. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/tests/test_tools.py +7 -7
  76. ripperdoc-0.2.0/ripperdoc/core/agents.py +0 -308
  77. ripperdoc-0.2.0/ripperdoc/core/query.py +0 -684
  78. ripperdoc-0.2.0/ripperdoc/tools/ls_tool.py +0 -298
  79. ripperdoc-0.2.0/ripperdoc/utils/log.py +0 -76
  80. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/LICENSE +0 -0
  81. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/__main__.py +0 -0
  82. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/__init__.py +0 -0
  83. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/base.py +0 -0
  84. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/clear_cmd.py +0 -0
  85. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/compact_cmd.py +0 -0
  86. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/config_cmd.py +0 -0
  87. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/exit_cmd.py +0 -0
  88. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/help_cmd.py +0 -0
  89. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/mcp_cmd.py +0 -0
  90. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/todos_cmd.py +0 -0
  91. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/commands/tools_cmd.py +0 -0
  92. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/ui/__init__.py +0 -0
  93. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/ui/context_display.py +0 -0
  94. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/ui/helpers.py +0 -0
  95. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/cli/ui/spinner.py +0 -0
  96. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/core/__init__.py +0 -0
  97. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/core/commands.py +0 -0
  98. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/sdk/__init__.py +0 -0
  99. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/__init__.py +0 -0
  100. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/bash_output_tool.py +0 -0
  101. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/tools/kill_bash_tool.py +0 -0
  102. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/__init__.py +0 -0
  103. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/bash_constants.py +0 -0
  104. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/bash_output_utils.py +0 -0
  105. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/exit_code_handlers.py +0 -0
  106. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/path_utils.py +0 -0
  107. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/permissions/__init__.py +0 -0
  108. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/permissions/shell_command_validation.py +0 -0
  109. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/permissions/tool_permission_utils.py +0 -0
  110. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/sandbox_utils.py +0 -0
  111. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc/utils/shell_token_utils.py +0 -0
  112. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc.egg-info/dependency_links.txt +0 -0
  113. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc.egg-info/entry_points.txt +0 -0
  114. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/ripperdoc.egg-info/top_level.txt +0 -0
  115. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/setup.cfg +0 -0
  116. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/setup.py +0 -0
  117. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/tests/test_cli_commands.py +0 -0
  118. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/tests/test_context_limits.py +0 -0
  119. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/tests/test_permissions.py +0 -0
  120. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/tests/test_sdk.py +0 -0
  121. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/tests/test_shell_permissions.py +0 -0
  122. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/tests/test_todo.py +0 -0
  123. {ripperdoc-0.2.0 → ripperdoc-0.2.4}/tests/test_tool_search.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripperdoc
3
- Version: 0.2.0
3
+ Version: 0.2.4
4
4
  Summary: AI-powered terminal assistant for coding tasks
5
5
  Author: Ripperdoc Team
6
6
  License: Apache-2.0
@@ -24,6 +24,9 @@ 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
28
+ Requires-Dist: tiktoken>=0.7.0
29
+ Requires-Dist: google-genai>=0.3.0
27
30
  Provides-Extra: dev
28
31
  Requires-Dist: pytest>=7.0.0; extra == "dev"
29
32
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -37,10 +40,12 @@ Dynamic: license-file
37
40
  Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an interactive interface for AI-assisted development, file management, and command execution.
38
41
 
39
42
  [中文文档](README_CN.md) | [Contributing](CONTRIBUTING.md) | [Documentation](docs/)
43
+
40
44
  ## Features
41
45
 
42
46
  - **AI-Powered Assistance** - Uses AI models to understand and respond to coding requests
43
47
  - **Multi-Model Support** - Support for Anthropic Claude and OpenAI models
48
+ - **Rich UI** - Beautiful terminal interface with syntax highlighting
44
49
  - **Code Editing** - Directly edit files with intelligent suggestions
45
50
  - **Codebase Understanding** - Analyzes project structure and code relationships
46
51
  - **Command Execution** - Run shell commands with real-time feedback
@@ -52,7 +57,6 @@ Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an int
52
57
  - **Permission System** - Safe mode with permission prompts for operations
53
58
  - **Multi-Edit Support** - Batch edit operations on files
54
59
  - **MCP Server Support** - Integration with Model Context Protocol servers
55
- - **Subagent System** - Delegate tasks to specialized agents
56
60
  - **Session Management** - Persistent session history and usage tracking
57
61
  - **Jupyter Notebook Support** - Edit .ipynb files directly
58
62
 
@@ -3,10 +3,12 @@
3
3
  Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an interactive interface for AI-assisted development, file management, and command execution.
4
4
 
5
5
  [中文文档](README_CN.md) | [Contributing](CONTRIBUTING.md) | [Documentation](docs/)
6
+
6
7
  ## Features
7
8
 
8
9
  - **AI-Powered Assistance** - Uses AI models to understand and respond to coding requests
9
10
  - **Multi-Model Support** - Support for Anthropic Claude and OpenAI models
11
+ - **Rich UI** - Beautiful terminal interface with syntax highlighting
10
12
  - **Code Editing** - Directly edit files with intelligent suggestions
11
13
  - **Codebase Understanding** - Analyzes project structure and code relationships
12
14
  - **Command Execution** - Run shell commands with real-time feedback
@@ -18,7 +20,6 @@ Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an int
18
20
  - **Permission System** - Safe mode with permission prompts for operations
19
21
  - **Multi-Edit Support** - Batch edit operations on files
20
22
  - **MCP Server Support** - Integration with Model Context Protocol servers
21
- - **Subagent System** - Delegate tasks to specialized agents
22
23
  - **Session Management** - Persistent session history and usage tracking
23
24
  - **Jupyter Notebook Support** - Edit .ipynb files directly
24
25
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ripperdoc"
7
- version = "0.2.0"
7
+ dynamic = ["version"]
8
8
  description = "AI-powered terminal assistant for coding tasks"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -33,6 +33,9 @@ dependencies = [
33
33
  "prompt-toolkit>=3.0.0",
34
34
  "PyYAML>=6.0.0",
35
35
  "mcp[cli]>=1.22.0",
36
+ "json_repair>=0.54.2",
37
+ "tiktoken>=0.7.0",
38
+ "google-genai>=0.3.0",
36
39
  ]
37
40
 
38
41
  [project.optional-dependencies]
@@ -48,6 +51,9 @@ dev = [
48
51
  ripperdoc = "ripperdoc.cli.cli:main"
49
52
  rd = "ripperdoc.cli.cli:main"
50
53
 
54
+ [tool.setuptools.dynamic]
55
+ version = {attr = "ripperdoc.__version__"}
56
+
51
57
  [tool.setuptools.packages.find]
52
58
  where = ["."]
53
59
  include = ["ripperdoc*"]
@@ -61,6 +67,8 @@ python_version = "3.10"
61
67
  warn_return_any = true
62
68
  warn_unused_configs = true
63
69
  disallow_untyped_defs = true
70
+ files = ["ripperdoc"]
71
+ exclude = ["^tests/"]
64
72
 
65
73
  [tool.ruff]
66
74
  line-length = 100
@@ -1,3 +1,3 @@
1
1
  """Ripperdoc - AI-powered coding agent."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.4"
@@ -6,6 +6,7 @@ This module provides the command-line interface for the Ripperdoc agent.
6
6
  import asyncio
7
7
  import click
8
8
  import sys
9
+ import uuid
9
10
  from pathlib import Path
10
11
  from typing import Any, Dict, List, Optional
11
12
 
@@ -29,6 +30,8 @@ from ripperdoc.utils.mcp import (
29
30
  shutdown_mcp_runtime,
30
31
  )
31
32
  from ripperdoc.tools.mcp_tools import load_dynamic_mcp_tools_async, merge_tools_with_dynamic
33
+ from ripperdoc.utils.log import enable_session_file_logging, get_logger
34
+ from ripperdoc.utils.prompt import prompt_secret
32
35
 
33
36
  from rich.console import Console
34
37
  from rich.markdown import Markdown
@@ -36,13 +39,33 @@ from rich.panel import Panel
36
39
  from rich.markup import escape
37
40
 
38
41
  console = Console()
42
+ logger = get_logger()
39
43
 
40
44
 
41
45
  async def run_query(
42
- prompt: str, tools: list, safe_mode: bool = False, verbose: bool = False
46
+ prompt: str,
47
+ tools: list,
48
+ safe_mode: bool = False,
49
+ verbose: bool = False,
50
+ session_id: Optional[str] = None,
43
51
  ) -> None:
44
52
  """Run a single query and print the response."""
45
53
 
54
+ logger.info(
55
+ "[cli] Running single prompt session",
56
+ extra={
57
+ "safe_mode": safe_mode,
58
+ "verbose": verbose,
59
+ "session_id": session_id,
60
+ "prompt_length": len(prompt),
61
+ },
62
+ )
63
+ if prompt:
64
+ logger.debug(
65
+ "[cli] Prompt preview",
66
+ extra={"session_id": session_id, "prompt_preview": prompt[:200]},
67
+ )
68
+
46
69
  project_path = Path.cwd()
47
70
  can_use_tool = make_permission_checker(project_path, safe_mode) if safe_mode else None
48
71
 
@@ -125,12 +148,20 @@ async def run_query(
125
148
  console.print("\n[yellow]Interrupted by user[/yellow]")
126
149
  except Exception as e:
127
150
  console.print(f"[red]Error: {escape(str(e))}[/red]")
151
+ logger.exception(
152
+ "[cli] Unhandled error while running prompt", extra={"session_id": session_id}
153
+ )
128
154
  if verbose:
129
155
  import traceback
130
156
 
131
157
  console.print(traceback.format_exc(), markup=False)
158
+ logger.info(
159
+ "[cli] Prompt session completed",
160
+ extra={"session_id": session_id, "message_count": len(messages)},
161
+ )
132
162
  finally:
133
163
  await shutdown_mcp_runtime()
164
+ logger.debug("[cli] Shutdown MCP runtime", extra={"session_id": session_id})
134
165
 
135
166
 
136
167
  def check_onboarding() -> bool:
@@ -169,7 +200,11 @@ def check_onboarding() -> bool:
169
200
  )
170
201
  api_base = click.prompt("API Base URL")
171
202
 
172
- api_key = click.prompt("Enter your API key", hide_input=True)
203
+ api_key = ""
204
+ while not api_key:
205
+ api_key = prompt_secret("Enter your API key").strip()
206
+ if not api_key:
207
+ console.print("[red]API key is required.[/red]")
173
208
 
174
209
  provider = ProviderType(provider_choice)
175
210
 
@@ -243,27 +278,51 @@ def cli(
243
278
  ctx: click.Context, cwd: Optional[str], unsafe: bool, verbose: bool, prompt: Optional[str]
244
279
  ) -> None:
245
280
  """Ripperdoc - AI-powered coding agent"""
246
-
247
- # Ensure onboarding is complete
248
- if not check_onboarding():
249
- sys.exit(1)
281
+ session_id = str(uuid.uuid4())
250
282
 
251
283
  # Set working directory
252
284
  if cwd:
253
285
  import os
254
286
 
255
287
  os.chdir(cwd)
288
+ logger.debug(
289
+ "[cli] Changed working directory via --cwd",
290
+ extra={"cwd": cwd, "session_id": session_id},
291
+ )
256
292
 
257
- # Initialize project configuration for the current working directory
258
293
  project_path = Path.cwd()
294
+ log_file = enable_session_file_logging(project_path, session_id)
295
+ logger.info(
296
+ "[cli] Starting CLI invocation",
297
+ extra={
298
+ "session_id": session_id,
299
+ "project_path": str(project_path),
300
+ "log_file": str(log_file),
301
+ "prompt_mode": bool(prompt),
302
+ },
303
+ )
304
+
305
+ # Ensure onboarding is complete
306
+ if not check_onboarding():
307
+ logger.info(
308
+ "[cli] Onboarding check failed or aborted; exiting.",
309
+ extra={"session_id": session_id},
310
+ )
311
+ sys.exit(1)
312
+
313
+ # Initialize project configuration for the current working directory
259
314
  get_project_config(project_path)
260
315
 
261
316
  safe_mode = not unsafe
317
+ logger.debug(
318
+ "[cli] Configuration initialized",
319
+ extra={"session_id": session_id, "safe_mode": safe_mode, "verbose": verbose},
320
+ )
262
321
 
263
322
  # If prompt is provided, run directly
264
323
  if prompt:
265
324
  tools = get_default_tools()
266
- asyncio.run(run_query(prompt, tools, safe_mode, verbose))
325
+ asyncio.run(run_query(prompt, tools, safe_mode, verbose, session_id=session_id))
267
326
  return
268
327
 
269
328
  # If no command specified, start interactive REPL with Rich interface
@@ -271,7 +330,12 @@ def cli(
271
330
  # Use Rich interface by default
272
331
  from ripperdoc.cli.ui.rich_ui import main_rich
273
332
 
274
- main_rich(safe_mode=safe_mode, verbose=verbose)
333
+ main_rich(
334
+ safe_mode=safe_mode,
335
+ verbose=verbose,
336
+ session_id=session_id,
337
+ log_file_path=log_file,
338
+ )
275
339
  return
276
340
 
277
341
 
@@ -312,6 +376,7 @@ def main() -> None:
312
376
  sys.exit(130)
313
377
  except Exception as e:
314
378
  console.print(f"[red]Fatal error: {escape(str(e))}[/red]")
379
+ logger.exception("[cli] Fatal error in main CLI entrypoint")
315
380
  sys.exit(1)
316
381
 
317
382
 
@@ -11,8 +11,10 @@ from .compact_cmd import command as compact_command
11
11
  from .config_cmd import command as config_command
12
12
  from .cost_cmd import command as cost_command
13
13
  from .context_cmd import command as context_command
14
+ from .doctor_cmd import command as doctor_command
14
15
  from .exit_cmd import command as exit_command
15
16
  from .help_cmd import command as help_command
17
+ from .memory_cmd import command as memory_command
16
18
  from .mcp_cmd import command as mcp_command
17
19
  from .models_cmd import command as models_command
18
20
  from .resume_cmd import command as resume_command
@@ -40,6 +42,8 @@ ALL_COMMANDS: List[SlashCommand] = [
40
42
  models_command,
41
43
  exit_command,
42
44
  status_command,
45
+ doctor_command,
46
+ memory_command,
43
47
  tasks_command,
44
48
  todos_command,
45
49
  mcp_command,
@@ -8,27 +8,40 @@ from ripperdoc.core.agents import (
8
8
  save_agent_definition,
9
9
  )
10
10
  from ripperdoc.core.config import get_global_config
11
+ from ripperdoc.utils.log import get_logger
11
12
 
12
13
  from typing import Any
13
14
  from .base import SlashCommand
14
15
 
16
+ logger = get_logger()
17
+
15
18
 
16
19
  def _handle(ui: Any, trimmed_arg: str) -> bool:
17
20
  console = ui.console
18
21
  tokens = trimmed_arg.split()
19
22
  subcmd = tokens[0].lower() if tokens else ""
23
+ logger.info(
24
+ "[agents_cmd] Handling /agents command",
25
+ extra={
26
+ "subcommand": subcmd or "list",
27
+ "session_id": getattr(ui, "session_id", None),
28
+ },
29
+ )
20
30
 
21
31
  def print_agents_usage() -> None:
22
32
  console.print("[bold]/agents[/bold] — list configured agents")
23
33
  console.print(
24
- "[bold]/agents create <name> [location] [model][/bold] — create agent (location: user|project, default user)"
34
+ "[bold]/agents create <name> [location] [model][/bold] — "
35
+ "create agent (location: user|project, default user)"
25
36
  )
26
37
  console.print("[bold]/agents edit <name> [location][/bold] — edit an existing agent")
27
38
  console.print(
28
- "[bold]/agents delete <name> [location][/bold] — delete agent (location: user|project, default user)"
39
+ "[bold]/agents delete <name> [location][/bold] — "
40
+ "delete agent (location: user|project, default user)"
29
41
  )
30
42
  console.print(
31
- f"[dim]Agent files live in ~/.ripperdoc/{AGENT_DIR_NAME} or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
43
+ f"[dim]Agent files live in ~/.ripperdoc/{AGENT_DIR_NAME} "
44
+ f"or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
32
45
  )
33
46
  console.print(
34
47
  "[dim]Model can be a profile name or pointer (task/main/etc). Defaults to 'task'.[/dim]"
@@ -82,7 +95,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
82
95
  and model_input not in pointer_map
83
96
  ):
84
97
  console.print(
85
- "[yellow]Model not found in profiles or pointers; will fall back to main if unavailable.[/yellow]"
98
+ "[yellow]Model not found in profiles or pointers; "
99
+ "will fall back to main if unavailable.[/yellow]"
86
100
  )
87
101
 
88
102
  try:
@@ -100,6 +114,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
100
114
  except Exception as exc:
101
115
  console.print(f"[red]Failed to create agent: {escape(str(exc))}[/red]")
102
116
  print_agents_usage()
117
+ logger.exception(
118
+ "[agents_cmd] Failed to create agent",
119
+ extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
120
+ )
103
121
  return True
104
122
 
105
123
  if subcmd in ("delete", "del", "remove"):
@@ -127,6 +145,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
127
145
  except Exception as exc:
128
146
  console.print(f"[red]Failed to delete agent: {escape(str(exc))}[/red]")
129
147
  print_agents_usage()
148
+ logger.exception(
149
+ "[agents_cmd] Failed to delete agent",
150
+ extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
151
+ )
130
152
  return True
131
153
 
132
154
  if subcmd in ("edit", "update"):
@@ -200,6 +222,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
200
222
  except Exception as exc:
201
223
  console.print(f"[red]Failed to update agent: {escape(str(exc))}[/red]")
202
224
  print_agents_usage()
225
+ logger.exception(
226
+ "[agents_cmd] Failed to update agent",
227
+ extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
228
+ )
203
229
  return True
204
230
 
205
231
  agents = load_agent_definitions()
@@ -9,22 +9,29 @@ from ripperdoc.core.query import QueryContext
9
9
  from ripperdoc.core.system_prompt import build_system_prompt
10
10
  from ripperdoc.utils.memory import build_memory_instructions
11
11
  from ripperdoc.utils.message_compaction import (
12
- estimate_tokens_from_text,
13
12
  get_remaining_context_tokens,
14
13
  resolve_auto_compact_enabled,
15
14
  summarize_context_usage,
16
15
  )
16
+ from ripperdoc.utils.token_estimation import estimate_tokens
17
17
  from ripperdoc.utils.mcp import (
18
18
  estimate_mcp_tokens,
19
19
  format_mcp_instructions,
20
20
  load_mcp_servers_async,
21
21
  shutdown_mcp_runtime,
22
22
  )
23
+ from ripperdoc.utils.log import get_logger
23
24
 
24
25
  from .base import SlashCommand
25
26
 
27
+ logger = get_logger()
28
+
26
29
 
27
30
  def _handle(ui: Any, _: str) -> bool:
31
+ logger.info(
32
+ "[context_cmd] Rendering context summary",
33
+ extra={"session_id": getattr(ui, "session_id", None)},
34
+ )
28
35
  config = get_global_config()
29
36
  model_profile = get_profile_for_pointer("main")
30
37
  max_context_tokens = get_remaining_context_tokens(model_profile, config.context_token_limit)
@@ -53,7 +60,7 @@ def _handle(ui: Any, _: str) -> bool:
53
60
  mcp_instructions=mcp_instructions,
54
61
  )
55
62
  memory_instructions = build_memory_instructions()
56
- memory_tokens = estimate_tokens_from_text(memory_instructions) if memory_instructions else 0
63
+ memory_tokens = estimate_tokens(memory_instructions) if memory_instructions else 0
57
64
  mcp_tokens = estimate_mcp_tokens(servers) if mcp_instructions else 0
58
65
 
59
66
  breakdown = summarize_context_usage(
@@ -91,14 +98,17 @@ def _handle(ui: Any, _: str) -> bool:
91
98
  display = f"{display} ({server})"
92
99
  try:
93
100
  schema = tool.input_schema.model_json_schema()
94
- token_est = estimate_tokens_from_text(json.dumps(schema, sort_keys=True))
101
+ token_est = estimate_tokens(json.dumps(schema, sort_keys=True))
95
102
  except Exception:
96
103
  token_est = 0
97
104
  lines.append(f" └ {display}: {format_tokens(token_est)} tokens")
98
105
  if len(mcp_tools) > 20:
99
106
  lines.append(f" └ ... (+{len(mcp_tools) - 20} more)")
100
107
  except Exception:
101
- pass
108
+ logger.exception(
109
+ "[context_cmd] Failed to summarize MCP tools",
110
+ extra={"session_id": getattr(ui, "session_id", None)},
111
+ )
102
112
  for line in lines:
103
113
  ui.console.print(line)
104
114
  return True
@@ -32,6 +32,7 @@ def _handle(ui: Any, _: str) -> bool:
32
32
  total_cache_read = usage.total_cache_read_tokens
33
33
  total_cache_creation = usage.total_cache_creation_tokens
34
34
  total_tokens = total_input + total_output + total_cache_read + total_cache_creation
35
+ total_cost = usage.total_cost_usd
35
36
 
36
37
  ui.console.print("\n[bold]Session token usage[/bold]")
37
38
  ui.console.print(
@@ -44,6 +45,8 @@ def _handle(ui: Any, _: str) -> bool:
44
45
  f"{_fmt_tokens(total_cache_creation)} write"
45
46
  )
46
47
  ui.console.print(f" Requests: {usage.total_requests}")
48
+ if total_cost:
49
+ ui.console.print(f" Cost: ${total_cost:.4f}")
47
50
  if usage.total_duration_ms:
48
51
  ui.console.print(f" API time: {_format_duration(usage.total_duration_ms)}")
49
52
 
@@ -62,6 +65,8 @@ def _handle(ui: Any, _: str) -> bool:
62
65
  if stats.duration_ms:
63
66
  line += f", {_format_duration(stats.duration_ms)} total"
64
67
  line += ")"
68
+ if stats.cost_usd:
69
+ line += f", ${stats.cost_usd:.4f}"
65
70
  ui.console.print(line)
66
71
 
67
72
  return True
@@ -0,0 +1,208 @@
1
+ """Slash command to diagnose common setup issues."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from pathlib import Path
7
+ from typing import Any, List, Optional, Tuple
8
+
9
+ from rich.markup import escape
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+
13
+ from ripperdoc.core.config import (
14
+ ProviderType,
15
+ api_key_env_candidates,
16
+ get_global_config,
17
+ get_project_config,
18
+ )
19
+ from ripperdoc.cli.ui.helpers import get_profile_for_pointer
20
+ from ripperdoc.utils.log import get_logger
21
+ from ripperdoc.utils.mcp import load_mcp_servers_async, shutdown_mcp_runtime
22
+ from ripperdoc.utils.sandbox_utils import is_sandbox_available
23
+
24
+ from .base import SlashCommand
25
+
26
+ logger = get_logger()
27
+
28
+
29
+ def _status_row(label: str, status: str, detail: str = "") -> Tuple[str, str, str]:
30
+ """Build a (label, status, detail) tuple with icon."""
31
+ icons = {
32
+ "ok": "[green]✓[/green]",
33
+ "warn": "[yellow]![/yellow]",
34
+ "error": "[red]×[/red]",
35
+ }
36
+ icon = icons.get(status, "[yellow]?[/yellow]")
37
+ return (label, icon, detail)
38
+
39
+
40
+ def _api_key_status(provider: ProviderType, profile_key: Optional[str]) -> Tuple[str, str]:
41
+ """Check API key presence and source."""
42
+ import os
43
+
44
+ for env_var in api_key_env_candidates(provider):
45
+ if os.environ.get(env_var):
46
+ masked = os.environ[env_var]
47
+ masked = masked[:4] + "…" if len(masked) > 4 else "set"
48
+ return ("ok", f"Found in ${env_var} ({masked})")
49
+
50
+ if profile_key:
51
+ return ("ok", "Stored in config profile")
52
+
53
+ return ("error", "Missing API key for active provider; set $ENV or edit config")
54
+
55
+
56
+ def _model_status(project_path: Path) -> List[Tuple[str, str, str]]:
57
+ config = get_global_config()
58
+ pointer = getattr(config.model_pointers, "main", "default")
59
+ profile = get_profile_for_pointer("main")
60
+ rows: List[Tuple[str, str, str]] = []
61
+
62
+ if not profile:
63
+ rows.append(
64
+ _status_row("Model profile", "error", "No profile configured for pointer 'main'")
65
+ )
66
+ return rows
67
+
68
+ if pointer not in config.model_profiles:
69
+ rows.append(
70
+ _status_row(
71
+ "Model pointer",
72
+ "warn",
73
+ f"Pointer 'main' targets '{pointer}' which is missing; using fallback.",
74
+ )
75
+ )
76
+ rows.append(
77
+ _status_row(
78
+ "Model",
79
+ "ok",
80
+ f"{profile.model} ({profile.provider.value})",
81
+ )
82
+ )
83
+
84
+ key_status, key_detail = _api_key_status(profile.provider, profile.api_key)
85
+ rows.append(_status_row("API key", key_status, key_detail))
86
+ return rows
87
+
88
+
89
+ def _onboarding_status() -> Tuple[str, str, str]:
90
+ config = get_global_config()
91
+ if config.has_completed_onboarding:
92
+ return _status_row(
93
+ "Onboarding",
94
+ "ok",
95
+ f"Completed (version {str(config.last_onboarding_version or 'unknown')})",
96
+ )
97
+ return _status_row(
98
+ "Onboarding",
99
+ "warn",
100
+ "Not completed; run the CLI without flags to configure provider/model.",
101
+ )
102
+
103
+
104
+ def _sandbox_status() -> Tuple[str, str, str]:
105
+ available = is_sandbox_available()
106
+ if available:
107
+ return _status_row("Sandbox", "ok", "'srt' runtime is available")
108
+ return _status_row("Sandbox", "warn", "Sandbox runtime not detected; commands run normally")
109
+
110
+
111
+ def _mcp_status(project_path: Path) -> Tuple[List[Tuple[str, str, str]], List[str]]:
112
+ """Return MCP status rows and errors."""
113
+ rows: List[Tuple[str, str, str]] = []
114
+ errors: List[str] = []
115
+
116
+ async def _load() -> List[Any]:
117
+ try:
118
+ return await load_mcp_servers_async(project_path)
119
+ finally:
120
+ await shutdown_mcp_runtime()
121
+
122
+ try:
123
+ servers = asyncio.run(_load())
124
+ except Exception as exc: # pragma: no cover - defensive
125
+ logger.exception("[doctor] Failed to load MCP servers", exc_info=exc)
126
+ rows.append(_status_row("MCP", "error", f"Failed to load MCP config: {exc}"))
127
+ return rows, errors
128
+
129
+ if not servers:
130
+ rows.append(_status_row("MCP", "warn", "No MCP servers configured (.mcp.json)"))
131
+ return rows, errors
132
+
133
+ failing = [s for s in servers if getattr(s, "error", None)]
134
+ rows.append(
135
+ _status_row(
136
+ "MCP",
137
+ "ok" if not failing else "warn",
138
+ f"{len(servers)} configured; {len(failing)} with errors",
139
+ )
140
+ )
141
+ for server in failing[:5]:
142
+ errors.append(f"{server.name}: {server.error}")
143
+ if len(failing) > 5:
144
+ errors.append(f"... {len(failing) - 5} more")
145
+ return rows, errors
146
+
147
+
148
+ def _project_status(project_path: Path) -> Tuple[str, str, str]:
149
+ try:
150
+ config = get_project_config(project_path)
151
+ # Access a field to ensure model parsing does not throw.
152
+ _ = len(config.allowed_tools)
153
+ return _status_row(
154
+ "Project config", "ok", f".ripperdoc/config.json loaded for {project_path}"
155
+ )
156
+ except Exception as exc: # pragma: no cover - defensive
157
+ logger.exception("[doctor] Failed to load project config", exc_info=exc)
158
+ return _status_row(
159
+ "Project config", "warn", f"Could not read .ripperdoc/config.json: {exc}"
160
+ )
161
+
162
+
163
+ def _render_table(console: Any, rows: List[Tuple[str, str, str]]) -> None:
164
+ table = Table(show_header=True, header_style="bold cyan")
165
+ table.add_column("Check")
166
+ table.add_column("")
167
+ table.add_column("Details")
168
+ for label, status, detail in rows:
169
+ table.add_row(label, status, escape(detail) if detail else "")
170
+ console.print(table)
171
+
172
+
173
+ def _handle(ui: Any, _: str) -> bool:
174
+ project_path = getattr(ui, "project_path", Path.cwd())
175
+ results: List[Tuple[str, str, str]] = []
176
+
177
+ results.append(_onboarding_status())
178
+ results.extend(_model_status(project_path))
179
+ project_row = _project_status(project_path)
180
+ results.append(project_row)
181
+
182
+ mcp_rows, mcp_errors = _mcp_status(project_path)
183
+ results.extend(mcp_rows)
184
+ results.append(_sandbox_status())
185
+
186
+ ui.console.print(Panel("Environment diagnostics", title="/doctor", border_style="cyan"))
187
+ _render_table(ui.console, results)
188
+
189
+ if mcp_errors:
190
+ ui.console.print("\n[bold]MCP issues:[/bold]")
191
+ for err in mcp_errors:
192
+ ui.console.print(f" • {escape(err)}")
193
+
194
+ ui.console.print(
195
+ "\n[dim]If a check is failing, run `ripperdoc` without flags "
196
+ "to rerun onboarding or update ~/.ripperdoc.json[/dim]"
197
+ )
198
+ return True
199
+
200
+
201
+ command = SlashCommand(
202
+ name="doctor",
203
+ description="Diagnose model config, API keys, MCP, and sandbox support",
204
+ handler=_handle,
205
+ )
206
+
207
+
208
+ __all__ = ["command"]