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