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.
Files changed (93) hide show
  1. elidia/__init__.py +0 -0
  2. elidia/__main__.py +4 -0
  3. elidia/agent/__init__.py +3 -0
  4. elidia/agent/loop.py +449 -0
  5. elidia/agent/personas.py +130 -0
  6. elidia/agent/portal.py +115 -0
  7. elidia/api/__init__.py +0 -0
  8. elidia/api/client.py +221 -0
  9. elidia/api/streaming.py +55 -0
  10. elidia/auth/__init__.py +0 -0
  11. elidia/auth/keychain.py +86 -0
  12. elidia/cache/__init__.py +1 -0
  13. elidia/cache/lru.py +158 -0
  14. elidia/cli/__init__.py +0 -0
  15. elidia/cli/commands.py +122 -0
  16. elidia/cli/main.py +293 -0
  17. elidia/cli/pager.py +104 -0
  18. elidia/cli/progress.py +173 -0
  19. elidia/cli/renderer.py +32 -0
  20. elidia/cli/repl.py +1234 -0
  21. elidia/cli/themes.py +214 -0
  22. elidia/config/__init__.py +0 -0
  23. elidia/config/defaults.py +5 -0
  24. elidia/config/rules.py +48 -0
  25. elidia/config/settings.py +217 -0
  26. elidia/creative/__init__.py +1 -0
  27. elidia/creative/audio.py +213 -0
  28. elidia/creative/display.py +260 -0
  29. elidia/creative/image.py +142 -0
  30. elidia/creative/local.py +163 -0
  31. elidia/creative/video.py +181 -0
  32. elidia/daemon/__init__.py +0 -0
  33. elidia/daemon/manager.py +349 -0
  34. elidia/db/__init__.py +0 -0
  35. elidia/db/database.py +87 -0
  36. elidia/mcp/__init__.py +0 -0
  37. elidia/mcp/client.py +198 -0
  38. elidia/mcp/config.py +93 -0
  39. elidia/mcp/registry.py +101 -0
  40. elidia/mcp/types.py +43 -0
  41. elidia/memory/__init__.py +3 -0
  42. elidia/memory/auto.py +164 -0
  43. elidia/memory/compaction.py +101 -0
  44. elidia/memory/embeddings.py +47 -0
  45. elidia/memory/outcomes.py +131 -0
  46. elidia/memory/patterns.py +79 -0
  47. elidia/memory/store.py +338 -0
  48. elidia/models/__init__.py +0 -0
  49. elidia/models/adaptive.py +53 -0
  50. elidia/models/router.py +115 -0
  51. elidia/modes/__init__.py +1 -0
  52. elidia/modes/autonomous.py +318 -0
  53. elidia/modes/budget.py +182 -0
  54. elidia/modes/classifier.py +169 -0
  55. elidia/modes/consensus.py +210 -0
  56. elidia/modes/deep_think.py +190 -0
  57. elidia/modes/swarm.py +229 -0
  58. elidia/modes/thinking.py +153 -0
  59. elidia/permissions/__init__.py +0 -0
  60. elidia/permissions/audit.py +138 -0
  61. elidia/permissions/manager.py +199 -0
  62. elidia/permissions/trust.py +63 -0
  63. elidia/rag/__init__.py +4 -0
  64. elidia/rag/chunker.py +216 -0
  65. elidia/rag/engine.py +345 -0
  66. elidia/rag/ingest.py +154 -0
  67. elidia/rag/portal_bridge.py +84 -0
  68. elidia/rag/watcher.py +98 -0
  69. elidia/research/__init__.py +1 -0
  70. elidia/research/export.py +237 -0
  71. elidia/research/orchestrator.py +310 -0
  72. elidia/research/sources.py +223 -0
  73. elidia/session/__init__.py +0 -0
  74. elidia/session/history.py +119 -0
  75. elidia/session/manager.py +82 -0
  76. elidia/tools/__init__.py +28 -0
  77. elidia/tools/base.py +70 -0
  78. elidia/tools/fetch.py +100 -0
  79. elidia/tools/filesystem.py +204 -0
  80. elidia/tools/git.py +156 -0
  81. elidia/tools/search.py +99 -0
  82. elidia/tools/terminal.py +77 -0
  83. elidia/widgets/__init__.py +1 -0
  84. elidia/widgets/protocol.py +248 -0
  85. elidia/widgets/renderer.py +225 -0
  86. elidia/workflow/__init__.py +1 -0
  87. elidia/workflow/engine.py +377 -0
  88. elidia_agent_cli-0.1.0.dist-info/METADATA +344 -0
  89. elidia_agent_cli-0.1.0.dist-info/RECORD +93 -0
  90. elidia_agent_cli-0.1.0.dist-info/WHEEL +5 -0
  91. elidia_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
  92. elidia_agent_cli-0.1.0.dist-info/licenses/LICENSE +57 -0
  93. 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
@@ -0,0 +1,5 @@
1
+ from elidia.config.settings import ElidiaConfig
2
+
3
+ VERSION = "0.1.0"
4
+
5
+ DEFAULT_CONFIG = ElidiaConfig()
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."""