agent-cli 0.70.5__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.
- agent_cli/__init__.py +5 -0
- agent_cli/__main__.py +6 -0
- agent_cli/_extras.json +14 -0
- agent_cli/_requirements/.gitkeep +0 -0
- agent_cli/_requirements/audio.txt +79 -0
- agent_cli/_requirements/faster-whisper.txt +215 -0
- agent_cli/_requirements/kokoro.txt +425 -0
- agent_cli/_requirements/llm.txt +183 -0
- agent_cli/_requirements/memory.txt +355 -0
- agent_cli/_requirements/mlx-whisper.txt +222 -0
- agent_cli/_requirements/piper.txt +176 -0
- agent_cli/_requirements/rag.txt +402 -0
- agent_cli/_requirements/server.txt +154 -0
- agent_cli/_requirements/speed.txt +77 -0
- agent_cli/_requirements/vad.txt +155 -0
- agent_cli/_requirements/wyoming.txt +71 -0
- agent_cli/_tools.py +368 -0
- agent_cli/agents/__init__.py +23 -0
- agent_cli/agents/_voice_agent_common.py +136 -0
- agent_cli/agents/assistant.py +383 -0
- agent_cli/agents/autocorrect.py +284 -0
- agent_cli/agents/chat.py +496 -0
- agent_cli/agents/memory/__init__.py +31 -0
- agent_cli/agents/memory/add.py +190 -0
- agent_cli/agents/memory/proxy.py +160 -0
- agent_cli/agents/rag_proxy.py +128 -0
- agent_cli/agents/speak.py +209 -0
- agent_cli/agents/transcribe.py +671 -0
- agent_cli/agents/transcribe_daemon.py +499 -0
- agent_cli/agents/voice_edit.py +291 -0
- agent_cli/api.py +22 -0
- agent_cli/cli.py +106 -0
- agent_cli/config.py +503 -0
- agent_cli/config_cmd.py +307 -0
- agent_cli/constants.py +27 -0
- agent_cli/core/__init__.py +1 -0
- agent_cli/core/audio.py +461 -0
- agent_cli/core/audio_format.py +299 -0
- agent_cli/core/chroma.py +88 -0
- agent_cli/core/deps.py +191 -0
- agent_cli/core/openai_proxy.py +139 -0
- agent_cli/core/process.py +195 -0
- agent_cli/core/reranker.py +120 -0
- agent_cli/core/sse.py +87 -0
- agent_cli/core/transcription_logger.py +70 -0
- agent_cli/core/utils.py +526 -0
- agent_cli/core/vad.py +175 -0
- agent_cli/core/watch.py +65 -0
- agent_cli/dev/__init__.py +14 -0
- agent_cli/dev/cli.py +1588 -0
- agent_cli/dev/coding_agents/__init__.py +19 -0
- agent_cli/dev/coding_agents/aider.py +24 -0
- agent_cli/dev/coding_agents/base.py +167 -0
- agent_cli/dev/coding_agents/claude.py +39 -0
- agent_cli/dev/coding_agents/codex.py +24 -0
- agent_cli/dev/coding_agents/continue_dev.py +15 -0
- agent_cli/dev/coding_agents/copilot.py +24 -0
- agent_cli/dev/coding_agents/cursor_agent.py +48 -0
- agent_cli/dev/coding_agents/gemini.py +28 -0
- agent_cli/dev/coding_agents/opencode.py +15 -0
- agent_cli/dev/coding_agents/registry.py +49 -0
- agent_cli/dev/editors/__init__.py +19 -0
- agent_cli/dev/editors/base.py +89 -0
- agent_cli/dev/editors/cursor.py +15 -0
- agent_cli/dev/editors/emacs.py +46 -0
- agent_cli/dev/editors/jetbrains.py +56 -0
- agent_cli/dev/editors/nano.py +31 -0
- agent_cli/dev/editors/neovim.py +33 -0
- agent_cli/dev/editors/registry.py +59 -0
- agent_cli/dev/editors/sublime.py +20 -0
- agent_cli/dev/editors/vim.py +42 -0
- agent_cli/dev/editors/vscode.py +15 -0
- agent_cli/dev/editors/zed.py +20 -0
- agent_cli/dev/project.py +568 -0
- agent_cli/dev/registry.py +52 -0
- agent_cli/dev/skill/SKILL.md +141 -0
- agent_cli/dev/skill/examples.md +571 -0
- agent_cli/dev/terminals/__init__.py +19 -0
- agent_cli/dev/terminals/apple_terminal.py +82 -0
- agent_cli/dev/terminals/base.py +56 -0
- agent_cli/dev/terminals/gnome.py +51 -0
- agent_cli/dev/terminals/iterm2.py +84 -0
- agent_cli/dev/terminals/kitty.py +77 -0
- agent_cli/dev/terminals/registry.py +48 -0
- agent_cli/dev/terminals/tmux.py +58 -0
- agent_cli/dev/terminals/warp.py +132 -0
- agent_cli/dev/terminals/zellij.py +78 -0
- agent_cli/dev/worktree.py +856 -0
- agent_cli/docs_gen.py +417 -0
- agent_cli/example-config.toml +185 -0
- agent_cli/install/__init__.py +5 -0
- agent_cli/install/common.py +89 -0
- agent_cli/install/extras.py +174 -0
- agent_cli/install/hotkeys.py +48 -0
- agent_cli/install/services.py +87 -0
- agent_cli/memory/__init__.py +7 -0
- agent_cli/memory/_files.py +250 -0
- agent_cli/memory/_filters.py +63 -0
- agent_cli/memory/_git.py +157 -0
- agent_cli/memory/_indexer.py +142 -0
- agent_cli/memory/_ingest.py +408 -0
- agent_cli/memory/_persistence.py +182 -0
- agent_cli/memory/_prompt.py +91 -0
- agent_cli/memory/_retrieval.py +294 -0
- agent_cli/memory/_store.py +169 -0
- agent_cli/memory/_streaming.py +44 -0
- agent_cli/memory/_tasks.py +48 -0
- agent_cli/memory/api.py +113 -0
- agent_cli/memory/client.py +272 -0
- agent_cli/memory/engine.py +361 -0
- agent_cli/memory/entities.py +43 -0
- agent_cli/memory/models.py +112 -0
- agent_cli/opts.py +433 -0
- agent_cli/py.typed +0 -0
- agent_cli/rag/__init__.py +3 -0
- agent_cli/rag/_indexer.py +67 -0
- agent_cli/rag/_indexing.py +226 -0
- agent_cli/rag/_prompt.py +30 -0
- agent_cli/rag/_retriever.py +156 -0
- agent_cli/rag/_store.py +48 -0
- agent_cli/rag/_utils.py +218 -0
- agent_cli/rag/api.py +175 -0
- agent_cli/rag/client.py +299 -0
- agent_cli/rag/engine.py +302 -0
- agent_cli/rag/models.py +55 -0
- agent_cli/scripts/.runtime/.gitkeep +0 -0
- agent_cli/scripts/__init__.py +1 -0
- agent_cli/scripts/check_plugin_skill_sync.py +50 -0
- agent_cli/scripts/linux-hotkeys/README.md +63 -0
- agent_cli/scripts/linux-hotkeys/toggle-autocorrect.sh +45 -0
- agent_cli/scripts/linux-hotkeys/toggle-transcription.sh +58 -0
- agent_cli/scripts/linux-hotkeys/toggle-voice-edit.sh +58 -0
- agent_cli/scripts/macos-hotkeys/README.md +45 -0
- agent_cli/scripts/macos-hotkeys/skhd-config-example +5 -0
- agent_cli/scripts/macos-hotkeys/toggle-autocorrect.sh +12 -0
- agent_cli/scripts/macos-hotkeys/toggle-transcription.sh +37 -0
- agent_cli/scripts/macos-hotkeys/toggle-voice-edit.sh +37 -0
- agent_cli/scripts/nvidia-asr-server/README.md +99 -0
- agent_cli/scripts/nvidia-asr-server/pyproject.toml +27 -0
- agent_cli/scripts/nvidia-asr-server/server.py +255 -0
- agent_cli/scripts/nvidia-asr-server/shell.nix +32 -0
- agent_cli/scripts/nvidia-asr-server/uv.lock +4654 -0
- agent_cli/scripts/run-openwakeword.sh +11 -0
- agent_cli/scripts/run-piper-windows.ps1 +30 -0
- agent_cli/scripts/run-piper.sh +24 -0
- agent_cli/scripts/run-whisper-linux.sh +40 -0
- agent_cli/scripts/run-whisper-macos.sh +6 -0
- agent_cli/scripts/run-whisper-windows.ps1 +51 -0
- agent_cli/scripts/run-whisper.sh +9 -0
- agent_cli/scripts/run_faster_whisper_server.py +136 -0
- agent_cli/scripts/setup-linux-hotkeys.sh +72 -0
- agent_cli/scripts/setup-linux.sh +108 -0
- agent_cli/scripts/setup-macos-hotkeys.sh +61 -0
- agent_cli/scripts/setup-macos.sh +76 -0
- agent_cli/scripts/setup-windows.ps1 +63 -0
- agent_cli/scripts/start-all-services-windows.ps1 +53 -0
- agent_cli/scripts/start-all-services.sh +178 -0
- agent_cli/scripts/sync_extras.py +138 -0
- agent_cli/server/__init__.py +3 -0
- agent_cli/server/cli.py +721 -0
- agent_cli/server/common.py +222 -0
- agent_cli/server/model_manager.py +288 -0
- agent_cli/server/model_registry.py +225 -0
- agent_cli/server/proxy/__init__.py +3 -0
- agent_cli/server/proxy/api.py +444 -0
- agent_cli/server/streaming.py +67 -0
- agent_cli/server/tts/__init__.py +3 -0
- agent_cli/server/tts/api.py +335 -0
- agent_cli/server/tts/backends/__init__.py +82 -0
- agent_cli/server/tts/backends/base.py +139 -0
- agent_cli/server/tts/backends/kokoro.py +403 -0
- agent_cli/server/tts/backends/piper.py +253 -0
- agent_cli/server/tts/model_manager.py +201 -0
- agent_cli/server/tts/model_registry.py +28 -0
- agent_cli/server/tts/wyoming_handler.py +249 -0
- agent_cli/server/whisper/__init__.py +3 -0
- agent_cli/server/whisper/api.py +413 -0
- agent_cli/server/whisper/backends/__init__.py +89 -0
- agent_cli/server/whisper/backends/base.py +97 -0
- agent_cli/server/whisper/backends/faster_whisper.py +225 -0
- agent_cli/server/whisper/backends/mlx.py +270 -0
- agent_cli/server/whisper/languages.py +116 -0
- agent_cli/server/whisper/model_manager.py +157 -0
- agent_cli/server/whisper/model_registry.py +28 -0
- agent_cli/server/whisper/wyoming_handler.py +203 -0
- agent_cli/services/__init__.py +343 -0
- agent_cli/services/_wyoming_utils.py +64 -0
- agent_cli/services/asr.py +506 -0
- agent_cli/services/llm.py +228 -0
- agent_cli/services/tts.py +450 -0
- agent_cli/services/wake_word.py +142 -0
- agent_cli-0.70.5.dist-info/METADATA +2118 -0
- agent_cli-0.70.5.dist-info/RECORD +196 -0
- agent_cli-0.70.5.dist-info/WHEEL +4 -0
- agent_cli-0.70.5.dist-info/entry_points.txt +4 -0
- agent_cli-0.70.5.dist-info/licenses/LICENSE +21 -0
agent_cli/config.py
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""Pydantic models for agent configurations, aligned with CLI option groups."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, field_validator
|
|
10
|
+
|
|
11
|
+
from agent_cli.core.utils import console
|
|
12
|
+
|
|
13
|
+
USER_CONFIG_PATH = Path.home() / ".config" / "agent-cli" / "config.toml"
|
|
14
|
+
|
|
15
|
+
CONFIG_PATHS = [
|
|
16
|
+
Path("agent-cli-config.toml"),
|
|
17
|
+
USER_CONFIG_PATH,
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _normalize_provider_value(field: str, value: str) -> str:
|
|
22
|
+
"""Map deprecated provider names to their replacements."""
|
|
23
|
+
alias_map = _DEPRECATED_PROVIDER_ALIASES.get(field, {})
|
|
24
|
+
normalized = value.lower()
|
|
25
|
+
if normalized in alias_map:
|
|
26
|
+
replacement = alias_map[normalized]
|
|
27
|
+
console.print(
|
|
28
|
+
f"[yellow]Deprecated provider '{value}' for {field.replace('_', '-')}."
|
|
29
|
+
f" Using '{replacement}' instead.[/yellow]",
|
|
30
|
+
)
|
|
31
|
+
return replacement
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_DEPRECATED_PROVIDER_ALIASES: dict[str, dict[str, str]] = {
|
|
36
|
+
"llm_provider": {"local": "ollama"},
|
|
37
|
+
"asr_provider": {"local": "wyoming"},
|
|
38
|
+
"tts_provider": {"local": "wyoming"},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# --- Panel: Provider Selection ---
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ProviderSelection(BaseModel):
|
|
45
|
+
"""Configuration for selecting service providers."""
|
|
46
|
+
|
|
47
|
+
llm_provider: Literal["ollama", "openai", "gemini"]
|
|
48
|
+
asr_provider: Literal["wyoming", "openai", "gemini"]
|
|
49
|
+
tts_provider: Literal["wyoming", "openai", "kokoro", "gemini"]
|
|
50
|
+
|
|
51
|
+
@field_validator("llm_provider", mode="before")
|
|
52
|
+
@classmethod
|
|
53
|
+
def _normalize_llm_provider(cls, v: str) -> str:
|
|
54
|
+
if isinstance(v, str):
|
|
55
|
+
return _normalize_provider_value("llm_provider", v)
|
|
56
|
+
return v
|
|
57
|
+
|
|
58
|
+
@field_validator("asr_provider", mode="before")
|
|
59
|
+
@classmethod
|
|
60
|
+
def _normalize_asr_provider(cls, v: str) -> str:
|
|
61
|
+
if isinstance(v, str):
|
|
62
|
+
return _normalize_provider_value("asr_provider", v)
|
|
63
|
+
return v
|
|
64
|
+
|
|
65
|
+
@field_validator("tts_provider", mode="before")
|
|
66
|
+
@classmethod
|
|
67
|
+
def _normalize_tts_provider(cls, v: str) -> str:
|
|
68
|
+
if isinstance(v, str):
|
|
69
|
+
return _normalize_provider_value("tts_provider", v)
|
|
70
|
+
return v
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --- Panel: LLM Configuration ---
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Ollama(BaseModel):
|
|
77
|
+
"""Configuration for the local Ollama LLM provider."""
|
|
78
|
+
|
|
79
|
+
llm_ollama_model: str
|
|
80
|
+
llm_ollama_host: str
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class OpenAILLM(BaseModel):
|
|
84
|
+
"""Configuration for the OpenAI LLM provider."""
|
|
85
|
+
|
|
86
|
+
llm_openai_model: str
|
|
87
|
+
openai_api_key: str | None = None
|
|
88
|
+
openai_base_url: str | None = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class GeminiLLM(BaseModel):
|
|
92
|
+
"""Configuration for the Gemini LLM provider."""
|
|
93
|
+
|
|
94
|
+
llm_gemini_model: str
|
|
95
|
+
gemini_api_key: str | None = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --- Panel: ASR (Audio) Configuration ---
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AudioInput(BaseModel):
|
|
102
|
+
"""Configuration for audio input devices."""
|
|
103
|
+
|
|
104
|
+
input_device_index: int | None = None
|
|
105
|
+
input_device_name: str | None = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class WyomingASR(BaseModel):
|
|
109
|
+
"""Configuration for the Wyoming ASR provider."""
|
|
110
|
+
|
|
111
|
+
asr_wyoming_ip: str
|
|
112
|
+
asr_wyoming_port: int
|
|
113
|
+
asr_wyoming_prompt: str | None = None
|
|
114
|
+
|
|
115
|
+
def get_effective_prompt(self, extra_instructions: str | None = None) -> str | None:
|
|
116
|
+
"""Get the effective prompt, combining asr_wyoming_prompt with extra_instructions.
|
|
117
|
+
|
|
118
|
+
If both are set, asr_wyoming_prompt takes precedence and extra_instructions
|
|
119
|
+
is appended. If only one is set, that one is used.
|
|
120
|
+
"""
|
|
121
|
+
if self.asr_wyoming_prompt and extra_instructions:
|
|
122
|
+
return f"{self.asr_wyoming_prompt}\n\n{extra_instructions}"
|
|
123
|
+
return self.asr_wyoming_prompt or extra_instructions
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class OpenAIASR(BaseModel):
|
|
127
|
+
"""Configuration for the OpenAI-compatible ASR provider."""
|
|
128
|
+
|
|
129
|
+
asr_openai_model: str
|
|
130
|
+
openai_api_key: str | None = None
|
|
131
|
+
openai_base_url: str | None = None
|
|
132
|
+
asr_openai_prompt: str | None = None
|
|
133
|
+
|
|
134
|
+
def get_effective_prompt(self, extra_instructions: str | None = None) -> str | None:
|
|
135
|
+
"""Get the effective prompt, combining asr_openai_prompt with extra_instructions.
|
|
136
|
+
|
|
137
|
+
If both are set, asr_openai_prompt takes precedence and extra_instructions
|
|
138
|
+
is appended. If only one is set, that one is used.
|
|
139
|
+
"""
|
|
140
|
+
if self.asr_openai_prompt and extra_instructions:
|
|
141
|
+
return f"{self.asr_openai_prompt}\n\n{extra_instructions}"
|
|
142
|
+
return self.asr_openai_prompt or extra_instructions
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class GeminiASR(BaseModel):
|
|
146
|
+
"""Configuration for the Gemini ASR provider."""
|
|
147
|
+
|
|
148
|
+
asr_gemini_model: str
|
|
149
|
+
gemini_api_key: str | None = None
|
|
150
|
+
asr_gemini_prompt: str | None = None
|
|
151
|
+
|
|
152
|
+
def get_effective_prompt(self, extra_instructions: str | None = None) -> str | None:
|
|
153
|
+
"""Get the effective prompt, combining asr_gemini_prompt with extra_instructions.
|
|
154
|
+
|
|
155
|
+
If both are set, asr_gemini_prompt takes precedence and extra_instructions
|
|
156
|
+
is appended. If only one is set, that one is used.
|
|
157
|
+
"""
|
|
158
|
+
if self.asr_gemini_prompt and extra_instructions:
|
|
159
|
+
return f"{self.asr_gemini_prompt}\n\n{extra_instructions}"
|
|
160
|
+
return self.asr_gemini_prompt or extra_instructions
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# --- Panel: TTS (Text-to-Speech) Configuration ---
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class AudioOutput(BaseModel):
|
|
167
|
+
"""Configuration for audio output devices and TTS behavior."""
|
|
168
|
+
|
|
169
|
+
output_device_index: int | None = None
|
|
170
|
+
output_device_name: str | None = None
|
|
171
|
+
tts_speed: float = 1.0
|
|
172
|
+
enable_tts: bool = False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class WyomingTTS(BaseModel):
|
|
176
|
+
"""Configuration for the Wyoming TTS provider."""
|
|
177
|
+
|
|
178
|
+
tts_wyoming_ip: str
|
|
179
|
+
tts_wyoming_port: int
|
|
180
|
+
tts_wyoming_voice: str | None = None
|
|
181
|
+
tts_wyoming_language: str | None = None
|
|
182
|
+
tts_wyoming_speaker: str | None = None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class OpenAITTS(BaseModel):
|
|
186
|
+
"""Configuration for the OpenAI-compatible TTS provider."""
|
|
187
|
+
|
|
188
|
+
tts_openai_model: str
|
|
189
|
+
tts_openai_voice: str
|
|
190
|
+
openai_api_key: str | None = None
|
|
191
|
+
tts_openai_base_url: str | None = None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class KokoroTTS(BaseModel):
|
|
195
|
+
"""Configuration for the Kokoro TTS provider."""
|
|
196
|
+
|
|
197
|
+
tts_kokoro_model: str
|
|
198
|
+
tts_kokoro_voice: str
|
|
199
|
+
tts_kokoro_host: str
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class GeminiTTS(BaseModel):
|
|
203
|
+
"""Configuration for the Gemini TTS provider."""
|
|
204
|
+
|
|
205
|
+
tts_gemini_model: str
|
|
206
|
+
tts_gemini_voice: str
|
|
207
|
+
gemini_api_key: str | None = None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# --- Panel: Wake Word Options ---
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class WakeWord(BaseModel):
|
|
214
|
+
"""Configuration for wake word detection."""
|
|
215
|
+
|
|
216
|
+
wake_server_ip: str
|
|
217
|
+
wake_server_port: int
|
|
218
|
+
wake_word: str
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# --- Panel: General Options ---
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class General(BaseModel):
|
|
225
|
+
"""General configuration parameters for logging and I/O."""
|
|
226
|
+
|
|
227
|
+
log_level: str
|
|
228
|
+
log_file: str | None = None
|
|
229
|
+
quiet: bool
|
|
230
|
+
clipboard: bool = True
|
|
231
|
+
save_file: Path | None = None
|
|
232
|
+
list_devices: bool = False
|
|
233
|
+
|
|
234
|
+
@field_validator("save_file", mode="before")
|
|
235
|
+
@classmethod
|
|
236
|
+
def _expand_user_path(cls, v: str | None) -> Path | None:
|
|
237
|
+
if v:
|
|
238
|
+
return Path(v).expanduser()
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# --- Panel: History Options ---
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class History(BaseModel):
|
|
246
|
+
"""Configuration for conversation history."""
|
|
247
|
+
|
|
248
|
+
history_dir: Path | None = None
|
|
249
|
+
last_n_messages: int = 50
|
|
250
|
+
|
|
251
|
+
@field_validator("history_dir", mode="before")
|
|
252
|
+
@classmethod
|
|
253
|
+
def _expand_user_path(cls, v: str | None) -> Path | None:
|
|
254
|
+
if v:
|
|
255
|
+
return Path(v).expanduser()
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# --- Panel: Dev (Parallel Development) Options ---
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class Dev(BaseModel):
|
|
263
|
+
"""Configuration for parallel development environments (git worktrees)."""
|
|
264
|
+
|
|
265
|
+
default_agent: str | None = None
|
|
266
|
+
default_editor: str | None = None
|
|
267
|
+
agent_args: dict[str, list[str]] | None = (
|
|
268
|
+
None # Per-agent args, e.g. {"claude": ["--dangerously-skip-permissions"]}
|
|
269
|
+
)
|
|
270
|
+
setup: bool = True # Run project setup (npm install, etc.)
|
|
271
|
+
copy_env: bool = True # Copy .env files from main repo
|
|
272
|
+
fetch: bool = True # Git fetch before creating worktree
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _config_path(config_path_str: str | None = None) -> Path | None:
|
|
276
|
+
"""Return a usable config path, expanding user directories."""
|
|
277
|
+
if config_path_str:
|
|
278
|
+
return Path(config_path_str).expanduser().resolve()
|
|
279
|
+
|
|
280
|
+
for path in CONFIG_PATHS:
|
|
281
|
+
candidate = path.expanduser()
|
|
282
|
+
if candidate.exists():
|
|
283
|
+
return candidate.resolve()
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def load_config(config_path_str: str | None = None) -> dict[str, Any]:
|
|
288
|
+
"""Load the TOML configuration file and process it for nested structures.
|
|
289
|
+
|
|
290
|
+
Supports both flat sections like [autocorrect] and nested sections like
|
|
291
|
+
[memory.proxy]. Nested sections are flattened to dot-notation keys.
|
|
292
|
+
"""
|
|
293
|
+
# Determine which config path to use
|
|
294
|
+
config_path = _config_path(config_path_str)
|
|
295
|
+
if config_path is None:
|
|
296
|
+
return {}
|
|
297
|
+
if config_path.exists():
|
|
298
|
+
with config_path.open("rb") as f:
|
|
299
|
+
cfg = tomllib.load(f)
|
|
300
|
+
# Flatten nested sections (e.g., [memory.proxy] -> "memory.proxy")
|
|
301
|
+
flattened = _flatten_nested_sections(cfg)
|
|
302
|
+
return {k: _replace_dashed_keys(v) for k, v in flattened.items()}
|
|
303
|
+
if config_path_str:
|
|
304
|
+
console.print(
|
|
305
|
+
f"[bold red]Config file not found at {config_path_str}[/bold red]",
|
|
306
|
+
)
|
|
307
|
+
return {}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def normalize_provider_defaults(cfg: dict[str, Any]) -> dict[str, Any]:
|
|
311
|
+
"""Normalize deprecated provider names in a config section."""
|
|
312
|
+
normalized = dict(cfg)
|
|
313
|
+
for provider_key in ("llm_provider", "asr_provider", "tts_provider"):
|
|
314
|
+
if provider_key in normalized and isinstance(normalized[provider_key], str):
|
|
315
|
+
normalized[provider_key] = _normalize_provider_value(
|
|
316
|
+
provider_key,
|
|
317
|
+
normalized[provider_key],
|
|
318
|
+
)
|
|
319
|
+
return normalized
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _replace_dashed_keys(cfg: dict[str, Any]) -> dict[str, Any]:
|
|
323
|
+
return {k.replace("-", "_"): v for k, v in cfg.items()}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _flatten_nested_sections(cfg: dict[str, Any], prefix: str = "") -> dict[str, Any]:
|
|
327
|
+
"""Flatten nested TOML sections: {"a": {"b": {"x": 1}}} -> {"a.b": {"x": 1}}."""
|
|
328
|
+
result = {}
|
|
329
|
+
for key, value in cfg.items():
|
|
330
|
+
full_key = f"{prefix}.{key}" if prefix else key
|
|
331
|
+
if isinstance(value, dict) and any(isinstance(v, dict) for v in value.values()):
|
|
332
|
+
result.update(_flatten_nested_sections(value, full_key))
|
|
333
|
+
else:
|
|
334
|
+
result[full_key] = value
|
|
335
|
+
return result
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# --- Common Config Bundle ---
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class ProviderConfigs(BaseModel):
|
|
342
|
+
"""Bundle of all provider-related configs constructed from CLI parameters."""
|
|
343
|
+
|
|
344
|
+
provider: ProviderSelection
|
|
345
|
+
audio_in: AudioInput
|
|
346
|
+
wyoming_asr: WyomingASR
|
|
347
|
+
openai_asr: OpenAIASR
|
|
348
|
+
gemini_asr: GeminiASR
|
|
349
|
+
ollama: Ollama
|
|
350
|
+
openai_llm: OpenAILLM
|
|
351
|
+
gemini_llm: GeminiLLM
|
|
352
|
+
audio_out: AudioOutput
|
|
353
|
+
wyoming_tts: WyomingTTS
|
|
354
|
+
openai_tts: OpenAITTS
|
|
355
|
+
kokoro_tts: KokoroTTS
|
|
356
|
+
gemini_tts: GeminiTTS
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def create_provider_configs(
|
|
360
|
+
*,
|
|
361
|
+
# Provider selection
|
|
362
|
+
asr_provider: str,
|
|
363
|
+
llm_provider: str,
|
|
364
|
+
tts_provider: str,
|
|
365
|
+
# Audio input
|
|
366
|
+
input_device_index: int | None,
|
|
367
|
+
input_device_name: str | None,
|
|
368
|
+
# Wyoming ASR
|
|
369
|
+
asr_wyoming_ip: str,
|
|
370
|
+
asr_wyoming_port: int,
|
|
371
|
+
# OpenAI ASR
|
|
372
|
+
asr_openai_model: str,
|
|
373
|
+
asr_openai_base_url: str | None = None,
|
|
374
|
+
asr_openai_prompt: str | None = None,
|
|
375
|
+
# Gemini ASR
|
|
376
|
+
asr_gemini_model: str,
|
|
377
|
+
# Ollama LLM
|
|
378
|
+
llm_ollama_model: str,
|
|
379
|
+
llm_ollama_host: str,
|
|
380
|
+
# OpenAI LLM
|
|
381
|
+
llm_openai_model: str,
|
|
382
|
+
# Gemini LLM
|
|
383
|
+
llm_gemini_model: str,
|
|
384
|
+
# Shared API keys
|
|
385
|
+
openai_api_key: str | None,
|
|
386
|
+
openai_base_url: str | None,
|
|
387
|
+
gemini_api_key: str | None,
|
|
388
|
+
# Audio output
|
|
389
|
+
enable_tts: bool,
|
|
390
|
+
output_device_index: int | None,
|
|
391
|
+
output_device_name: str | None,
|
|
392
|
+
tts_speed: float,
|
|
393
|
+
# Wyoming TTS
|
|
394
|
+
tts_wyoming_ip: str,
|
|
395
|
+
tts_wyoming_port: int,
|
|
396
|
+
tts_wyoming_voice: str | None,
|
|
397
|
+
tts_wyoming_language: str | None,
|
|
398
|
+
tts_wyoming_speaker: str | None,
|
|
399
|
+
# OpenAI TTS
|
|
400
|
+
tts_openai_model: str,
|
|
401
|
+
tts_openai_voice: str,
|
|
402
|
+
tts_openai_base_url: str | None,
|
|
403
|
+
# Kokoro TTS
|
|
404
|
+
tts_kokoro_model: str,
|
|
405
|
+
tts_kokoro_voice: str,
|
|
406
|
+
tts_kokoro_host: str,
|
|
407
|
+
# Gemini TTS
|
|
408
|
+
tts_gemini_model: str,
|
|
409
|
+
tts_gemini_voice: str,
|
|
410
|
+
) -> ProviderConfigs:
|
|
411
|
+
"""Create all provider-related config objects from CLI parameters.
|
|
412
|
+
|
|
413
|
+
This factory function centralizes the construction of provider configs
|
|
414
|
+
to eliminate duplication across CLI commands.
|
|
415
|
+
"""
|
|
416
|
+
return ProviderConfigs(
|
|
417
|
+
provider=ProviderSelection(
|
|
418
|
+
asr_provider=asr_provider,
|
|
419
|
+
llm_provider=llm_provider,
|
|
420
|
+
tts_provider=tts_provider,
|
|
421
|
+
),
|
|
422
|
+
audio_in=AudioInput(
|
|
423
|
+
input_device_index=input_device_index,
|
|
424
|
+
input_device_name=input_device_name,
|
|
425
|
+
),
|
|
426
|
+
wyoming_asr=WyomingASR(
|
|
427
|
+
asr_wyoming_ip=asr_wyoming_ip,
|
|
428
|
+
asr_wyoming_port=asr_wyoming_port,
|
|
429
|
+
),
|
|
430
|
+
openai_asr=OpenAIASR(
|
|
431
|
+
asr_openai_model=asr_openai_model,
|
|
432
|
+
openai_api_key=openai_api_key,
|
|
433
|
+
openai_base_url=asr_openai_base_url or openai_base_url,
|
|
434
|
+
asr_openai_prompt=asr_openai_prompt,
|
|
435
|
+
),
|
|
436
|
+
gemini_asr=GeminiASR(
|
|
437
|
+
asr_gemini_model=asr_gemini_model,
|
|
438
|
+
gemini_api_key=gemini_api_key,
|
|
439
|
+
),
|
|
440
|
+
ollama=Ollama(
|
|
441
|
+
llm_ollama_model=llm_ollama_model,
|
|
442
|
+
llm_ollama_host=llm_ollama_host,
|
|
443
|
+
),
|
|
444
|
+
openai_llm=OpenAILLM(
|
|
445
|
+
llm_openai_model=llm_openai_model,
|
|
446
|
+
openai_api_key=openai_api_key,
|
|
447
|
+
openai_base_url=openai_base_url,
|
|
448
|
+
),
|
|
449
|
+
gemini_llm=GeminiLLM(
|
|
450
|
+
llm_gemini_model=llm_gemini_model,
|
|
451
|
+
gemini_api_key=gemini_api_key,
|
|
452
|
+
),
|
|
453
|
+
audio_out=AudioOutput(
|
|
454
|
+
enable_tts=enable_tts,
|
|
455
|
+
output_device_index=output_device_index,
|
|
456
|
+
output_device_name=output_device_name,
|
|
457
|
+
tts_speed=tts_speed,
|
|
458
|
+
),
|
|
459
|
+
wyoming_tts=WyomingTTS(
|
|
460
|
+
tts_wyoming_ip=tts_wyoming_ip,
|
|
461
|
+
tts_wyoming_port=tts_wyoming_port,
|
|
462
|
+
tts_wyoming_voice=tts_wyoming_voice,
|
|
463
|
+
tts_wyoming_language=tts_wyoming_language,
|
|
464
|
+
tts_wyoming_speaker=tts_wyoming_speaker,
|
|
465
|
+
),
|
|
466
|
+
openai_tts=OpenAITTS(
|
|
467
|
+
tts_openai_model=tts_openai_model,
|
|
468
|
+
tts_openai_voice=tts_openai_voice,
|
|
469
|
+
openai_api_key=openai_api_key,
|
|
470
|
+
tts_openai_base_url=tts_openai_base_url,
|
|
471
|
+
),
|
|
472
|
+
kokoro_tts=KokoroTTS(
|
|
473
|
+
tts_kokoro_model=tts_kokoro_model,
|
|
474
|
+
tts_kokoro_voice=tts_kokoro_voice,
|
|
475
|
+
tts_kokoro_host=tts_kokoro_host,
|
|
476
|
+
),
|
|
477
|
+
gemini_tts=GeminiTTS(
|
|
478
|
+
tts_gemini_model=tts_gemini_model,
|
|
479
|
+
tts_gemini_voice=tts_gemini_voice,
|
|
480
|
+
gemini_api_key=gemini_api_key,
|
|
481
|
+
),
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# Parameter names used by create_provider_configs (all keyword-only)
|
|
486
|
+
_PROVIDER_CONFIG_PARAMS = frozenset(
|
|
487
|
+
create_provider_configs.__code__.co_varnames[
|
|
488
|
+
: create_provider_configs.__code__.co_kwonlyargcount
|
|
489
|
+
],
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def create_provider_configs_from_locals(local_vars: dict[str, Any]) -> ProviderConfigs:
|
|
494
|
+
"""Create provider configs by extracting parameters from a locals() dict.
|
|
495
|
+
|
|
496
|
+
This helper enables one-line config creation in CLI commands by automatically
|
|
497
|
+
extracting the relevant parameters from the command's local variables.
|
|
498
|
+
|
|
499
|
+
Usage:
|
|
500
|
+
cfgs = config.create_provider_configs_from_locals(locals())
|
|
501
|
+
"""
|
|
502
|
+
kwargs = {k: v for k, v in local_vars.items() if k in _PROVIDER_CONFIG_PARAMS}
|
|
503
|
+
return create_provider_configs(**kwargs)
|