elidia-agent-cli 0.1.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.
- elidia/__init__.py +0 -0
- elidia/__main__.py +4 -0
- elidia/agent/__init__.py +3 -0
- elidia/agent/loop.py +449 -0
- elidia/agent/personas.py +130 -0
- elidia/agent/portal.py +115 -0
- elidia/api/__init__.py +0 -0
- elidia/api/client.py +221 -0
- elidia/api/streaming.py +55 -0
- elidia/auth/__init__.py +0 -0
- elidia/auth/keychain.py +86 -0
- elidia/cache/__init__.py +1 -0
- elidia/cache/lru.py +158 -0
- elidia/cli/__init__.py +0 -0
- elidia/cli/commands.py +122 -0
- elidia/cli/main.py +293 -0
- elidia/cli/pager.py +104 -0
- elidia/cli/progress.py +173 -0
- elidia/cli/renderer.py +32 -0
- elidia/cli/repl.py +1234 -0
- elidia/cli/themes.py +214 -0
- elidia/config/__init__.py +0 -0
- elidia/config/defaults.py +5 -0
- elidia/config/rules.py +48 -0
- elidia/config/settings.py +217 -0
- elidia/creative/__init__.py +1 -0
- elidia/creative/audio.py +213 -0
- elidia/creative/display.py +260 -0
- elidia/creative/image.py +142 -0
- elidia/creative/local.py +163 -0
- elidia/creative/video.py +181 -0
- elidia/daemon/__init__.py +0 -0
- elidia/daemon/manager.py +349 -0
- elidia/db/__init__.py +0 -0
- elidia/db/database.py +87 -0
- elidia/mcp/__init__.py +0 -0
- elidia/mcp/client.py +198 -0
- elidia/mcp/config.py +93 -0
- elidia/mcp/registry.py +101 -0
- elidia/mcp/types.py +43 -0
- elidia/memory/__init__.py +3 -0
- elidia/memory/auto.py +164 -0
- elidia/memory/compaction.py +101 -0
- elidia/memory/embeddings.py +47 -0
- elidia/memory/outcomes.py +131 -0
- elidia/memory/patterns.py +79 -0
- elidia/memory/store.py +338 -0
- elidia/models/__init__.py +0 -0
- elidia/models/adaptive.py +53 -0
- elidia/models/router.py +115 -0
- elidia/modes/__init__.py +1 -0
- elidia/modes/autonomous.py +318 -0
- elidia/modes/budget.py +182 -0
- elidia/modes/classifier.py +169 -0
- elidia/modes/consensus.py +210 -0
- elidia/modes/deep_think.py +190 -0
- elidia/modes/swarm.py +229 -0
- elidia/modes/thinking.py +153 -0
- elidia/permissions/__init__.py +0 -0
- elidia/permissions/audit.py +138 -0
- elidia/permissions/manager.py +199 -0
- elidia/permissions/trust.py +63 -0
- elidia/rag/__init__.py +4 -0
- elidia/rag/chunker.py +216 -0
- elidia/rag/engine.py +345 -0
- elidia/rag/ingest.py +154 -0
- elidia/rag/portal_bridge.py +84 -0
- elidia/rag/watcher.py +98 -0
- elidia/research/__init__.py +1 -0
- elidia/research/export.py +237 -0
- elidia/research/orchestrator.py +310 -0
- elidia/research/sources.py +223 -0
- elidia/session/__init__.py +0 -0
- elidia/session/history.py +119 -0
- elidia/session/manager.py +82 -0
- elidia/tools/__init__.py +28 -0
- elidia/tools/base.py +70 -0
- elidia/tools/fetch.py +100 -0
- elidia/tools/filesystem.py +204 -0
- elidia/tools/git.py +156 -0
- elidia/tools/search.py +99 -0
- elidia/tools/terminal.py +77 -0
- elidia/widgets/__init__.py +1 -0
- elidia/widgets/protocol.py +248 -0
- elidia/widgets/renderer.py +225 -0
- elidia/workflow/__init__.py +1 -0
- elidia/workflow/engine.py +377 -0
- elidia_agent_cli-0.1.0.dist-info/METADATA +344 -0
- elidia_agent_cli-0.1.0.dist-info/RECORD +93 -0
- elidia_agent_cli-0.1.0.dist-info/WHEEL +5 -0
- elidia_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- elidia_agent_cli-0.1.0.dist-info/licenses/LICENSE +57 -0
- elidia_agent_cli-0.1.0.dist-info/top_level.txt +1 -0
elidia/cli/themes.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Configurable themes — dark/light/custom color schemes for the CLI.
|
|
2
|
+
|
|
3
|
+
Themes define colors for all UI elements: prompts, borders, status,
|
|
4
|
+
code blocks, errors, etc. Users can select a built-in theme or define
|
|
5
|
+
custom themes in ~/.elidia/themes.toml.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.style import Style
|
|
15
|
+
from rich.theme import Theme
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ElidiaTheme:
|
|
22
|
+
name: str
|
|
23
|
+
description: str = ""
|
|
24
|
+
primary: str = "cyan"
|
|
25
|
+
secondary: str = "blue"
|
|
26
|
+
accent: str = "magenta"
|
|
27
|
+
success: str = "green"
|
|
28
|
+
warning: str = "yellow"
|
|
29
|
+
error: str = "red"
|
|
30
|
+
dim: str = "dim"
|
|
31
|
+
border: str = "cyan"
|
|
32
|
+
prompt_style: str = "bold cyan"
|
|
33
|
+
assistant_style: str = ""
|
|
34
|
+
code_style: str = "dim"
|
|
35
|
+
heading_style: str = "bold"
|
|
36
|
+
status_style: str = "dim"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
BUILTIN_THEMES: dict[str, ElidiaTheme] = {
|
|
40
|
+
"default": ElidiaTheme(
|
|
41
|
+
name="default",
|
|
42
|
+
description="Default Elidia theme",
|
|
43
|
+
),
|
|
44
|
+
"dark": ElidiaTheme(
|
|
45
|
+
name="dark",
|
|
46
|
+
description="High contrast dark theme",
|
|
47
|
+
primary="bright_cyan",
|
|
48
|
+
secondary="bright_blue",
|
|
49
|
+
accent="bright_magenta",
|
|
50
|
+
border="bright_cyan",
|
|
51
|
+
prompt_style="bold bright_cyan",
|
|
52
|
+
),
|
|
53
|
+
"light": ElidiaTheme(
|
|
54
|
+
name="light",
|
|
55
|
+
description="Light terminal theme",
|
|
56
|
+
primary="blue",
|
|
57
|
+
secondary="dark_blue",
|
|
58
|
+
accent="dark_magenta",
|
|
59
|
+
success="dark_green",
|
|
60
|
+
warning="dark_orange",
|
|
61
|
+
error="dark_red",
|
|
62
|
+
border="blue",
|
|
63
|
+
prompt_style="bold blue",
|
|
64
|
+
),
|
|
65
|
+
"minimal": ElidiaTheme(
|
|
66
|
+
name="minimal",
|
|
67
|
+
description="Clean, low-color theme",
|
|
68
|
+
primary="white",
|
|
69
|
+
secondary="white",
|
|
70
|
+
accent="white",
|
|
71
|
+
success="green",
|
|
72
|
+
warning="yellow",
|
|
73
|
+
error="red",
|
|
74
|
+
border="white",
|
|
75
|
+
prompt_style="bold",
|
|
76
|
+
dim="dim white",
|
|
77
|
+
),
|
|
78
|
+
"ocean": ElidiaTheme(
|
|
79
|
+
name="ocean",
|
|
80
|
+
description="Cool blue-green palette",
|
|
81
|
+
primary="deep_sky_blue1",
|
|
82
|
+
secondary="steel_blue",
|
|
83
|
+
accent="medium_spring_green",
|
|
84
|
+
success="spring_green2",
|
|
85
|
+
warning="gold1",
|
|
86
|
+
error="indian_red1",
|
|
87
|
+
border="deep_sky_blue1",
|
|
88
|
+
prompt_style="bold deep_sky_blue1",
|
|
89
|
+
),
|
|
90
|
+
"sunset": ElidiaTheme(
|
|
91
|
+
name="sunset",
|
|
92
|
+
description="Warm orange-red palette",
|
|
93
|
+
primary="orange1",
|
|
94
|
+
secondary="dark_orange",
|
|
95
|
+
accent="hot_pink",
|
|
96
|
+
success="chartreuse1",
|
|
97
|
+
warning="gold1",
|
|
98
|
+
error="red1",
|
|
99
|
+
border="orange1",
|
|
100
|
+
prompt_style="bold orange1",
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ThemeManager:
|
|
106
|
+
"""Manages theme selection and application."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, theme_name: str = "default") -> None:
|
|
109
|
+
logger.debug(f"Entered into ThemeManager.__init__: theme={theme_name}")
|
|
110
|
+
self._current_name = theme_name
|
|
111
|
+
self._current = BUILTIN_THEMES.get(theme_name, BUILTIN_THEMES["default"])
|
|
112
|
+
self._custom_themes: dict[str, ElidiaTheme] = {}
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def current(self) -> ElidiaTheme:
|
|
116
|
+
return self._current
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def current_name(self) -> str:
|
|
120
|
+
return self._current_name
|
|
121
|
+
|
|
122
|
+
def set_theme(self, name: str) -> ElidiaTheme | None:
|
|
123
|
+
logger.debug(f"Entered into set_theme: name={name}")
|
|
124
|
+
theme = BUILTIN_THEMES.get(name) or self._custom_themes.get(name)
|
|
125
|
+
if theme:
|
|
126
|
+
self._current = theme
|
|
127
|
+
self._current_name = name
|
|
128
|
+
return theme
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def list_themes(self) -> list[dict[str, str]]:
|
|
132
|
+
logger.debug("Entered into list_themes")
|
|
133
|
+
result: list[dict[str, str]] = []
|
|
134
|
+
for name, theme in BUILTIN_THEMES.items():
|
|
135
|
+
result.append({
|
|
136
|
+
"name": name,
|
|
137
|
+
"description": theme.description,
|
|
138
|
+
"type": "builtin",
|
|
139
|
+
})
|
|
140
|
+
for name, theme in self._custom_themes.items():
|
|
141
|
+
result.append({
|
|
142
|
+
"name": name,
|
|
143
|
+
"description": theme.description,
|
|
144
|
+
"type": "custom",
|
|
145
|
+
})
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
def load_custom_themes(self, path: Path) -> int:
|
|
149
|
+
logger.debug(f"Entered into load_custom_themes: path={path}")
|
|
150
|
+
if not path.exists():
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
import tomllib
|
|
155
|
+
except ImportError:
|
|
156
|
+
try:
|
|
157
|
+
import tomli as tomllib
|
|
158
|
+
except ImportError:
|
|
159
|
+
logger.warning("tomllib/tomli not available for custom theme loading")
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.warning(f"Failed to parse themes file: {e}")
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
count = 0
|
|
169
|
+
for name, raw in data.items():
|
|
170
|
+
if not isinstance(raw, dict):
|
|
171
|
+
continue
|
|
172
|
+
theme = ElidiaTheme(
|
|
173
|
+
name=name,
|
|
174
|
+
description=raw.get("description", ""),
|
|
175
|
+
primary=raw.get("primary", "cyan"),
|
|
176
|
+
secondary=raw.get("secondary", "blue"),
|
|
177
|
+
accent=raw.get("accent", "magenta"),
|
|
178
|
+
success=raw.get("success", "green"),
|
|
179
|
+
warning=raw.get("warning", "yellow"),
|
|
180
|
+
error=raw.get("error", "red"),
|
|
181
|
+
dim=raw.get("dim", "dim"),
|
|
182
|
+
border=raw.get("border", "cyan"),
|
|
183
|
+
prompt_style=raw.get("prompt_style", "bold cyan"),
|
|
184
|
+
assistant_style=raw.get("assistant_style", ""),
|
|
185
|
+
code_style=raw.get("code_style", "dim"),
|
|
186
|
+
heading_style=raw.get("heading_style", "bold"),
|
|
187
|
+
status_style=raw.get("status_style", "dim"),
|
|
188
|
+
)
|
|
189
|
+
self._custom_themes[name] = theme
|
|
190
|
+
count += 1
|
|
191
|
+
|
|
192
|
+
return count
|
|
193
|
+
|
|
194
|
+
def to_rich_theme(self) -> Theme:
|
|
195
|
+
logger.debug("Entered into to_rich_theme")
|
|
196
|
+
t = self._current
|
|
197
|
+
return Theme({
|
|
198
|
+
"elidia.primary": Style.parse(t.primary),
|
|
199
|
+
"elidia.secondary": Style.parse(t.secondary),
|
|
200
|
+
"elidia.accent": Style.parse(t.accent),
|
|
201
|
+
"elidia.success": Style.parse(t.success),
|
|
202
|
+
"elidia.warning": Style.parse(t.warning),
|
|
203
|
+
"elidia.error": Style.parse(t.error),
|
|
204
|
+
"elidia.dim": Style.parse(t.dim),
|
|
205
|
+
"elidia.border": Style.parse(t.border),
|
|
206
|
+
"elidia.prompt": Style.parse(t.prompt_style),
|
|
207
|
+
"elidia.heading": Style.parse(t.heading_style),
|
|
208
|
+
"elidia.status": Style.parse(t.status_style),
|
|
209
|
+
"elidia.code": Style.parse(t.code_style),
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
def create_console(self) -> Console:
|
|
213
|
+
logger.debug("Entered into create_console")
|
|
214
|
+
return Console(theme=self.to_rich_theme())
|
|
File without changes
|
elidia/config/rules.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
RULES_FILENAMES = [".elidia/rules.md", ".elidia/rules.txt", ".elidia/RULES.md"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_project_rules(project_root: Path | None = None) -> str | None:
|
|
10
|
+
logger.debug(f"Entered into load_project_rules: root={project_root}")
|
|
11
|
+
|
|
12
|
+
if project_root is None:
|
|
13
|
+
project_root = _find_project_root()
|
|
14
|
+
if project_root is None:
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
for filename in RULES_FILENAMES:
|
|
18
|
+
rules_path = project_root / filename
|
|
19
|
+
if rules_path.exists() and rules_path.is_file():
|
|
20
|
+
try:
|
|
21
|
+
content = rules_path.read_text(encoding="utf-8")
|
|
22
|
+
if content.strip():
|
|
23
|
+
logger.info(f"Loaded project rules from {rules_path}")
|
|
24
|
+
return content.strip()
|
|
25
|
+
except Exception as e:
|
|
26
|
+
logger.warning(f"Could not read {rules_path}: {e}")
|
|
27
|
+
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _find_project_root() -> Path | None:
|
|
32
|
+
cwd = Path.cwd()
|
|
33
|
+
for parent in [cwd] + list(cwd.parents):
|
|
34
|
+
if (parent / ".git").exists():
|
|
35
|
+
return parent
|
|
36
|
+
if (parent / ".elidia").exists():
|
|
37
|
+
return parent
|
|
38
|
+
return cwd
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_rules_for_system_prompt(rules: str) -> str:
|
|
42
|
+
logger.debug(f"Entered into format_rules_for_system_prompt: len={len(rules)}")
|
|
43
|
+
return (
|
|
44
|
+
"\n\n--- Project Rules (from .elidia/rules.md) ---\n"
|
|
45
|
+
"The following rules are set by the project owner. Follow them.\n\n"
|
|
46
|
+
f"{rules}\n"
|
|
47
|
+
"\n--- End Project Rules ---\n"
|
|
48
|
+
)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import tomllib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
ELIDIA_HOME = Path(os.environ.get("ELIDIA_HOME", Path.home() / ".elidia"))
|
|
10
|
+
PROJECT_DIR = Path(".elidia")
|
|
11
|
+
|
|
12
|
+
_HOME_SUBDIRS = ("cache", "memory", "rag", "skills", "workflows", "prompts", "plugins")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclasses.dataclass
|
|
16
|
+
class ApiConfig:
|
|
17
|
+
key: str = ""
|
|
18
|
+
base_url: str = "https://developer.aiutils.io/v1"
|
|
19
|
+
timeout_seconds: int = 120
|
|
20
|
+
max_retries: int = 3
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclasses.dataclass
|
|
24
|
+
class ModelConfig:
|
|
25
|
+
default: str = "auto"
|
|
26
|
+
reasoning: str = "deepseek-reasoner"
|
|
27
|
+
code: str = "claude-sonnet-5"
|
|
28
|
+
creative: str = "gpt-5"
|
|
29
|
+
cheap: str = "deepseek-chat"
|
|
30
|
+
vision: str = "gpt-5-vision"
|
|
31
|
+
embedding: str = "local"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclasses.dataclass
|
|
35
|
+
class LocalModelConfig:
|
|
36
|
+
enabled: bool = True
|
|
37
|
+
ollama_url: str = "http://localhost:11434"
|
|
38
|
+
prefer_local_for: list[str] = dataclasses.field(default_factory=lambda: ["simple-chat", "summarize"])
|
|
39
|
+
auto_fallback: bool = True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclasses.dataclass
|
|
43
|
+
class MemoryConfig:
|
|
44
|
+
enabled: bool = True
|
|
45
|
+
auto_save: bool = True
|
|
46
|
+
cloud_sync: bool = False
|
|
47
|
+
max_entries: int = 50000
|
|
48
|
+
compaction_after_messages: int = 100
|
|
49
|
+
similarity_threshold: float = 0.30
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclasses.dataclass
|
|
53
|
+
class RagConfig:
|
|
54
|
+
enabled: bool = True
|
|
55
|
+
auto_index: bool = True
|
|
56
|
+
watch_extensions: list[str] = dataclasses.field(
|
|
57
|
+
default_factory=lambda: ["md", "txt", "pdf", "py", "ts", "js", "rs", "go", "json", "yaml", "toml"]
|
|
58
|
+
)
|
|
59
|
+
chunk_size: int = 512
|
|
60
|
+
chunk_overlap: int = 64
|
|
61
|
+
portal_bridge: bool = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclasses.dataclass
|
|
65
|
+
class PermissionConfig:
|
|
66
|
+
auto_approve_reads: bool = True
|
|
67
|
+
auto_approve_writes: bool = False
|
|
68
|
+
auto_approve_commands: bool = False
|
|
69
|
+
auto_approve_network: bool = True
|
|
70
|
+
progressive_trust: bool = True
|
|
71
|
+
trust_threshold: int = 20
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclasses.dataclass
|
|
75
|
+
class BudgetConfig:
|
|
76
|
+
session_limit: int = 0
|
|
77
|
+
monthly_limit: int = 0
|
|
78
|
+
warn_at_percentage: int = 80
|
|
79
|
+
default_thinking_level: str = "auto"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclasses.dataclass
|
|
83
|
+
class UiConfig:
|
|
84
|
+
theme: str = "auto"
|
|
85
|
+
markdown: bool = True
|
|
86
|
+
image_display: bool = True
|
|
87
|
+
syntax_highlighting: bool = True
|
|
88
|
+
max_output_lines: int = 500
|
|
89
|
+
keybindings: str = "emacs"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclasses.dataclass
|
|
93
|
+
class ElidiaConfig:
|
|
94
|
+
api: ApiConfig = dataclasses.field(default_factory=ApiConfig)
|
|
95
|
+
models: ModelConfig = dataclasses.field(default_factory=ModelConfig)
|
|
96
|
+
local_models: LocalModelConfig = dataclasses.field(default_factory=LocalModelConfig)
|
|
97
|
+
memory: MemoryConfig = dataclasses.field(default_factory=MemoryConfig)
|
|
98
|
+
rag: RagConfig = dataclasses.field(default_factory=RagConfig)
|
|
99
|
+
permissions: PermissionConfig = dataclasses.field(default_factory=PermissionConfig)
|
|
100
|
+
budget: BudgetConfig = dataclasses.field(default_factory=BudgetConfig)
|
|
101
|
+
ui: UiConfig = dataclasses.field(default_factory=UiConfig)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# env var -> (dataclass field on ElidiaConfig, field on the nested section)
|
|
105
|
+
_ENV_OVERRIDES: dict[str, tuple[str, str]] = {
|
|
106
|
+
"AIUTILS_API_KEY": ("api", "key"),
|
|
107
|
+
"ELIDIA_API_BASE_URL": ("api", "base_url"),
|
|
108
|
+
"ELIDIA_API_TIMEOUT_SECONDS": ("api", "timeout_seconds"),
|
|
109
|
+
"ELIDIA_API_MAX_RETRIES": ("api", "max_retries"),
|
|
110
|
+
"ELIDIA_MODEL": ("models", "default"),
|
|
111
|
+
"ELIDIA_MODEL_REASONING": ("models", "reasoning"),
|
|
112
|
+
"ELIDIA_MODEL_CODE": ("models", "code"),
|
|
113
|
+
"ELIDIA_MODEL_CREATIVE": ("models", "creative"),
|
|
114
|
+
"ELIDIA_MODEL_CHEAP": ("models", "cheap"),
|
|
115
|
+
"ELIDIA_MODEL_VISION": ("models", "vision"),
|
|
116
|
+
"ELIDIA_OLLAMA_URL": ("local_models", "ollama_url"),
|
|
117
|
+
"ELIDIA_LOCAL_MODELS_ENABLED": ("local_models", "enabled"),
|
|
118
|
+
"ELIDIA_THEME": ("ui", "theme"),
|
|
119
|
+
"ELIDIA_SESSION_BUDGET": ("budget", "session_limit"),
|
|
120
|
+
"ELIDIA_MONTHLY_BUDGET": ("budget", "monthly_limit"),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def ensure_elidia_home() -> None:
|
|
125
|
+
logger.debug(f"Entered into ensure_elidia_home: home={ELIDIA_HOME}")
|
|
126
|
+
ELIDIA_HOME.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
for subdir in _HOME_SUBDIRS:
|
|
128
|
+
(ELIDIA_HOME / subdir).mkdir(parents=True, exist_ok=True)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _merge_toml_into_dataclass(data: dict, dc: object) -> object:
|
|
132
|
+
logger.debug(f"Entered into _merge_toml_into_dataclass: keys={list(data.keys())}")
|
|
133
|
+
for field in dataclasses.fields(dc):
|
|
134
|
+
if field.name not in data:
|
|
135
|
+
continue
|
|
136
|
+
value = data[field.name]
|
|
137
|
+
current = getattr(dc, field.name)
|
|
138
|
+
if dataclasses.is_dataclass(current) and isinstance(value, dict):
|
|
139
|
+
_merge_toml_into_dataclass(value, current)
|
|
140
|
+
else:
|
|
141
|
+
setattr(dc, field.name, value)
|
|
142
|
+
return dc
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _load_toml_file(path: Path) -> dict:
|
|
146
|
+
logger.debug(f"Entered into _load_toml_file: path={path}")
|
|
147
|
+
try:
|
|
148
|
+
with path.open("rb") as f:
|
|
149
|
+
return tomllib.load(f)
|
|
150
|
+
except (tomllib.TOMLDecodeError, OSError) as exc:
|
|
151
|
+
logger.warning(f"Entered into _load_toml_file: failed to parse {path}: {exc}")
|
|
152
|
+
return {}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _apply_env_overrides(config: ElidiaConfig) -> None:
|
|
156
|
+
logger.debug("Entered into _apply_env_overrides: applying environment variable overrides")
|
|
157
|
+
for env_var, (section_name, field_name) in _ENV_OVERRIDES.items():
|
|
158
|
+
raw_value = os.environ.get(env_var)
|
|
159
|
+
if raw_value is None:
|
|
160
|
+
continue
|
|
161
|
+
section = getattr(config, section_name)
|
|
162
|
+
current_value = getattr(section, field_name)
|
|
163
|
+
if isinstance(current_value, bool):
|
|
164
|
+
coerced_value: object = raw_value.strip().lower() in {"1", "true", "yes", "on"}
|
|
165
|
+
elif isinstance(current_value, int):
|
|
166
|
+
coerced_value = int(raw_value)
|
|
167
|
+
elif isinstance(current_value, float):
|
|
168
|
+
coerced_value = float(raw_value)
|
|
169
|
+
else:
|
|
170
|
+
coerced_value = raw_value
|
|
171
|
+
setattr(section, field_name, coerced_value)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def load_config() -> ElidiaConfig:
|
|
175
|
+
logger.debug("Entered into load_config: loading configuration")
|
|
176
|
+
ensure_elidia_home()
|
|
177
|
+
config = ElidiaConfig()
|
|
178
|
+
|
|
179
|
+
user_config_path = ELIDIA_HOME / "config.toml"
|
|
180
|
+
if user_config_path.exists():
|
|
181
|
+
_merge_toml_into_dataclass(_load_toml_file(user_config_path), config)
|
|
182
|
+
|
|
183
|
+
project_config_path = PROJECT_DIR / "project.toml"
|
|
184
|
+
if project_config_path.exists():
|
|
185
|
+
_merge_toml_into_dataclass(_load_toml_file(project_config_path), config)
|
|
186
|
+
|
|
187
|
+
_apply_env_overrides(config)
|
|
188
|
+
return config
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _toml_value(value: object) -> str:
|
|
192
|
+
if isinstance(value, bool):
|
|
193
|
+
return "true" if value else "false"
|
|
194
|
+
if isinstance(value, int):
|
|
195
|
+
return str(value)
|
|
196
|
+
if isinstance(value, float):
|
|
197
|
+
return repr(value)
|
|
198
|
+
if isinstance(value, str):
|
|
199
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
200
|
+
return f'"{escaped}"'
|
|
201
|
+
if isinstance(value, list):
|
|
202
|
+
return "[" + ", ".join(_toml_value(item) for item in value) + "]"
|
|
203
|
+
raise TypeError(f"Unsupported TOML value type: {type(value)!r}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def save_config(config: ElidiaConfig, path: Path) -> None:
|
|
207
|
+
logger.debug(f"Entered into save_config: path={path}")
|
|
208
|
+
lines: list[str] = []
|
|
209
|
+
for section_field in dataclasses.fields(config):
|
|
210
|
+
section = getattr(config, section_field.name)
|
|
211
|
+
lines.append(f"[{section_field.name}]")
|
|
212
|
+
for value_field in dataclasses.fields(section):
|
|
213
|
+
value = getattr(section, value_field.name)
|
|
214
|
+
lines.append(f"{value_field.name} = {_toml_value(value)}")
|
|
215
|
+
lines.append("")
|
|
216
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Creative generation — image, video, audio via AiUtils API and local models."""
|