ralph-code 0.5.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.
- ralph/__init__.py +20 -0
- ralph/__main__.py +34 -0
- ralph/app.py +1328 -0
- ralph/claude_runner.py +22 -0
- ralph/colors.py +183 -0
- ralph/config.py +227 -0
- ralph/git_manager.py +304 -0
- ralph/harness.py +393 -0
- ralph/harness_runner.py +972 -0
- ralph/prd_manager.py +348 -0
- ralph/schemas/ralph_tasks_schema.json +95 -0
- ralph/schemas/task_schema.json +92 -0
- ralph/spinner.py +287 -0
- ralph/storage.py +77 -0
- ralph/tasks.py +298 -0
- ralph/user_stories.py +283 -0
- ralph/workflow.py +1036 -0
- ralph_code-0.5.0.dist-info/METADATA +79 -0
- ralph_code-0.5.0.dist-info/RECORD +23 -0
- ralph_code-0.5.0.dist-info/WHEEL +5 -0
- ralph_code-0.5.0.dist-info/entry_points.txt +2 -0
- ralph_code-0.5.0.dist-info/licenses/LICENSE +21 -0
- ralph_code-0.5.0.dist-info/top_level.txt +1 -0
ralph/claude_runner.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Claude Code CLI integration for ralph-coding application.
|
|
2
|
+
|
|
3
|
+
This module is deprecated. Use harness_runner instead.
|
|
4
|
+
Maintained for backwards compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Re-export everything from harness_runner for backwards compatibility
|
|
8
|
+
from .harness_runner import (
|
|
9
|
+
HarnessResponse as ClaudeResponse,
|
|
10
|
+
HarnessRunner as ClaudeRunner,
|
|
11
|
+
HarnessResponse,
|
|
12
|
+
HarnessRunner,
|
|
13
|
+
HARNESS_MODEL_MAPPING,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ClaudeResponse",
|
|
18
|
+
"ClaudeRunner",
|
|
19
|
+
"HarnessResponse",
|
|
20
|
+
"HarnessRunner",
|
|
21
|
+
"HARNESS_MODEL_MAPPING",
|
|
22
|
+
]
|
ralph/colors.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Nord Theme color constants for ralph-coding.
|
|
2
|
+
|
|
3
|
+
This module defines the complete Nord Theme color palette (16 colors) for use
|
|
4
|
+
throughout the application. Colors are organized into four semantic groups:
|
|
5
|
+
|
|
6
|
+
POLAR NIGHT (Background colors):
|
|
7
|
+
NORD0-NORD3: Dark background colors, from darkest to lightest.
|
|
8
|
+
Use for backgrounds, panels, and UI chrome.
|
|
9
|
+
- NORD0: Main background
|
|
10
|
+
- NORD1: Elevated surfaces, secondary backgrounds
|
|
11
|
+
- NORD2: Selections, highlights, UI borders
|
|
12
|
+
- NORD3: Comments, inactive elements, subtle text
|
|
13
|
+
|
|
14
|
+
SNOW STORM (Foreground colors):
|
|
15
|
+
NORD4-NORD6: Light text colors, from dimmest to brightest.
|
|
16
|
+
Use for text and foreground elements.
|
|
17
|
+
- NORD4: Main text color
|
|
18
|
+
- NORD5: Brighter text, emphasis
|
|
19
|
+
- NORD6: Brightest text, maximum contrast
|
|
20
|
+
|
|
21
|
+
FROST (Accent colors):
|
|
22
|
+
NORD7-NORD10: Cool accent colors for interactive elements.
|
|
23
|
+
- NORD7 (FROST_TEAL): Success states, secondary accents
|
|
24
|
+
- NORD8 (FROST_CYAN): Primary accent, highlights, links
|
|
25
|
+
- NORD9 (FROST_LIGHT_BLUE): Tertiary accents, decorative
|
|
26
|
+
- NORD10 (FROST_BLUE): Primary buttons, key UI elements
|
|
27
|
+
|
|
28
|
+
AURORA (State colors):
|
|
29
|
+
NORD11-NORD15: Vivid colors for semantic states.
|
|
30
|
+
- NORD11 (AURORA_RED): Errors, destructive actions, critical
|
|
31
|
+
- NORD12 (AURORA_ORANGE): Warnings, caution states
|
|
32
|
+
- NORD13 (AURORA_YELLOW): Attention, pending states, in-progress
|
|
33
|
+
- NORD14 (AURORA_GREEN): Success, completed, positive states
|
|
34
|
+
- NORD15 (AURORA_PURPLE): Special, info, accent states
|
|
35
|
+
|
|
36
|
+
Color Mappings for Application States:
|
|
37
|
+
- Idle/Inactive: NORD3 (dim polar night)
|
|
38
|
+
- Pending: NORD13 (aurora yellow)
|
|
39
|
+
- In Progress: NORD8 (frost cyan) or NORD13 (aurora yellow)
|
|
40
|
+
- Success/Complete: NORD14 (aurora green)
|
|
41
|
+
- Error/Failed: NORD11 (aurora red)
|
|
42
|
+
- Warning: NORD12 (aurora orange)
|
|
43
|
+
- Info/Special: NORD15 (aurora purple)
|
|
44
|
+
|
|
45
|
+
Usage Example:
|
|
46
|
+
from ralph.colors import NORD8, AURORA_GREEN, POLAR_NIGHT_0
|
|
47
|
+
|
|
48
|
+
# Use hex values directly
|
|
49
|
+
print(f"Primary accent: {NORD8}")
|
|
50
|
+
|
|
51
|
+
# Use semantic aliases
|
|
52
|
+
print(f"Success color: {AURORA_GREEN}")
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from typing import Final
|
|
56
|
+
|
|
57
|
+
# =============================================================================
|
|
58
|
+
# POLAR NIGHT - Background Colors
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Dark base colors for backgrounds and UI surfaces
|
|
61
|
+
|
|
62
|
+
NORD0: Final[str] = "#2E3440"
|
|
63
|
+
"""Darkest background - main application background."""
|
|
64
|
+
|
|
65
|
+
NORD1: Final[str] = "#3B4252"
|
|
66
|
+
"""Elevated surfaces - panels, cards, secondary backgrounds."""
|
|
67
|
+
|
|
68
|
+
NORD2: Final[str] = "#434C5E"
|
|
69
|
+
"""Selection backgrounds - highlights, UI borders, dividers."""
|
|
70
|
+
|
|
71
|
+
NORD3: Final[str] = "#4C566A"
|
|
72
|
+
"""Comments and inactive - subtle text, disabled states."""
|
|
73
|
+
|
|
74
|
+
# Semantic aliases for Polar Night
|
|
75
|
+
POLAR_NIGHT_0: Final[str] = NORD0
|
|
76
|
+
POLAR_NIGHT_1: Final[str] = NORD1
|
|
77
|
+
POLAR_NIGHT_2: Final[str] = NORD2
|
|
78
|
+
POLAR_NIGHT_3: Final[str] = NORD3
|
|
79
|
+
|
|
80
|
+
# Functional aliases for backgrounds
|
|
81
|
+
BG_PRIMARY: Final[str] = NORD0
|
|
82
|
+
BG_SECONDARY: Final[str] = NORD1
|
|
83
|
+
BG_HIGHLIGHT: Final[str] = NORD2
|
|
84
|
+
BG_COMMENT: Final[str] = NORD3
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# SNOW STORM - Foreground Colors
|
|
88
|
+
# =============================================================================
|
|
89
|
+
# Light colors for text and foreground elements
|
|
90
|
+
|
|
91
|
+
NORD4: Final[str] = "#D8DEE9"
|
|
92
|
+
"""Main text color - standard foreground text."""
|
|
93
|
+
|
|
94
|
+
NORD5: Final[str] = "#E5E9F0"
|
|
95
|
+
"""Brighter text - emphasis, important content."""
|
|
96
|
+
|
|
97
|
+
NORD6: Final[str] = "#ECEFF4"
|
|
98
|
+
"""Brightest text - maximum contrast, headings."""
|
|
99
|
+
|
|
100
|
+
# Semantic aliases for Snow Storm
|
|
101
|
+
SNOW_STORM_0: Final[str] = NORD4
|
|
102
|
+
SNOW_STORM_1: Final[str] = NORD5
|
|
103
|
+
SNOW_STORM_2: Final[str] = NORD6
|
|
104
|
+
|
|
105
|
+
# Functional aliases for foregrounds
|
|
106
|
+
FG_PRIMARY: Final[str] = NORD4
|
|
107
|
+
FG_BRIGHT: Final[str] = NORD5
|
|
108
|
+
FG_BRIGHTEST: Final[str] = NORD6
|
|
109
|
+
|
|
110
|
+
# =============================================================================
|
|
111
|
+
# FROST - Accent Colors
|
|
112
|
+
# =============================================================================
|
|
113
|
+
# Cool accent colors for interactive and decorative elements
|
|
114
|
+
|
|
115
|
+
NORD7: Final[str] = "#8FBCBB"
|
|
116
|
+
"""Frost teal - secondary accents, success indicators."""
|
|
117
|
+
|
|
118
|
+
NORD8: Final[str] = "#88C0D0"
|
|
119
|
+
"""Frost cyan - primary accent, highlights, links."""
|
|
120
|
+
|
|
121
|
+
NORD9: Final[str] = "#81A1C1"
|
|
122
|
+
"""Frost light blue - tertiary accents, decorative elements."""
|
|
123
|
+
|
|
124
|
+
NORD10: Final[str] = "#5E81AC"
|
|
125
|
+
"""Frost blue - primary buttons, key interactive elements."""
|
|
126
|
+
|
|
127
|
+
# Semantic aliases for Frost
|
|
128
|
+
FROST_TEAL: Final[str] = NORD7
|
|
129
|
+
FROST_CYAN: Final[str] = NORD8
|
|
130
|
+
FROST_LIGHT_BLUE: Final[str] = NORD9
|
|
131
|
+
FROST_BLUE: Final[str] = NORD10
|
|
132
|
+
|
|
133
|
+
# Functional aliases for accents
|
|
134
|
+
ACCENT_PRIMARY: Final[str] = NORD8
|
|
135
|
+
ACCENT_SECONDARY: Final[str] = NORD7
|
|
136
|
+
ACCENT_TERTIARY: Final[str] = NORD9
|
|
137
|
+
ACCENT_BUTTON: Final[str] = NORD10
|
|
138
|
+
|
|
139
|
+
# =============================================================================
|
|
140
|
+
# AURORA - State Colors
|
|
141
|
+
# =============================================================================
|
|
142
|
+
# Vivid colors for semantic states and feedback
|
|
143
|
+
|
|
144
|
+
NORD11: Final[str] = "#BF616A"
|
|
145
|
+
"""Aurora red - errors, destructive actions, critical alerts."""
|
|
146
|
+
|
|
147
|
+
NORD12: Final[str] = "#D08770"
|
|
148
|
+
"""Aurora orange - warnings, caution states, attention needed."""
|
|
149
|
+
|
|
150
|
+
NORD13: Final[str] = "#EBCB8B"
|
|
151
|
+
"""Aurora yellow - pending states, in-progress, highlights."""
|
|
152
|
+
|
|
153
|
+
NORD14: Final[str] = "#A3BE8C"
|
|
154
|
+
"""Aurora green - success, completed, positive confirmation."""
|
|
155
|
+
|
|
156
|
+
NORD15: Final[str] = "#B48EAD"
|
|
157
|
+
"""Aurora purple - special states, info, accent highlights."""
|
|
158
|
+
|
|
159
|
+
# Semantic aliases for Aurora
|
|
160
|
+
AURORA_RED: Final[str] = NORD11
|
|
161
|
+
AURORA_ORANGE: Final[str] = NORD12
|
|
162
|
+
AURORA_YELLOW: Final[str] = NORD13
|
|
163
|
+
AURORA_GREEN: Final[str] = NORD14
|
|
164
|
+
AURORA_PURPLE: Final[str] = NORD15
|
|
165
|
+
|
|
166
|
+
# Functional aliases for states
|
|
167
|
+
STATE_ERROR: Final[str] = NORD11
|
|
168
|
+
STATE_WARNING: Final[str] = NORD12
|
|
169
|
+
STATE_PENDING: Final[str] = NORD13
|
|
170
|
+
STATE_SUCCESS: Final[str] = NORD14
|
|
171
|
+
STATE_INFO: Final[str] = NORD15
|
|
172
|
+
|
|
173
|
+
# =============================================================================
|
|
174
|
+
# All colors tuple for iteration
|
|
175
|
+
# =============================================================================
|
|
176
|
+
|
|
177
|
+
ALL_NORD_COLORS: Final[tuple[str, ...]] = (
|
|
178
|
+
NORD0, NORD1, NORD2, NORD3, # Polar Night
|
|
179
|
+
NORD4, NORD5, NORD6, # Snow Storm
|
|
180
|
+
NORD7, NORD8, NORD9, NORD10, # Frost
|
|
181
|
+
NORD11, NORD12, NORD13, NORD14, NORD15, # Aurora
|
|
182
|
+
)
|
|
183
|
+
"""All 16 Nord colors in order (NORD0-NORD15)."""
|
ralph/config.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Configuration management for ralph-coding application."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from .storage import get_config_path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ErrorMode = Literal["block", "retry", "pause", "skip"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_CONFIG = {
|
|
14
|
+
"harness": "claude",
|
|
15
|
+
"worker_model": "opus",
|
|
16
|
+
"summary_model": "haiku",
|
|
17
|
+
"max_iterations": 10,
|
|
18
|
+
"max_story_attempts": 3,
|
|
19
|
+
"auto_spec_without_oversight": True,
|
|
20
|
+
"wait_on_rate_limit": True,
|
|
21
|
+
"pause_on_completion": True,
|
|
22
|
+
"always_build_tests": False,
|
|
23
|
+
"branch_prefix": "ralph",
|
|
24
|
+
"on_error": "block",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Config:
|
|
29
|
+
"""Global configuration manager for ralph-coding."""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self._config: dict[str, Any] = {}
|
|
33
|
+
self._load()
|
|
34
|
+
|
|
35
|
+
def _load(self) -> None:
|
|
36
|
+
"""Load configuration from disk, using defaults for missing values."""
|
|
37
|
+
config_path = get_config_path()
|
|
38
|
+
needs_save = False
|
|
39
|
+
|
|
40
|
+
if config_path.exists():
|
|
41
|
+
try:
|
|
42
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
43
|
+
self._config = json.load(f)
|
|
44
|
+
except (json.JSONDecodeError, IOError):
|
|
45
|
+
self._config = {}
|
|
46
|
+
|
|
47
|
+
# Migration: convert old 'claude_binary' key to 'harness'
|
|
48
|
+
if "claude_binary" in self._config and "harness" not in self._config:
|
|
49
|
+
self._config["harness"] = self._config.pop("claude_binary")
|
|
50
|
+
needs_save = True
|
|
51
|
+
|
|
52
|
+
# Migration: map old 'model' key to worker_model
|
|
53
|
+
if "model" in self._config and "worker_model" not in self._config:
|
|
54
|
+
self._config["worker_model"] = self._config["model"]
|
|
55
|
+
needs_save = True
|
|
56
|
+
if "model" in self._config:
|
|
57
|
+
self._config.pop("model", None)
|
|
58
|
+
needs_save = True
|
|
59
|
+
|
|
60
|
+
# Set worker model if missing (based on harness defaults)
|
|
61
|
+
if "worker_model" not in self._config:
|
|
62
|
+
harness = self._config.get("harness", DEFAULT_CONFIG["harness"])
|
|
63
|
+
if harness == "codex":
|
|
64
|
+
self._config["worker_model"] = "gpt-5.2-codex"
|
|
65
|
+
else:
|
|
66
|
+
self._config["worker_model"] = "opus"
|
|
67
|
+
needs_save = True
|
|
68
|
+
|
|
69
|
+
# Set summary model if missing (based on harness defaults)
|
|
70
|
+
if "summary_model" not in self._config:
|
|
71
|
+
harness = self._config.get("harness", DEFAULT_CONFIG["harness"])
|
|
72
|
+
if harness == "codex":
|
|
73
|
+
self._config["summary_model"] = "gpt-5.2"
|
|
74
|
+
else:
|
|
75
|
+
self._config["summary_model"] = "haiku"
|
|
76
|
+
needs_save = True
|
|
77
|
+
|
|
78
|
+
# Apply defaults for any missing keys
|
|
79
|
+
for key, default in DEFAULT_CONFIG.items():
|
|
80
|
+
if key not in self._config:
|
|
81
|
+
self._config[key] = default
|
|
82
|
+
|
|
83
|
+
# Save if migration was performed
|
|
84
|
+
if needs_save:
|
|
85
|
+
self._save()
|
|
86
|
+
|
|
87
|
+
def _save(self) -> None:
|
|
88
|
+
"""Save configuration to disk."""
|
|
89
|
+
config_path = get_config_path()
|
|
90
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
|
|
92
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
93
|
+
json.dump(self._config, f, indent=2, ensure_ascii=False)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def harness(self) -> str:
|
|
97
|
+
"""Path to the harness CLI tool (e.g., claude, aider)."""
|
|
98
|
+
return str(self._config["harness"])
|
|
99
|
+
|
|
100
|
+
@harness.setter
|
|
101
|
+
def harness(self, value: str) -> None:
|
|
102
|
+
self._config["harness"] = value
|
|
103
|
+
self._save()
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def worker_model(self) -> str:
|
|
107
|
+
"""Model to use for implementation (harness-specific model names)."""
|
|
108
|
+
return str(self._config["worker_model"])
|
|
109
|
+
|
|
110
|
+
@worker_model.setter
|
|
111
|
+
def worker_model(self, value: str) -> None:
|
|
112
|
+
self._config["worker_model"] = value
|
|
113
|
+
self._save()
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def summary_model(self) -> str:
|
|
117
|
+
"""Model to use for summarization and review tasks."""
|
|
118
|
+
return str(self._config["summary_model"])
|
|
119
|
+
|
|
120
|
+
@summary_model.setter
|
|
121
|
+
def summary_model(self, value: str) -> None:
|
|
122
|
+
self._config["summary_model"] = value
|
|
123
|
+
self._save()
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def max_iterations(self) -> int:
|
|
127
|
+
"""Maximum iterations for implementation loop."""
|
|
128
|
+
return int(self._config["max_iterations"])
|
|
129
|
+
|
|
130
|
+
@max_iterations.setter
|
|
131
|
+
def max_iterations(self, value: int) -> None:
|
|
132
|
+
self._config["max_iterations"] = max(1, value)
|
|
133
|
+
self._save()
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def auto_spec_without_oversight(self) -> bool:
|
|
137
|
+
"""Whether to auto-spec tasks without user confirmation."""
|
|
138
|
+
return bool(self._config["auto_spec_without_oversight"])
|
|
139
|
+
|
|
140
|
+
@auto_spec_without_oversight.setter
|
|
141
|
+
def auto_spec_without_oversight(self, value: bool) -> None:
|
|
142
|
+
self._config["auto_spec_without_oversight"] = value
|
|
143
|
+
self._save()
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def wait_on_rate_limit(self) -> bool:
|
|
147
|
+
"""Whether to wait and retry on rate limit."""
|
|
148
|
+
return bool(self._config["wait_on_rate_limit"])
|
|
149
|
+
|
|
150
|
+
@wait_on_rate_limit.setter
|
|
151
|
+
def wait_on_rate_limit(self, value: bool) -> None:
|
|
152
|
+
self._config["wait_on_rate_limit"] = value
|
|
153
|
+
self._save()
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def pause_on_completion(self) -> bool:
|
|
157
|
+
"""Whether to pause after completing all tasks."""
|
|
158
|
+
return bool(self._config["pause_on_completion"])
|
|
159
|
+
|
|
160
|
+
@pause_on_completion.setter
|
|
161
|
+
def pause_on_completion(self, value: bool) -> None:
|
|
162
|
+
self._config["pause_on_completion"] = value
|
|
163
|
+
self._save()
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def always_build_tests(self) -> bool:
|
|
167
|
+
"""Whether to always build tests for implementations."""
|
|
168
|
+
return bool(self._config["always_build_tests"])
|
|
169
|
+
|
|
170
|
+
@always_build_tests.setter
|
|
171
|
+
def always_build_tests(self, value: bool) -> None:
|
|
172
|
+
self._config["always_build_tests"] = value
|
|
173
|
+
self._save()
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def max_story_attempts(self) -> int:
|
|
177
|
+
"""Maximum attempts per story before marking as blocked."""
|
|
178
|
+
return int(self._config.get("max_story_attempts", 3))
|
|
179
|
+
|
|
180
|
+
@max_story_attempts.setter
|
|
181
|
+
def max_story_attempts(self, value: int) -> None:
|
|
182
|
+
self._config["max_story_attempts"] = max(1, value)
|
|
183
|
+
self._save()
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def branch_prefix(self) -> str:
|
|
187
|
+
"""Prefix for feature branch names (e.g., 'ralph' creates 'ralph/feature-name')."""
|
|
188
|
+
return str(self._config.get("branch_prefix", "ralph"))
|
|
189
|
+
|
|
190
|
+
@branch_prefix.setter
|
|
191
|
+
def branch_prefix(self, value: str) -> None:
|
|
192
|
+
self._config["branch_prefix"] = value
|
|
193
|
+
self._save()
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def on_error(self) -> ErrorMode:
|
|
197
|
+
"""Error handling mode (block, retry, pause, skip)."""
|
|
198
|
+
value: ErrorMode = self._config["on_error"]
|
|
199
|
+
return value
|
|
200
|
+
|
|
201
|
+
@on_error.setter
|
|
202
|
+
def on_error(self, value: ErrorMode) -> None:
|
|
203
|
+
if value not in ("block", "retry", "pause", "skip"):
|
|
204
|
+
raise ValueError(f"Invalid error mode: {value}")
|
|
205
|
+
self._config["on_error"] = value
|
|
206
|
+
self._save()
|
|
207
|
+
|
|
208
|
+
def to_dict(self) -> dict[str, Any]:
|
|
209
|
+
"""Return a copy of the configuration as a dictionary."""
|
|
210
|
+
return self._config.copy()
|
|
211
|
+
|
|
212
|
+
def reset_to_defaults(self) -> None:
|
|
213
|
+
"""Reset all settings to defaults."""
|
|
214
|
+
self._config = DEFAULT_CONFIG.copy()
|
|
215
|
+
self._save()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# Global config instance
|
|
219
|
+
_config: Config | None = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_config() -> Config:
|
|
223
|
+
"""Get the global configuration instance."""
|
|
224
|
+
global _config
|
|
225
|
+
if _config is None:
|
|
226
|
+
_config = Config()
|
|
227
|
+
return _config
|