kolega-code 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.
- kolega_code/__init__.py +151 -0
- kolega_code/agent/__init__.py +42 -0
- kolega_code/agent/baseagent.py +998 -0
- kolega_code/agent/browseragent.py +123 -0
- kolega_code/agent/coder.py +157 -0
- kolega_code/agent/common.py +41 -0
- kolega_code/agent/compression.py +81 -0
- kolega_code/agent/context.py +112 -0
- kolega_code/agent/conversation.py +408 -0
- kolega_code/agent/generalagent.py +146 -0
- kolega_code/agent/investigationagent.py +123 -0
- kolega_code/agent/planningagent.py +187 -0
- kolega_code/agent/prompt_provider.py +196 -0
- kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
- kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
- kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
- kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
- kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
- kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
- kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
- kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
- kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
- kolega_code/agent/prompts.py +192 -0
- kolega_code/agent/tests/__init__.py +0 -0
- kolega_code/agent/tests/llm/__init__.py +0 -0
- kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
- kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
- kolega_code/agent/tests/llm/test_client.py +773 -0
- kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
- kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
- kolega_code/agent/tests/llm/test_exceptions.py +249 -0
- kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
- kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
- kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
- kolega_code/agent/tests/llm/test_model_specs.py +17 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
- kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
- kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
- kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
- kolega_code/agent/tests/services/__init__.py +1 -0
- kolega_code/agent/tests/services/test_browser.py +447 -0
- kolega_code/agent/tests/services/test_browser_parity.py +353 -0
- kolega_code/agent/tests/services/test_file_system.py +699 -0
- kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
- kolega_code/agent/tests/services/test_terminal.py +154 -0
- kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
- kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
- kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
- kolega_code/agent/tests/test_base_agent.py +1942 -0
- kolega_code/agent/tests/test_coder_attachments.py +330 -0
- kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
- kolega_code/agent/tests/test_commands.py +179 -0
- kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
- kolega_code/agent/tests/test_empty_message_handling.py +48 -0
- kolega_code/agent/tests/test_general_agent.py +242 -0
- kolega_code/agent/tests/test_html.py +320 -0
- kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
- kolega_code/agent/tests/test_planning_agent.py +227 -0
- kolega_code/agent/tests/test_prompt_provider.py +271 -0
- kolega_code/agent/tests/test_tool_registry.py +102 -0
- kolega_code/agent/tests/test_tools.py +549 -0
- kolega_code/agent/tests/tool_backend/__init__.py +0 -0
- kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
- kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
- kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
- kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
- kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
- kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
- kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
- kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
- kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
- kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
- kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
- kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
- kolega_code/agent/tool_backend/agent_tool.py +414 -0
- kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
- kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
- kolega_code/agent/tool_backend/base_tool.py +217 -0
- kolega_code/agent/tool_backend/browser_tool.py +271 -0
- kolega_code/agent/tool_backend/build_tool.py +93 -0
- kolega_code/agent/tool_backend/create_file_tool.py +52 -0
- kolega_code/agent/tool_backend/glob_tool.py +323 -0
- kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
- kolega_code/agent/tool_backend/memory_tool.py +79 -0
- kolega_code/agent/tool_backend/read_file_tool.py +119 -0
- kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
- kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
- kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
- kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
- kolega_code/agent/tool_backend/streaming_tool.py +47 -0
- kolega_code/agent/tool_backend/terminal_tool.py +643 -0
- kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
- kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
- kolega_code/agent/tools.py +1704 -0
- kolega_code/agent/utils/commands.py +94 -0
- kolega_code/cli/__init__.py +1 -0
- kolega_code/cli/app.py +2756 -0
- kolega_code/cli/config.py +280 -0
- kolega_code/cli/connection.py +49 -0
- kolega_code/cli/file_index.py +147 -0
- kolega_code/cli/main.py +564 -0
- kolega_code/cli/mentions.py +155 -0
- kolega_code/cli/messages.py +89 -0
- kolega_code/cli/provider_registry.py +96 -0
- kolega_code/cli/session_store.py +207 -0
- kolega_code/cli/settings.py +87 -0
- kolega_code/cli/skills.py +409 -0
- kolega_code/cli/slash_commands.py +108 -0
- kolega_code/cli/tests/__init__.py +1 -0
- kolega_code/cli/tests/test_app.py +4251 -0
- kolega_code/cli/tests/test_cli_config.py +171 -0
- kolega_code/cli/tests/test_connection.py +26 -0
- kolega_code/cli/tests/test_file_index.py +103 -0
- kolega_code/cli/tests/test_main.py +455 -0
- kolega_code/cli/tests/test_mentions.py +108 -0
- kolega_code/cli/tests/test_session_store.py +67 -0
- kolega_code/cli/tests/test_settings.py +62 -0
- kolega_code/cli/tests/test_skills.py +157 -0
- kolega_code/cli/tests/test_slash_commands.py +88 -0
- kolega_code/cli/theme.py +180 -0
- kolega_code/config.py +154 -0
- kolega_code/events.py +202 -0
- kolega_code/llm/client.py +300 -0
- kolega_code/llm/exceptions.py +285 -0
- kolega_code/llm/instrumented_client.py +520 -0
- kolega_code/llm/models.py +1368 -0
- kolega_code/llm/providers/__init__.py +0 -0
- kolega_code/llm/providers/anthropic.py +387 -0
- kolega_code/llm/providers/base.py +71 -0
- kolega_code/llm/providers/google.py +157 -0
- kolega_code/llm/providers/models.py +37 -0
- kolega_code/llm/providers/openai.py +363 -0
- kolega_code/llm/ratelimit.py +40 -0
- kolega_code/llm/specs.py +67 -0
- kolega_code/llm/tool_execution_ids.py +18 -0
- kolega_code/models/__init__.py +9 -0
- kolega_code/models/sandbox_terminal_state.py +47 -0
- kolega_code/runtime.py +50 -0
- kolega_code/sandbox/README.md +200 -0
- kolega_code/sandbox/__init__.py +21 -0
- kolega_code/sandbox/async_filesystem.py +475 -0
- kolega_code/sandbox/base.py +297 -0
- kolega_code/sandbox/browser.py +25 -0
- kolega_code/sandbox/event_loop.py +43 -0
- kolega_code/sandbox/filesystem.py +341 -0
- kolega_code/sandbox/local.py +118 -0
- kolega_code/sandbox/serializer.py +175 -0
- kolega_code/sandbox/terminal.py +868 -0
- kolega_code/sandbox/utils.py +216 -0
- kolega_code/services/base.py +255 -0
- kolega_code/services/browser.py +444 -0
- kolega_code/services/file_system.py +749 -0
- kolega_code/services/html.py +221 -0
- kolega_code/services/terminal.py +903 -0
- kolega_code/tools/__init__.py +22 -0
- kolega_code/tools/core.py +33 -0
- kolega_code/tools/definitions.py +81 -0
- kolega_code/tools/registry.py +73 -0
- kolega_code-0.1.0.dist-info/METADATA +157 -0
- kolega_code-0.1.0.dist-info/RECORD +171 -0
- kolega_code-0.1.0.dist-info/WHEEL +4 -0
- kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
- kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Configuration helpers for the Kolega Code CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Mapping, Optional
|
|
9
|
+
|
|
10
|
+
from dotenv import dotenv_values
|
|
11
|
+
|
|
12
|
+
from kolega_code.config import AgentConfig, ModelConfig, ModelProvider, RateLimitConfig
|
|
13
|
+
from kolega_code.llm.specs import get_model_specs
|
|
14
|
+
|
|
15
|
+
from .provider_registry import UI_DEFAULT_PROVIDER, default_model_for_provider
|
|
16
|
+
from .settings import CliSettings
|
|
17
|
+
|
|
18
|
+
DEFAULT_LONG_PROVIDER = ModelProvider.ANTHROPIC
|
|
19
|
+
DEFAULT_LONG_MODEL = "claude-opus-4-7"
|
|
20
|
+
DEFAULT_FAST_PROVIDER = ModelProvider.ANTHROPIC
|
|
21
|
+
DEFAULT_FAST_MODEL = "claude-haiku-4-5-20251001"
|
|
22
|
+
DEFAULT_EDIT_PROVIDER = ModelProvider.ANTHROPIC
|
|
23
|
+
DEFAULT_EDIT_MODEL = "claude-sonnet-4-6"
|
|
24
|
+
DEFAULT_THINKING_PROVIDER = ModelProvider.ANTHROPIC
|
|
25
|
+
DEFAULT_THINKING_MODEL = "claude-opus-4-7"
|
|
26
|
+
DEFAULT_THINKING_TOKENS = 1024
|
|
27
|
+
|
|
28
|
+
API_KEY_ENV = {
|
|
29
|
+
ModelProvider.ANTHROPIC: "ANTHROPIC_API_KEY",
|
|
30
|
+
ModelProvider.OPENAI: "OPENAI_API_KEY",
|
|
31
|
+
ModelProvider.GOOGLE: "GOOGLE_API_KEY",
|
|
32
|
+
ModelProvider.GROQ: "GROQ_API_KEY",
|
|
33
|
+
ModelProvider.TOGETHER: "TOGETHER_API_KEY",
|
|
34
|
+
ModelProvider.FIREWORKS: "FIREWORKS_API_KEY",
|
|
35
|
+
ModelProvider.XAI: "XAI_API_KEY",
|
|
36
|
+
ModelProvider.DASHSCOPE: "DASHSCOPE_API_KEY",
|
|
37
|
+
ModelProvider.MOONSHOT: "MOONSHOT_API_KEY",
|
|
38
|
+
ModelProvider.DEEPSEEK: "DEEPSEEK_API_KEY",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CliConfigError(ValueError):
|
|
43
|
+
"""Raised when CLI configuration is incomplete or invalid."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class CliConfigOverrides:
|
|
48
|
+
"""Model and provider overrides supplied by CLI flags."""
|
|
49
|
+
|
|
50
|
+
provider: Optional[str] = None
|
|
51
|
+
model: Optional[str] = None
|
|
52
|
+
fast_provider: Optional[str] = None
|
|
53
|
+
fast_model: Optional[str] = None
|
|
54
|
+
edit_provider: Optional[str] = None
|
|
55
|
+
edit_model: Optional[str] = None
|
|
56
|
+
thinking_provider: Optional[str] = None
|
|
57
|
+
thinking_model: Optional[str] = None
|
|
58
|
+
thinking_tokens: Optional[int] = None
|
|
59
|
+
environment: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_cli_env(project_path: Path, env: Optional[Mapping[str, str]] = None) -> dict[str, str]:
|
|
63
|
+
"""Load process environment over a project-local .env file."""
|
|
64
|
+
base_env = dict(env if env is not None else os.environ)
|
|
65
|
+
dotenv_path = project_path / ".env"
|
|
66
|
+
if not dotenv_path.exists():
|
|
67
|
+
return base_env
|
|
68
|
+
|
|
69
|
+
file_env = {key: value for key, value in dotenv_values(dotenv_path).items() if value is not None}
|
|
70
|
+
return {**file_env, **base_env}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _env_or_default(
|
|
74
|
+
env: Mapping[str, str],
|
|
75
|
+
key: str,
|
|
76
|
+
override: Optional[str],
|
|
77
|
+
default: str,
|
|
78
|
+
) -> str:
|
|
79
|
+
if override:
|
|
80
|
+
return override
|
|
81
|
+
return env.get(key) or default
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _provider(value: str) -> ModelProvider:
|
|
85
|
+
try:
|
|
86
|
+
return ModelProvider(value.lower())
|
|
87
|
+
except ValueError as exc:
|
|
88
|
+
valid = ", ".join(provider.value for provider in ModelProvider)
|
|
89
|
+
raise CliConfigError(f"Unsupported provider '{value}'. Valid providers: {valid}") from exc
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _api_key_for_provider(
|
|
93
|
+
provider: ModelProvider,
|
|
94
|
+
env: Mapping[str, str],
|
|
95
|
+
settings: Optional[CliSettings],
|
|
96
|
+
) -> Optional[str]:
|
|
97
|
+
env_name = API_KEY_ENV.get(provider)
|
|
98
|
+
if env_name and env.get(env_name):
|
|
99
|
+
return env[env_name]
|
|
100
|
+
if settings:
|
|
101
|
+
return settings.get_api_key(provider.value)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _active_provider_model(
|
|
106
|
+
env: Mapping[str, str],
|
|
107
|
+
overrides: CliConfigOverrides,
|
|
108
|
+
settings: Optional[CliSettings],
|
|
109
|
+
) -> tuple[Optional[ModelProvider], Optional[str]]:
|
|
110
|
+
provider_value = overrides.provider or env.get("KOLEGA_CODE_PROVIDER")
|
|
111
|
+
model_value = overrides.model or env.get("KOLEGA_CODE_MODEL")
|
|
112
|
+
|
|
113
|
+
if provider_value or model_value:
|
|
114
|
+
provider = _provider(provider_value or DEFAULT_LONG_PROVIDER.value)
|
|
115
|
+
return provider, model_value or default_model_for_provider(provider)
|
|
116
|
+
|
|
117
|
+
if settings and (settings.active_provider or settings.active_model):
|
|
118
|
+
provider = _provider(settings.active_provider or UI_DEFAULT_PROVIDER)
|
|
119
|
+
return provider, settings.active_model or default_model_for_provider(provider)
|
|
120
|
+
|
|
121
|
+
return None, None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _slot_provider_model(
|
|
125
|
+
env: Mapping[str, str],
|
|
126
|
+
provider_env_key: str,
|
|
127
|
+
model_env_key: str,
|
|
128
|
+
provider_override: Optional[str],
|
|
129
|
+
model_override: Optional[str],
|
|
130
|
+
default_provider: ModelProvider,
|
|
131
|
+
default_model: str,
|
|
132
|
+
active_provider: Optional[ModelProvider],
|
|
133
|
+
active_model: Optional[str],
|
|
134
|
+
) -> tuple[ModelProvider, str]:
|
|
135
|
+
provider_value = provider_override or env.get(provider_env_key)
|
|
136
|
+
model_value = model_override or env.get(model_env_key)
|
|
137
|
+
|
|
138
|
+
if provider_value or model_value:
|
|
139
|
+
provider = _provider(provider_value or (active_provider.value if active_provider else default_provider.value))
|
|
140
|
+
return provider, model_value or (
|
|
141
|
+
active_model if active_provider == provider and active_model else default_model_for_provider(provider)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if active_provider and active_model:
|
|
145
|
+
return active_provider, active_model
|
|
146
|
+
|
|
147
|
+
return default_provider, default_model
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _model_config(provider: ModelProvider, model: str, thinking_tokens: Optional[int] = None) -> ModelConfig:
|
|
151
|
+
try:
|
|
152
|
+
get_model_specs(provider, model)
|
|
153
|
+
except ValueError as exc:
|
|
154
|
+
raise CliConfigError(str(exc)) from exc
|
|
155
|
+
|
|
156
|
+
return ModelConfig(
|
|
157
|
+
provider=provider,
|
|
158
|
+
model=model,
|
|
159
|
+
rate_limits=RateLimitConfig(),
|
|
160
|
+
thinking_tokens=thinking_tokens,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def build_agent_config(
|
|
165
|
+
project_path: Path,
|
|
166
|
+
overrides: Optional[CliConfigOverrides] = None,
|
|
167
|
+
env: Optional[Mapping[str, str]] = None,
|
|
168
|
+
settings: Optional[CliSettings] = None,
|
|
169
|
+
) -> AgentConfig:
|
|
170
|
+
"""Build an AgentConfig for CLI-hosted agents."""
|
|
171
|
+
overrides = overrides or CliConfigOverrides()
|
|
172
|
+
loaded_env = load_cli_env(project_path, env)
|
|
173
|
+
|
|
174
|
+
active_provider, active_model = _active_provider_model(loaded_env, overrides, settings)
|
|
175
|
+
|
|
176
|
+
long_provider, long_model = _slot_provider_model(
|
|
177
|
+
loaded_env,
|
|
178
|
+
"KOLEGA_CODE_PROVIDER",
|
|
179
|
+
"KOLEGA_CODE_MODEL",
|
|
180
|
+
overrides.provider,
|
|
181
|
+
overrides.model,
|
|
182
|
+
DEFAULT_LONG_PROVIDER,
|
|
183
|
+
DEFAULT_LONG_MODEL,
|
|
184
|
+
active_provider,
|
|
185
|
+
active_model,
|
|
186
|
+
)
|
|
187
|
+
fast_provider, fast_model = _slot_provider_model(
|
|
188
|
+
loaded_env,
|
|
189
|
+
"KOLEGA_CODE_FAST_PROVIDER",
|
|
190
|
+
"KOLEGA_CODE_FAST_MODEL",
|
|
191
|
+
overrides.fast_provider,
|
|
192
|
+
overrides.fast_model,
|
|
193
|
+
DEFAULT_FAST_PROVIDER,
|
|
194
|
+
DEFAULT_FAST_MODEL,
|
|
195
|
+
active_provider,
|
|
196
|
+
active_model,
|
|
197
|
+
)
|
|
198
|
+
edit_provider, edit_model = _slot_provider_model(
|
|
199
|
+
loaded_env,
|
|
200
|
+
"KOLEGA_CODE_EDIT_PROVIDER",
|
|
201
|
+
"KOLEGA_CODE_EDIT_MODEL",
|
|
202
|
+
overrides.edit_provider,
|
|
203
|
+
overrides.edit_model,
|
|
204
|
+
DEFAULT_EDIT_PROVIDER,
|
|
205
|
+
DEFAULT_EDIT_MODEL,
|
|
206
|
+
active_provider,
|
|
207
|
+
active_model,
|
|
208
|
+
)
|
|
209
|
+
thinking_provider, thinking_model = _slot_provider_model(
|
|
210
|
+
loaded_env,
|
|
211
|
+
"KOLEGA_CODE_THINKING_PROVIDER",
|
|
212
|
+
"KOLEGA_CODE_THINKING_MODEL",
|
|
213
|
+
overrides.thinking_provider,
|
|
214
|
+
overrides.thinking_model,
|
|
215
|
+
DEFAULT_THINKING_PROVIDER,
|
|
216
|
+
DEFAULT_THINKING_MODEL,
|
|
217
|
+
active_provider,
|
|
218
|
+
active_model,
|
|
219
|
+
)
|
|
220
|
+
thinking_tokens = overrides.thinking_tokens or int(
|
|
221
|
+
loaded_env.get("KOLEGA_CODE_THINKING_TOKENS", str(DEFAULT_THINKING_TOKENS))
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
required_providers = {long_provider, fast_provider, edit_provider, thinking_provider}
|
|
225
|
+
missing_keys = [
|
|
226
|
+
API_KEY_ENV[provider]
|
|
227
|
+
for provider in sorted(required_providers, key=lambda item: item.value)
|
|
228
|
+
if provider != ModelProvider.LLAMA and not _api_key_for_provider(provider, loaded_env, settings)
|
|
229
|
+
]
|
|
230
|
+
if missing_keys:
|
|
231
|
+
raise CliConfigError(f"Missing required API key environment variable(s): {', '.join(missing_keys)}")
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
return AgentConfig(
|
|
235
|
+
anthropic_api_key=_api_key_for_provider(ModelProvider.ANTHROPIC, loaded_env, settings),
|
|
236
|
+
openai_api_key=_api_key_for_provider(ModelProvider.OPENAI, loaded_env, settings),
|
|
237
|
+
google_api_key=_api_key_for_provider(ModelProvider.GOOGLE, loaded_env, settings),
|
|
238
|
+
groq_api_key=_api_key_for_provider(ModelProvider.GROQ, loaded_env, settings),
|
|
239
|
+
together_api_key=_api_key_for_provider(ModelProvider.TOGETHER, loaded_env, settings),
|
|
240
|
+
fireworks_api_key=_api_key_for_provider(ModelProvider.FIREWORKS, loaded_env, settings),
|
|
241
|
+
xai_api_key=_api_key_for_provider(ModelProvider.XAI, loaded_env, settings),
|
|
242
|
+
dashscope_api_key=_api_key_for_provider(ModelProvider.DASHSCOPE, loaded_env, settings),
|
|
243
|
+
moonshot_api_key=_api_key_for_provider(ModelProvider.MOONSHOT, loaded_env, settings),
|
|
244
|
+
deepseek_api_key=_api_key_for_provider(ModelProvider.DEEPSEEK, loaded_env, settings),
|
|
245
|
+
environment=overrides.environment or loaded_env.get("KOLEGA_CODE_ENVIRONMENT", "development"),
|
|
246
|
+
long_context_config=_model_config(long_provider, long_model),
|
|
247
|
+
fast_config=_model_config(fast_provider, fast_model),
|
|
248
|
+
edit_model_config=_model_config(edit_provider, edit_model),
|
|
249
|
+
thinking_config=_model_config(thinking_provider, thinking_model, thinking_tokens=thinking_tokens),
|
|
250
|
+
)
|
|
251
|
+
except ValueError as exc:
|
|
252
|
+
raise CliConfigError(str(exc)) from exc
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def key_status(provider: str, project_path: Path, settings: Optional[CliSettings] = None) -> str:
|
|
256
|
+
"""Return the API-key source for display without exposing the key."""
|
|
257
|
+
provider_value = _provider(provider)
|
|
258
|
+
env = load_cli_env(project_path)
|
|
259
|
+
env_name = API_KEY_ENV.get(provider_value)
|
|
260
|
+
if env_name and env.get(env_name):
|
|
261
|
+
return f"present via {env_name}"
|
|
262
|
+
if settings and settings.get_api_key(provider_value.value):
|
|
263
|
+
return "present in local settings"
|
|
264
|
+
return "missing"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def config_summary(config: AgentConfig) -> dict[str, str | int | None]:
|
|
268
|
+
"""Return a session-safe summary of model configuration."""
|
|
269
|
+
return {
|
|
270
|
+
"environment": config.environment,
|
|
271
|
+
"long_provider": config.long_context_config.provider.value,
|
|
272
|
+
"long_model": config.long_context_config.model,
|
|
273
|
+
"fast_provider": config.fast_config.provider.value,
|
|
274
|
+
"fast_model": config.fast_config.model,
|
|
275
|
+
"edit_provider": config.edit_model_config.provider.value,
|
|
276
|
+
"edit_model": config.edit_model_config.model,
|
|
277
|
+
"thinking_provider": config.thinking_config.provider.value,
|
|
278
|
+
"thinking_model": config.thinking_config.model,
|
|
279
|
+
"thinking_tokens": config.thinking_config.thinking_tokens,
|
|
280
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""CLI event bridge for agent broadcasts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from kolega_code.events import AgentConnectionManager
|
|
10
|
+
from kolega_code.events import AgentEvent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CliConnectionManager(AgentConnectionManager):
|
|
14
|
+
"""Connection manager that exposes agent broadcasts through an asyncio queue."""
|
|
15
|
+
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
self.events: asyncio.Queue[AgentEvent] = asyncio.Queue()
|
|
18
|
+
self._connections: Counter[tuple[str, str, str]] = Counter()
|
|
19
|
+
|
|
20
|
+
async def connect(
|
|
21
|
+
self,
|
|
22
|
+
websocket: Any,
|
|
23
|
+
workspace_id: str,
|
|
24
|
+
thread_id: str,
|
|
25
|
+
connection_type: str,
|
|
26
|
+
user_info=None,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._connections[(workspace_id, thread_id, connection_type)] += 1
|
|
29
|
+
|
|
30
|
+
def disconnect(self, websocket: Any, workspace_id: str, thread_id: str, connection_type: str) -> None:
|
|
31
|
+
key = (workspace_id, thread_id, connection_type)
|
|
32
|
+
if self._connections[key] > 1:
|
|
33
|
+
self._connections[key] -= 1
|
|
34
|
+
else:
|
|
35
|
+
self._connections.pop(key, None)
|
|
36
|
+
|
|
37
|
+
async def broadcast_event(self, event: AgentEvent, workspace_id: str, thread_id: str) -> None:
|
|
38
|
+
await self.events.put(event)
|
|
39
|
+
|
|
40
|
+
def get_connection_count(self, workspace_id: str, thread_id: str) -> dict:
|
|
41
|
+
counts: dict[str, int] = {}
|
|
42
|
+
for (ws_id, th_id, connection_type), count in self._connections.items():
|
|
43
|
+
if ws_id == workspace_id and th_id == thread_id:
|
|
44
|
+
counts[connection_type] = count
|
|
45
|
+
return counts
|
|
46
|
+
|
|
47
|
+
async def next_event(self) -> AgentEvent:
|
|
48
|
+
"""Return the next broadcast event."""
|
|
49
|
+
return await self.events.get()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Workspace file index powering @ mention autocomplete in the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
import pathspec
|
|
12
|
+
|
|
13
|
+
from kolega_code.agent.tool_backend.glob_tool import GlobTool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class IndexEntry:
|
|
18
|
+
path: str # relative to project root, posix-style separators
|
|
19
|
+
is_dir: bool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WorkspaceFileIndex:
|
|
23
|
+
"""Cached, gitignore-aware listing of workspace files for completion."""
|
|
24
|
+
|
|
25
|
+
MAX_FILES = 5000
|
|
26
|
+
TTL_SECONDS = 5.0
|
|
27
|
+
|
|
28
|
+
def __init__(self, project_path: Path) -> None:
|
|
29
|
+
self.project_path = Path(project_path)
|
|
30
|
+
self._entries: List[IndexEntry] = []
|
|
31
|
+
self._refreshed_at: Optional[float] = None
|
|
32
|
+
|
|
33
|
+
def entries(self) -> List[IndexEntry]:
|
|
34
|
+
now = time.monotonic()
|
|
35
|
+
if self._refreshed_at is None or now - self._refreshed_at > self.TTL_SECONDS:
|
|
36
|
+
self.refresh()
|
|
37
|
+
return self._entries
|
|
38
|
+
|
|
39
|
+
def refresh(self) -> None:
|
|
40
|
+
gitignore = self._load_gitignore_spec()
|
|
41
|
+
entries: List[IndexEntry] = []
|
|
42
|
+
root = self.project_path
|
|
43
|
+
for dirpath, dirnames, filenames in os.walk(root, topdown=True):
|
|
44
|
+
rel_dir = Path(dirpath).relative_to(root)
|
|
45
|
+
kept_dirs = []
|
|
46
|
+
for name in sorted(dirnames):
|
|
47
|
+
if name in GlobTool.EXCLUDE_DIRS:
|
|
48
|
+
continue
|
|
49
|
+
rel = self._posix(rel_dir / name)
|
|
50
|
+
if gitignore is not None and gitignore.match_file(rel + "/"):
|
|
51
|
+
continue
|
|
52
|
+
kept_dirs.append(name)
|
|
53
|
+
entries.append(IndexEntry(path=rel + "/", is_dir=True))
|
|
54
|
+
dirnames[:] = kept_dirs
|
|
55
|
+
|
|
56
|
+
for name in sorted(filenames):
|
|
57
|
+
if Path(name).suffix.lower() in GlobTool.BINARY_EXTENSIONS:
|
|
58
|
+
continue
|
|
59
|
+
rel = self._posix(rel_dir / name)
|
|
60
|
+
if gitignore is not None and gitignore.match_file(rel):
|
|
61
|
+
continue
|
|
62
|
+
entries.append(IndexEntry(path=rel, is_dir=False))
|
|
63
|
+
|
|
64
|
+
if len(entries) >= self.MAX_FILES:
|
|
65
|
+
entries = entries[: self.MAX_FILES]
|
|
66
|
+
break
|
|
67
|
+
|
|
68
|
+
self._entries = entries
|
|
69
|
+
self._refreshed_at = time.monotonic()
|
|
70
|
+
|
|
71
|
+
def search(self, query: str, limit: int = 8) -> List[IndexEntry]:
|
|
72
|
+
scored = []
|
|
73
|
+
for entry in self.entries():
|
|
74
|
+
score = fuzzy_score(query, entry.path)
|
|
75
|
+
if score is not None:
|
|
76
|
+
scored.append((score, entry))
|
|
77
|
+
scored.sort(key=lambda pair: (-pair[0], pair[1].path))
|
|
78
|
+
return [entry for _, entry in scored[:limit]]
|
|
79
|
+
|
|
80
|
+
def _load_gitignore_spec(self) -> Optional[pathspec.PathSpec]:
|
|
81
|
+
gitignore_path = self.project_path / ".gitignore"
|
|
82
|
+
try:
|
|
83
|
+
if not gitignore_path.is_file():
|
|
84
|
+
return None
|
|
85
|
+
content = gitignore_path.read_text(encoding="utf-8")
|
|
86
|
+
return pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, content.splitlines())
|
|
87
|
+
except Exception:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _posix(path: Path) -> str:
|
|
92
|
+
rel = str(PurePosixPath(path.as_posix()))
|
|
93
|
+
return "" if rel == "." else rel
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def fuzzy_score(query: str, path: str) -> Optional[float]:
|
|
97
|
+
"""Score how well ``query`` matches ``path``; higher is better, None means no match.
|
|
98
|
+
|
|
99
|
+
Case-insensitive subsequence match with bonuses for consecutive runs,
|
|
100
|
+
matches at path-segment boundaries, and matches inside the basename.
|
|
101
|
+
"""
|
|
102
|
+
if not query:
|
|
103
|
+
# Empty query matches everything; prefer shallow paths.
|
|
104
|
+
return -float(path.count("/"))
|
|
105
|
+
|
|
106
|
+
q = query.lower()
|
|
107
|
+
p = path.lower()
|
|
108
|
+
basename_start = p.rstrip("/").rfind("/") + 1
|
|
109
|
+
penalty = path.count("/") * 0.5 + len(path) * 0.01
|
|
110
|
+
|
|
111
|
+
# Substring matches outrank scattered subsequences.
|
|
112
|
+
idx = p.find(q)
|
|
113
|
+
if idx != -1:
|
|
114
|
+
score = 100.0
|
|
115
|
+
if idx >= basename_start:
|
|
116
|
+
score += 50.0
|
|
117
|
+
if idx == 0 or p[idx - 1] in "/._-":
|
|
118
|
+
score += 25.0
|
|
119
|
+
return score - penalty
|
|
120
|
+
|
|
121
|
+
# Subsequence match: try a basename-anchored scan too, so dense matches in
|
|
122
|
+
# the filename beat scattered matches that start in the directory part.
|
|
123
|
+
full = _subsequence_score(q, p, 0, basename_start)
|
|
124
|
+
anchored = _subsequence_score(q, p, basename_start, basename_start)
|
|
125
|
+
candidates = [score for score in (full, anchored) if score is not None]
|
|
126
|
+
if not candidates:
|
|
127
|
+
return None
|
|
128
|
+
return max(candidates) - penalty
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _subsequence_score(q: str, p: str, start: int, basename_start: int) -> Optional[float]:
|
|
132
|
+
score = 0.0
|
|
133
|
+
pos = start - 1
|
|
134
|
+
prev_matched = -2
|
|
135
|
+
for ch in q:
|
|
136
|
+
pos = p.find(ch, pos + 1)
|
|
137
|
+
if pos == -1:
|
|
138
|
+
return None
|
|
139
|
+
if pos == prev_matched + 1:
|
|
140
|
+
score += 3.0 # consecutive run
|
|
141
|
+
if pos == 0 or p[pos - 1] in "/._-":
|
|
142
|
+
score += 5.0 # segment/word boundary
|
|
143
|
+
if pos >= basename_start:
|
|
144
|
+
score += 2.0
|
|
145
|
+
prev_matched = pos
|
|
146
|
+
score += 1.0
|
|
147
|
+
return score
|