ripperdoc 0.2.7__py3-none-any.whl → 0.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +33 -115
- ripperdoc/cli/commands/__init__.py +70 -6
- ripperdoc/cli/commands/agents_cmd.py +6 -3
- ripperdoc/cli/commands/clear_cmd.py +1 -4
- ripperdoc/cli/commands/config_cmd.py +1 -1
- ripperdoc/cli/commands/context_cmd.py +3 -2
- ripperdoc/cli/commands/doctor_cmd.py +18 -4
- ripperdoc/cli/commands/help_cmd.py +11 -1
- ripperdoc/cli/commands/hooks_cmd.py +610 -0
- ripperdoc/cli/commands/models_cmd.py +26 -9
- ripperdoc/cli/commands/permissions_cmd.py +57 -37
- ripperdoc/cli/commands/resume_cmd.py +6 -4
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +64 -8
- ripperdoc/cli/ui/interrupt_handler.py +3 -4
- ripperdoc/cli/ui/message_display.py +5 -3
- ripperdoc/cli/ui/panels.py +13 -10
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +196 -77
- ripperdoc/cli/ui/spinner.py +25 -1
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +215 -0
- ripperdoc/core/agents.py +9 -3
- ripperdoc/core/config.py +49 -12
- ripperdoc/core/custom_commands.py +412 -0
- ripperdoc/core/default_tools.py +11 -2
- ripperdoc/core/hooks/__init__.py +99 -0
- ripperdoc/core/hooks/config.py +301 -0
- ripperdoc/core/hooks/events.py +535 -0
- ripperdoc/core/hooks/executor.py +496 -0
- ripperdoc/core/hooks/integration.py +344 -0
- ripperdoc/core/hooks/manager.py +745 -0
- ripperdoc/core/permissions.py +40 -8
- ripperdoc/core/providers/anthropic.py +548 -68
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +60 -5
- ripperdoc/core/query.py +140 -39
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +9 -5
- ripperdoc/sdk/client.py +2 -2
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +2 -1
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +30 -20
- ripperdoc/tools/dynamic_mcp_tool.py +29 -8
- ripperdoc/tools/enter_plan_mode_tool.py +1 -1
- ripperdoc/tools/exit_plan_mode_tool.py +1 -1
- ripperdoc/tools/file_edit_tool.py +8 -4
- ripperdoc/tools/file_read_tool.py +9 -5
- ripperdoc/tools/file_write_tool.py +9 -5
- ripperdoc/tools/glob_tool.py +3 -2
- ripperdoc/tools/grep_tool.py +3 -2
- ripperdoc/tools/kill_bash_tool.py +1 -1
- ripperdoc/tools/ls_tool.py +1 -1
- ripperdoc/tools/mcp_tools.py +13 -10
- ripperdoc/tools/multi_edit_tool.py +8 -7
- ripperdoc/tools/notebook_edit_tool.py +7 -4
- ripperdoc/tools/skill_tool.py +1 -1
- ripperdoc/tools/task_tool.py +5 -4
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +11 -7
- ripperdoc/utils/file_watch.py +8 -2
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +11 -7
- ripperdoc/utils/messages.py +105 -66
- ripperdoc/utils/path_ignore.py +38 -12
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +24 -3
- ripperdoc-0.2.9.dist-info/RECORD +123 -0
- ripperdoc-0.2.7.dist-info/RECORD +0 -113
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive onboarding wizard for Ripperdoc.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from ripperdoc.cli.ui.provider_options import (
|
|
11
|
+
KNOWN_PROVIDERS,
|
|
12
|
+
ProviderOption,
|
|
13
|
+
default_model_for_protocol,
|
|
14
|
+
)
|
|
15
|
+
from ripperdoc.core.config import (
|
|
16
|
+
GlobalConfig,
|
|
17
|
+
ModelProfile,
|
|
18
|
+
ProviderType,
|
|
19
|
+
get_global_config,
|
|
20
|
+
save_global_config,
|
|
21
|
+
)
|
|
22
|
+
from ripperdoc.utils.prompt import prompt_secret
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_provider_choice(raw_choice: str, provider_keys: List[str]) -> Optional[str]:
|
|
29
|
+
"""Normalize user input into a provider key."""
|
|
30
|
+
normalized = raw_choice.strip().lower()
|
|
31
|
+
if normalized in provider_keys:
|
|
32
|
+
return normalized
|
|
33
|
+
try:
|
|
34
|
+
idx = int(normalized)
|
|
35
|
+
if 1 <= idx <= len(provider_keys):
|
|
36
|
+
return provider_keys[idx - 1]
|
|
37
|
+
except ValueError:
|
|
38
|
+
return None
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check_onboarding() -> bool:
|
|
43
|
+
"""Check if onboarding is complete and run if needed."""
|
|
44
|
+
config = get_global_config()
|
|
45
|
+
|
|
46
|
+
if config.has_completed_onboarding:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
console.print("[bold cyan]Welcome to Ripperdoc![/bold cyan]\n")
|
|
50
|
+
console.print("Let's set up your AI model configuration.\n")
|
|
51
|
+
|
|
52
|
+
return run_onboarding_wizard(config)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run_onboarding_wizard(config: GlobalConfig) -> bool:
|
|
56
|
+
"""Run interactive onboarding wizard."""
|
|
57
|
+
provider_keys = KNOWN_PROVIDERS.keys() + ["custom"]
|
|
58
|
+
default_choice_key = KNOWN_PROVIDERS.default_choice.key
|
|
59
|
+
|
|
60
|
+
# Display provider options vertically
|
|
61
|
+
console.print("[bold]Available providers:[/bold]")
|
|
62
|
+
for i, provider_key in enumerate(provider_keys, 1):
|
|
63
|
+
marker = "[cyan]→[/cyan]" if provider_key == default_choice_key else " "
|
|
64
|
+
console.print(f" {marker} {i}. {provider_key}")
|
|
65
|
+
console.print("")
|
|
66
|
+
|
|
67
|
+
# Prompt for provider choice with validation
|
|
68
|
+
provider_choice: Optional[str] = None
|
|
69
|
+
while provider_choice is None:
|
|
70
|
+
raw_choice = click.prompt(
|
|
71
|
+
"Choose your model provider",
|
|
72
|
+
default=default_choice_key,
|
|
73
|
+
)
|
|
74
|
+
provider_choice = resolve_provider_choice(raw_choice, provider_keys)
|
|
75
|
+
if provider_choice is None:
|
|
76
|
+
console.print(
|
|
77
|
+
f"[red]Invalid choice. Please enter a provider name or number (1-{len(provider_keys)}).[/red]"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
api_base_override: Optional[str] = None
|
|
81
|
+
if provider_choice == "custom":
|
|
82
|
+
protocol_input = click.prompt(
|
|
83
|
+
"Protocol family (for API compatibility)",
|
|
84
|
+
type=click.Choice([p.value for p in ProviderType]),
|
|
85
|
+
default=ProviderType.OPENAI_COMPATIBLE.value,
|
|
86
|
+
)
|
|
87
|
+
protocol = ProviderType(protocol_input)
|
|
88
|
+
api_base_override = click.prompt("API Base URL")
|
|
89
|
+
provider_option = ProviderOption(
|
|
90
|
+
key="custom",
|
|
91
|
+
protocol=protocol,
|
|
92
|
+
default_model=default_model_for_protocol(protocol),
|
|
93
|
+
model_suggestions=(),
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
provider_option = KNOWN_PROVIDERS.get(provider_choice)
|
|
97
|
+
if provider_option is None:
|
|
98
|
+
provider_option = ProviderOption(
|
|
99
|
+
key=provider_choice,
|
|
100
|
+
protocol=ProviderType.OPENAI_COMPATIBLE,
|
|
101
|
+
default_model=default_model_for_protocol(ProviderType.OPENAI_COMPATIBLE),
|
|
102
|
+
model_suggestions=(),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
api_key = ""
|
|
106
|
+
while not api_key:
|
|
107
|
+
api_key = prompt_secret("Enter your API key").strip()
|
|
108
|
+
if not api_key:
|
|
109
|
+
console.print("[red]API key is required.[/red]")
|
|
110
|
+
|
|
111
|
+
# Get model name with provider-specific suggestions
|
|
112
|
+
model, api_base = get_model_name_with_suggestions(provider_option, api_base_override)
|
|
113
|
+
|
|
114
|
+
# Get context window
|
|
115
|
+
context_window = get_context_window()
|
|
116
|
+
|
|
117
|
+
# Create model profile
|
|
118
|
+
config.model_profiles["default"] = ModelProfile(
|
|
119
|
+
provider=provider_option.protocol,
|
|
120
|
+
model=model,
|
|
121
|
+
api_key=api_key,
|
|
122
|
+
api_base=api_base,
|
|
123
|
+
context_window=context_window,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
config.has_completed_onboarding = True
|
|
127
|
+
config.last_onboarding_version = get_version()
|
|
128
|
+
|
|
129
|
+
save_global_config(config)
|
|
130
|
+
|
|
131
|
+
console.print("\n[green]✓ Configuration saved![/green]\n")
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_model_name_with_suggestions(
|
|
136
|
+
provider: ProviderOption,
|
|
137
|
+
api_base_override: Optional[str],
|
|
138
|
+
) -> Tuple[str, Optional[str]]:
|
|
139
|
+
"""Get model name with provider-specific suggestions and default API base.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Tuple of (model_name, api_base)
|
|
143
|
+
"""
|
|
144
|
+
# Set default API base based on provider choice
|
|
145
|
+
api_base = api_base_override
|
|
146
|
+
if api_base is None and provider.default_api_base:
|
|
147
|
+
api_base = provider.default_api_base
|
|
148
|
+
console.print(f"[dim]Using default API base: {api_base}[/dim]")
|
|
149
|
+
|
|
150
|
+
default_model = provider.default_model or default_model_for_protocol(provider.protocol)
|
|
151
|
+
suggestions = list(provider.model_suggestions)
|
|
152
|
+
|
|
153
|
+
# Show suggestions if available
|
|
154
|
+
if suggestions:
|
|
155
|
+
console.print("\n[dim]Available models for this provider:[/dim]")
|
|
156
|
+
for i, model_name in enumerate(suggestions[:5]): # Show top 5
|
|
157
|
+
console.print(f" [dim]{i+1}. {model_name}[/dim]")
|
|
158
|
+
console.print("")
|
|
159
|
+
|
|
160
|
+
# Prompt for model name
|
|
161
|
+
if provider.protocol == ProviderType.ANTHROPIC:
|
|
162
|
+
model = click.prompt("Model name", default=default_model)
|
|
163
|
+
elif provider.protocol == ProviderType.OPENAI_COMPATIBLE:
|
|
164
|
+
model = click.prompt("Model name", default=default_model)
|
|
165
|
+
# Prompt for API base if still not set
|
|
166
|
+
if api_base is None:
|
|
167
|
+
api_base_input = click.prompt(
|
|
168
|
+
"API base URL (optional)", default="", show_default=False
|
|
169
|
+
)
|
|
170
|
+
api_base = api_base_input or None
|
|
171
|
+
elif provider.protocol == ProviderType.GEMINI:
|
|
172
|
+
model = click.prompt("Model name", default=default_model)
|
|
173
|
+
if api_base is None:
|
|
174
|
+
api_base_input = click.prompt(
|
|
175
|
+
"API base URL (optional)", default="", show_default=False
|
|
176
|
+
)
|
|
177
|
+
api_base = api_base_input or None
|
|
178
|
+
else:
|
|
179
|
+
model = click.prompt("Model name", default=default_model)
|
|
180
|
+
|
|
181
|
+
return model, api_base
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_context_window() -> Optional[int]:
|
|
185
|
+
"""Get context window size from user."""
|
|
186
|
+
context_window_input = click.prompt(
|
|
187
|
+
"Context window in tokens (optional, press Enter to skip)",
|
|
188
|
+
default="",
|
|
189
|
+
show_default=False,
|
|
190
|
+
)
|
|
191
|
+
context_window = None
|
|
192
|
+
if context_window_input.strip():
|
|
193
|
+
try:
|
|
194
|
+
context_window = int(context_window_input.strip())
|
|
195
|
+
except ValueError:
|
|
196
|
+
console.print(
|
|
197
|
+
"[yellow]Invalid context window, using auto-detected defaults.[/yellow]"
|
|
198
|
+
)
|
|
199
|
+
return context_window
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def get_version() -> str:
|
|
203
|
+
"""Get current version of Ripperdoc."""
|
|
204
|
+
try:
|
|
205
|
+
from ripperdoc import __version__
|
|
206
|
+
return __version__
|
|
207
|
+
except ImportError:
|
|
208
|
+
return "unknown"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
# For testing
|
|
213
|
+
config = get_global_config()
|
|
214
|
+
config.has_completed_onboarding = False
|
|
215
|
+
run_onboarding_wizard(config)
|
ripperdoc/core/agents.py
CHANGED
|
@@ -278,10 +278,15 @@ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
|
|
|
278
278
|
body = "\n".join(lines[idx + 1 :])
|
|
279
279
|
try:
|
|
280
280
|
frontmatter = yaml.safe_load(frontmatter_text) or {}
|
|
281
|
-
except (
|
|
281
|
+
except (
|
|
282
|
+
yaml.YAMLError,
|
|
283
|
+
ValueError,
|
|
284
|
+
TypeError,
|
|
285
|
+
) as exc: # pragma: no cover - defensive
|
|
282
286
|
logger.warning(
|
|
283
287
|
"Invalid frontmatter in agent file: %s: %s",
|
|
284
|
-
type(exc).__name__,
|
|
288
|
+
type(exc).__name__,
|
|
289
|
+
exc,
|
|
285
290
|
extra={"error": str(exc)},
|
|
286
291
|
)
|
|
287
292
|
return {"__error__": f"Invalid frontmatter: {exc}"}, body
|
|
@@ -312,7 +317,8 @@ def _parse_agent_file(
|
|
|
312
317
|
except (OSError, IOError, UnicodeDecodeError) as exc:
|
|
313
318
|
logger.warning(
|
|
314
319
|
"Failed to read agent file: %s: %s",
|
|
315
|
-
type(exc).__name__,
|
|
320
|
+
type(exc).__name__,
|
|
321
|
+
exc,
|
|
316
322
|
extra={"error": str(exc), "path": str(path)},
|
|
317
323
|
)
|
|
318
324
|
return None, f"Failed to read agent file {path}: {exc}"
|
ripperdoc/core/config.py
CHANGED
|
@@ -7,8 +7,8 @@ including API keys, model settings, and user preferences.
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Dict, Optional, Literal
|
|
11
|
-
from pydantic import BaseModel, Field
|
|
10
|
+
from typing import Any, Dict, Optional, Literal
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
12
12
|
from enum import Enum
|
|
13
13
|
|
|
14
14
|
from ripperdoc.utils.log import get_logger
|
|
@@ -111,7 +111,7 @@ class ModelProfile(BaseModel):
|
|
|
111
111
|
# interactions into plain text to support providers that reject tool roles.
|
|
112
112
|
openai_tool_mode: Literal["native", "text"] = "native"
|
|
113
113
|
# Optional override for thinking protocol handling (e.g., "deepseek", "openrouter",
|
|
114
|
-
# "qwen", "gemini_openai", "
|
|
114
|
+
# "qwen", "gemini_openai", "openai"). When unset, provider heuristics are used.
|
|
115
115
|
thinking_mode: Optional[str] = None
|
|
116
116
|
# Pricing (USD per 1M tokens). Leave as 0 to skip cost calculation.
|
|
117
117
|
input_cost_per_million_tokens: float = 0.0
|
|
@@ -130,7 +130,7 @@ class ModelPointers(BaseModel):
|
|
|
130
130
|
class GlobalConfig(BaseModel):
|
|
131
131
|
"""Global configuration stored in ~/.ripperdoc.json"""
|
|
132
132
|
|
|
133
|
-
model_config = {"protected_namespaces": ()}
|
|
133
|
+
model_config = {"protected_namespaces": (), "populate_by_name": True}
|
|
134
134
|
|
|
135
135
|
# Model configuration
|
|
136
136
|
model_profiles: Dict[str, ModelProfile] = Field(default_factory=dict)
|
|
@@ -139,7 +139,8 @@ class GlobalConfig(BaseModel):
|
|
|
139
139
|
# User preferences
|
|
140
140
|
theme: str = "dark"
|
|
141
141
|
verbose: bool = False
|
|
142
|
-
|
|
142
|
+
yolo_mode: bool = Field(default=False)
|
|
143
|
+
show_full_thinking: bool = Field(default=False)
|
|
143
144
|
auto_compact_enabled: bool = True
|
|
144
145
|
context_token_limit: Optional[int] = None
|
|
145
146
|
|
|
@@ -154,6 +155,18 @@ class GlobalConfig(BaseModel):
|
|
|
154
155
|
# Statistics
|
|
155
156
|
num_startups: int = 0
|
|
156
157
|
|
|
158
|
+
@model_validator(mode="before")
|
|
159
|
+
@classmethod
|
|
160
|
+
def _migrate_safe_mode(cls, data: Any) -> Any:
|
|
161
|
+
"""Translate legacy safe_mode to the new yolo_mode flag."""
|
|
162
|
+
if isinstance(data, dict) and "safe_mode" in data and "yolo_mode" not in data:
|
|
163
|
+
data = dict(data)
|
|
164
|
+
try:
|
|
165
|
+
data["yolo_mode"] = not bool(data.pop("safe_mode"))
|
|
166
|
+
except Exception:
|
|
167
|
+
data["yolo_mode"] = False
|
|
168
|
+
return data
|
|
169
|
+
|
|
157
170
|
|
|
158
171
|
class ProjectConfig(BaseModel):
|
|
159
172
|
"""Project-specific configuration stored in .ripperdoc/config.json"""
|
|
@@ -167,7 +180,7 @@ class ProjectConfig(BaseModel):
|
|
|
167
180
|
# Path ignore patterns (gitignore-style)
|
|
168
181
|
ignore_patterns: list[str] = Field(
|
|
169
182
|
default_factory=list,
|
|
170
|
-
description="Gitignore-style patterns for paths to ignore in file operations"
|
|
183
|
+
description="Gitignore-style patterns for paths to ignore in file operations",
|
|
171
184
|
)
|
|
172
185
|
|
|
173
186
|
# Context
|
|
@@ -222,10 +235,18 @@ class ConfigManager:
|
|
|
222
235
|
"profile_count": len(self._global_config.model_profiles),
|
|
223
236
|
},
|
|
224
237
|
)
|
|
225
|
-
except (
|
|
238
|
+
except (
|
|
239
|
+
json.JSONDecodeError,
|
|
240
|
+
OSError,
|
|
241
|
+
IOError,
|
|
242
|
+
UnicodeDecodeError,
|
|
243
|
+
ValueError,
|
|
244
|
+
TypeError,
|
|
245
|
+
) as e:
|
|
226
246
|
logger.warning(
|
|
227
247
|
"Error loading global config: %s: %s",
|
|
228
|
-
type(e).__name__,
|
|
248
|
+
type(e).__name__,
|
|
249
|
+
e,
|
|
229
250
|
extra={"error": str(e)},
|
|
230
251
|
)
|
|
231
252
|
self._global_config = GlobalConfig()
|
|
@@ -276,10 +297,18 @@ class ConfigManager:
|
|
|
276
297
|
"allowed_tools": len(self._project_config.allowed_tools),
|
|
277
298
|
},
|
|
278
299
|
)
|
|
279
|
-
except (
|
|
300
|
+
except (
|
|
301
|
+
json.JSONDecodeError,
|
|
302
|
+
OSError,
|
|
303
|
+
IOError,
|
|
304
|
+
UnicodeDecodeError,
|
|
305
|
+
ValueError,
|
|
306
|
+
TypeError,
|
|
307
|
+
) as e:
|
|
280
308
|
logger.warning(
|
|
281
309
|
"Error loading project config: %s: %s",
|
|
282
|
-
type(e).__name__,
|
|
310
|
+
type(e).__name__,
|
|
311
|
+
e,
|
|
283
312
|
extra={"error": str(e), "path": str(config_path)},
|
|
284
313
|
)
|
|
285
314
|
self._project_config = ProjectConfig()
|
|
@@ -344,10 +373,18 @@ class ConfigManager:
|
|
|
344
373
|
"project_path": str(self.current_project_path),
|
|
345
374
|
},
|
|
346
375
|
)
|
|
347
|
-
except (
|
|
376
|
+
except (
|
|
377
|
+
json.JSONDecodeError,
|
|
378
|
+
OSError,
|
|
379
|
+
IOError,
|
|
380
|
+
UnicodeDecodeError,
|
|
381
|
+
ValueError,
|
|
382
|
+
TypeError,
|
|
383
|
+
) as e:
|
|
348
384
|
logger.warning(
|
|
349
385
|
"Error loading project-local config: %s: %s",
|
|
350
|
-
type(e).__name__,
|
|
386
|
+
type(e).__name__,
|
|
387
|
+
e,
|
|
351
388
|
extra={"error": str(e), "path": str(config_path)},
|
|
352
389
|
)
|
|
353
390
|
self._project_local_config = ProjectLocalConfig()
|