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
kolega_code/cli/main.py
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
"""Entrypoint for the Kolega Code CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import importlib.util
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Iterable, Optional
|
|
12
|
+
|
|
13
|
+
from kolega_code import __version__
|
|
14
|
+
from kolega_code.agent import CoderAgent
|
|
15
|
+
from kolega_code.llm.models import TextBlock
|
|
16
|
+
from kolega_code.agent.prompt_provider import AgentMode
|
|
17
|
+
from kolega_code.services.browser import PlaywrightBrowserManager
|
|
18
|
+
|
|
19
|
+
from .config import CliConfigError, CliConfigOverrides, build_agent_config, config_summary, key_status
|
|
20
|
+
from .connection import CliConnectionManager
|
|
21
|
+
from .mentions import build_file_attachments
|
|
22
|
+
from .session_store import SessionRecord, SessionStore, SessionStoreError
|
|
23
|
+
from .settings import SettingsStore, SettingsStoreError
|
|
24
|
+
from .slash_commands import SKILLS_LIST_COMMAND, agent_command_names
|
|
25
|
+
from .skills import (
|
|
26
|
+
SkillCatalog,
|
|
27
|
+
activated_skill_names,
|
|
28
|
+
build_skill_prompt_extension,
|
|
29
|
+
build_skill_tool_extension,
|
|
30
|
+
discover_skills,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
SUBCOMMANDS = {"ask", "sessions", "doctor"}
|
|
34
|
+
RESUME_LATEST = "__latest__"
|
|
35
|
+
CLI_AGENT_MODE = AgentMode.CLI.value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main(argv: Optional[Iterable[str]] = None) -> int:
|
|
39
|
+
args = parse_args(list(argv) if argv is not None else sys.argv[1:])
|
|
40
|
+
try:
|
|
41
|
+
if args.command == "ask":
|
|
42
|
+
return asyncio.run(_run_ask(args))
|
|
43
|
+
if args.command == "sessions":
|
|
44
|
+
return _run_sessions(args)
|
|
45
|
+
if args.command == "doctor":
|
|
46
|
+
return _run_doctor(args)
|
|
47
|
+
return _run_tui(args)
|
|
48
|
+
except (CliConfigError, SessionStoreError, SettingsStoreError, ValueError) as exc:
|
|
49
|
+
_print_styled(f"kolega-code: {exc}", style="error", stderr=True)
|
|
50
|
+
return 2
|
|
51
|
+
except KeyboardInterrupt:
|
|
52
|
+
_print_styled("\nInterrupted.", style="warning", stderr=True)
|
|
53
|
+
return 130
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _make_console(stderr: bool = False):
|
|
57
|
+
"""Build a themed rich Console, or None when rich is unavailable.
|
|
58
|
+
|
|
59
|
+
rich is only a transitive dependency via textual, so plain installs
|
|
60
|
+
without the [cli] extra fall back to unstyled print output.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
from rich.console import Console
|
|
64
|
+
|
|
65
|
+
from .theme import build_rich_theme
|
|
66
|
+
except ImportError:
|
|
67
|
+
return None
|
|
68
|
+
return Console(theme=build_rich_theme(), stderr=stderr)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _print_styled(text: str, style: Optional[str] = None, stderr: bool = False) -> None:
|
|
72
|
+
console = _make_console(stderr=stderr)
|
|
73
|
+
if console is None:
|
|
74
|
+
print(text, file=sys.stderr if stderr else sys.stdout)
|
|
75
|
+
return
|
|
76
|
+
console.print(text, style=style, highlight=False, markup=False, soft_wrap=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
80
|
+
if argv and argv[0] in SUBCOMMANDS:
|
|
81
|
+
return _build_subcommand_parser().parse_args(argv)
|
|
82
|
+
return _build_tui_parser().parse_args(argv)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _add_common_model_args(parser: argparse.ArgumentParser) -> None:
|
|
86
|
+
parser.add_argument("--provider", help="Provider for the main coding model.")
|
|
87
|
+
parser.add_argument("--model", help="Main coding model.")
|
|
88
|
+
parser.add_argument("--fast-provider", help="Provider for fast utility calls.")
|
|
89
|
+
parser.add_argument("--fast-model", help="Fast utility model.")
|
|
90
|
+
parser.add_argument("--edit-provider", help="Provider for edit-file operations.")
|
|
91
|
+
parser.add_argument("--edit-model", help="Model for edit-file operations.")
|
|
92
|
+
parser.add_argument("--thinking-provider", help="Provider for think-hard operations.")
|
|
93
|
+
parser.add_argument("--thinking-model", help="Model for think-hard operations.")
|
|
94
|
+
parser.add_argument("--thinking-tokens", type=int, help="Thinking token budget for think-hard operations.")
|
|
95
|
+
parser.add_argument("--environment", help="Environment label for tracing/metadata.")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _add_session_args(parser: argparse.ArgumentParser, session_help: str = "Session ID to resume or create.") -> None:
|
|
99
|
+
parser.add_argument("--state-dir", type=Path, help="Directory for CLI session state.")
|
|
100
|
+
parser.add_argument("--session", help=session_help)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _build_tui_parser() -> argparse.ArgumentParser:
|
|
104
|
+
parser = argparse.ArgumentParser(prog="kolega-code", description="Run the Kolega Code Textual CLI.")
|
|
105
|
+
parser.set_defaults(command="tui")
|
|
106
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
107
|
+
parser.add_argument("project_path", nargs="?", default=".", type=Path, help="Project directory to work in.")
|
|
108
|
+
parser.add_argument("--mode", choices=[mode.value for mode in AgentMode], default=CLI_AGENT_MODE, help=argparse.SUPPRESS)
|
|
109
|
+
parser.add_argument("--new", action="store_true", help="Start a new session. This is now the default.")
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--resume",
|
|
112
|
+
nargs="?",
|
|
113
|
+
const=RESUME_LATEST,
|
|
114
|
+
metavar="THREAD_ID",
|
|
115
|
+
help="Resume the latest saved thread, or resume the given thread/session ID.",
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument("--browser-visible", action="store_true", help="Launch visible Playwright browser windows.")
|
|
118
|
+
_add_session_args(parser, session_help="Legacy alias for --resume THREAD_ID.")
|
|
119
|
+
_add_common_model_args(parser)
|
|
120
|
+
return parser
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _build_subcommand_parser() -> argparse.ArgumentParser:
|
|
124
|
+
parser = argparse.ArgumentParser(prog="kolega-code", description="Kolega Code CLI.")
|
|
125
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
126
|
+
|
|
127
|
+
ask = subparsers.add_parser("ask", help="Run a single prompt and print the answer.")
|
|
128
|
+
ask.add_argument("prompt", help="Prompt to send to Kolega Code.")
|
|
129
|
+
ask.add_argument("--project", default=".", type=Path, help="Project directory to work in.")
|
|
130
|
+
ask.add_argument("--mode", choices=[mode.value for mode in AgentMode], default=CLI_AGENT_MODE, help=argparse.SUPPRESS)
|
|
131
|
+
ask.add_argument("--save", action="store_true", help="Persist the session after the prompt completes.")
|
|
132
|
+
ask.add_argument("--json", action="store_true", help="Emit response chunks and events as JSON.")
|
|
133
|
+
ask.add_argument("--browser-visible", action="store_true", help="Launch visible Playwright browser windows.")
|
|
134
|
+
_add_session_args(ask)
|
|
135
|
+
_add_common_model_args(ask)
|
|
136
|
+
|
|
137
|
+
sessions = subparsers.add_parser("sessions", help="Manage local CLI sessions.")
|
|
138
|
+
sessions_sub = sessions.add_subparsers(dest="sessions_command", required=True)
|
|
139
|
+
sessions_list = sessions_sub.add_parser("list", help="List sessions.")
|
|
140
|
+
sessions_list.add_argument("--project", type=Path, help="Filter by project path.")
|
|
141
|
+
sessions_list.add_argument("--state-dir", type=Path, help="Directory for CLI session state.")
|
|
142
|
+
sessions_delete = sessions_sub.add_parser("delete", help="Delete a session.")
|
|
143
|
+
sessions_delete.add_argument("session_id")
|
|
144
|
+
sessions_delete.add_argument("--state-dir", type=Path, help="Directory for CLI session state.")
|
|
145
|
+
sessions_export = sessions_sub.add_parser("export", help="Print a session as JSON.")
|
|
146
|
+
sessions_export.add_argument("session_id")
|
|
147
|
+
sessions_export.add_argument("--output", type=Path, help="Write JSON to a file instead of stdout.")
|
|
148
|
+
sessions_export.add_argument("--state-dir", type=Path, help="Directory for CLI session state.")
|
|
149
|
+
|
|
150
|
+
doctor = subparsers.add_parser("doctor", help="Check local CLI configuration.")
|
|
151
|
+
doctor.add_argument("--project", default=".", type=Path, help="Project directory to check.")
|
|
152
|
+
doctor.add_argument("--state-dir", type=Path, help="Directory for CLI session state.")
|
|
153
|
+
_add_common_model_args(doctor)
|
|
154
|
+
|
|
155
|
+
return parser
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _overrides_from_args(args: argparse.Namespace) -> CliConfigOverrides:
|
|
159
|
+
return CliConfigOverrides(
|
|
160
|
+
provider=getattr(args, "provider", None),
|
|
161
|
+
model=getattr(args, "model", None),
|
|
162
|
+
fast_provider=getattr(args, "fast_provider", None),
|
|
163
|
+
fast_model=getattr(args, "fast_model", None),
|
|
164
|
+
edit_provider=getattr(args, "edit_provider", None),
|
|
165
|
+
edit_model=getattr(args, "edit_model", None),
|
|
166
|
+
thinking_provider=getattr(args, "thinking_provider", None),
|
|
167
|
+
thinking_model=getattr(args, "thinking_model", None),
|
|
168
|
+
thinking_tokens=getattr(args, "thinking_tokens", None),
|
|
169
|
+
environment=getattr(args, "environment", None),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _store_from_args(args: argparse.Namespace) -> SessionStore:
|
|
174
|
+
return SessionStore(root=getattr(args, "state_dir", None))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _settings_store_from_args(args: argparse.Namespace) -> SettingsStore:
|
|
178
|
+
return SettingsStore(root=getattr(args, "state_dir", None))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _validate_project(project_path: Path) -> Path:
|
|
182
|
+
project_path = project_path.expanduser().resolve()
|
|
183
|
+
if not project_path.exists():
|
|
184
|
+
raise ValueError(f"Project path does not exist: {project_path}")
|
|
185
|
+
if not project_path.is_dir():
|
|
186
|
+
raise ValueError(f"Project path is not a directory: {project_path}")
|
|
187
|
+
return project_path
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _get_or_create_session(
|
|
191
|
+
store: SessionStore,
|
|
192
|
+
project_path: Path,
|
|
193
|
+
mode: str,
|
|
194
|
+
summary: dict,
|
|
195
|
+
session_id: Optional[str],
|
|
196
|
+
force_new: bool = False,
|
|
197
|
+
) -> SessionRecord:
|
|
198
|
+
if session_id and not force_new:
|
|
199
|
+
try:
|
|
200
|
+
return store.load(session_id)
|
|
201
|
+
except SessionStoreError:
|
|
202
|
+
return store.create(project_path, mode, summary, session_id=session_id)
|
|
203
|
+
|
|
204
|
+
if not force_new:
|
|
205
|
+
latest = store.latest_for_project(project_path)
|
|
206
|
+
if latest:
|
|
207
|
+
return latest
|
|
208
|
+
|
|
209
|
+
return store.create(project_path, mode, summary, session_id=session_id)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _validate_session_project(session: SessionRecord, project_path: Path) -> SessionRecord:
|
|
213
|
+
resolved_project = str(project_path.resolve())
|
|
214
|
+
if session.project_path != resolved_project:
|
|
215
|
+
raise SessionStoreError(
|
|
216
|
+
f"Session {session.session_id} belongs to project {session.project_path}, not {resolved_project}"
|
|
217
|
+
)
|
|
218
|
+
return session
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _normalize_cli_session_mode(store: SessionStore, session: SessionRecord, *, persist: bool) -> SessionRecord:
|
|
222
|
+
if session.mode != CLI_AGENT_MODE:
|
|
223
|
+
session.mode = CLI_AGENT_MODE
|
|
224
|
+
if persist:
|
|
225
|
+
store.save(session)
|
|
226
|
+
return session
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _resolve_tui_session(
|
|
230
|
+
store: SessionStore,
|
|
231
|
+
project_path: Path,
|
|
232
|
+
summary: dict,
|
|
233
|
+
resume: Optional[str],
|
|
234
|
+
legacy_session_id: Optional[str],
|
|
235
|
+
) -> SessionRecord:
|
|
236
|
+
if resume is not None and legacy_session_id:
|
|
237
|
+
raise ValueError("Use either --resume or --session, not both.")
|
|
238
|
+
|
|
239
|
+
if legacy_session_id:
|
|
240
|
+
session = _validate_session_project(store.load_session_or_thread(legacy_session_id), project_path)
|
|
241
|
+
return _normalize_cli_session_mode(store, session, persist=True)
|
|
242
|
+
|
|
243
|
+
if resume == RESUME_LATEST:
|
|
244
|
+
latest = store.latest_for_project(project_path)
|
|
245
|
+
if latest is None:
|
|
246
|
+
raise SessionStoreError(f"No saved sessions found for project: {project_path}")
|
|
247
|
+
return _normalize_cli_session_mode(store, latest, persist=True)
|
|
248
|
+
|
|
249
|
+
if resume:
|
|
250
|
+
session = _validate_session_project(store.load_session_or_thread(resume), project_path)
|
|
251
|
+
return _normalize_cli_session_mode(store, session, persist=True)
|
|
252
|
+
|
|
253
|
+
return store.create(project_path, CLI_AGENT_MODE, summary)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _run_tui(args: argparse.Namespace) -> int:
|
|
257
|
+
if importlib.util.find_spec("textual") is None:
|
|
258
|
+
print("Textual is not installed. Reinstall the CLI with: uv tool install --force kolega-code", file=sys.stderr)
|
|
259
|
+
return 2
|
|
260
|
+
|
|
261
|
+
project_path = _validate_project(args.project_path)
|
|
262
|
+
store = _store_from_args(args)
|
|
263
|
+
settings_store = _settings_store_from_args(args)
|
|
264
|
+
settings = settings_store.load()
|
|
265
|
+
summary = {}
|
|
266
|
+
try:
|
|
267
|
+
config = build_agent_config(project_path, _overrides_from_args(args), settings=settings)
|
|
268
|
+
summary = config_summary(config)
|
|
269
|
+
except CliConfigError:
|
|
270
|
+
config = None
|
|
271
|
+
session = _resolve_tui_session(
|
|
272
|
+
store,
|
|
273
|
+
project_path,
|
|
274
|
+
summary,
|
|
275
|
+
args.resume,
|
|
276
|
+
args.session,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
from .app import KolegaCodeApp
|
|
280
|
+
|
|
281
|
+
app = KolegaCodeApp(
|
|
282
|
+
project_path=project_path,
|
|
283
|
+
config=config,
|
|
284
|
+
mode=CLI_AGENT_MODE,
|
|
285
|
+
store=store,
|
|
286
|
+
settings_store=settings_store,
|
|
287
|
+
overrides=_overrides_from_args(args),
|
|
288
|
+
session=session,
|
|
289
|
+
browser_visible=args.browser_visible,
|
|
290
|
+
)
|
|
291
|
+
app.run()
|
|
292
|
+
return 0
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
async def _run_ask(args: argparse.Namespace) -> int:
|
|
296
|
+
project_path = _validate_project(args.project)
|
|
297
|
+
skill_catalog = discover_skills(project_path)
|
|
298
|
+
skill_command = _parse_skill_prompt(args.prompt, skill_catalog)
|
|
299
|
+
|
|
300
|
+
if skill_command and skill_command[0] == "skills":
|
|
301
|
+
if args.json:
|
|
302
|
+
print(json.dumps({"kind": "skills", "data": skill_catalog.format_catalog()}, default=str))
|
|
303
|
+
else:
|
|
304
|
+
print(skill_catalog.format_catalog())
|
|
305
|
+
return 0
|
|
306
|
+
|
|
307
|
+
if skill_command and skill_command[0] != "skills" and not skill_command[1] and not (args.save or args.session):
|
|
308
|
+
activation_content = skill_catalog.activation_content(skill_command[0])
|
|
309
|
+
if args.json:
|
|
310
|
+
print(
|
|
311
|
+
json.dumps(
|
|
312
|
+
{
|
|
313
|
+
"kind": "skill",
|
|
314
|
+
"data": {
|
|
315
|
+
"name": skill_command[0],
|
|
316
|
+
"content": activation_content,
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
default=str,
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
print(activation_content)
|
|
324
|
+
return 0
|
|
325
|
+
|
|
326
|
+
store = _store_from_args(args)
|
|
327
|
+
settings_store = _settings_store_from_args(args)
|
|
328
|
+
settings = settings_store.load()
|
|
329
|
+
config = build_agent_config(project_path, _overrides_from_args(args), settings=settings)
|
|
330
|
+
summary = config_summary(config)
|
|
331
|
+
|
|
332
|
+
if args.session:
|
|
333
|
+
session = _get_or_create_session(store, project_path, CLI_AGENT_MODE, summary, args.session, force_new=False)
|
|
334
|
+
session = _normalize_cli_session_mode(store, session, persist=True)
|
|
335
|
+
elif args.save:
|
|
336
|
+
session = store.create(project_path, CLI_AGENT_MODE, summary)
|
|
337
|
+
else:
|
|
338
|
+
session = SessionRecord.create(project_path, CLI_AGENT_MODE, summary)
|
|
339
|
+
|
|
340
|
+
manager = CliConnectionManager()
|
|
341
|
+
browser_manager = PlaywrightBrowserManager()
|
|
342
|
+
browser_manager.headless = not args.browser_visible
|
|
343
|
+
agent_ref: dict[str, CoderAgent] = {}
|
|
344
|
+
prompt_extensions = []
|
|
345
|
+
tool_extensions = []
|
|
346
|
+
skill_prompt_extension = build_skill_prompt_extension(skill_catalog)
|
|
347
|
+
skill_tool_extension = build_skill_tool_extension(
|
|
348
|
+
skill_catalog,
|
|
349
|
+
lambda: agent_ref["agent"].history if "agent" in agent_ref else [],
|
|
350
|
+
)
|
|
351
|
+
if skill_prompt_extension is not None:
|
|
352
|
+
prompt_extensions.append(skill_prompt_extension)
|
|
353
|
+
if skill_tool_extension is not None:
|
|
354
|
+
tool_extensions.append(skill_tool_extension)
|
|
355
|
+
agent = CoderAgent(
|
|
356
|
+
project_path=project_path,
|
|
357
|
+
workspace_id=session.workspace_id,
|
|
358
|
+
thread_id=session.thread_id,
|
|
359
|
+
connection_manager=manager,
|
|
360
|
+
config=config,
|
|
361
|
+
browser_manager=browser_manager,
|
|
362
|
+
agent_mode=AgentMode.CLI,
|
|
363
|
+
prompt_extensions=prompt_extensions,
|
|
364
|
+
tool_extensions=tool_extensions,
|
|
365
|
+
)
|
|
366
|
+
agent_ref["agent"] = agent
|
|
367
|
+
if session.history:
|
|
368
|
+
agent.restore_message_history(session.history)
|
|
369
|
+
|
|
370
|
+
prompt = args.prompt
|
|
371
|
+
if skill_command:
|
|
372
|
+
skill_name, skill_prompt = skill_command
|
|
373
|
+
active_names = activated_skill_names(agent.history)
|
|
374
|
+
activation_content = skill_catalog.activation_content(skill_name, active_names=active_names)
|
|
375
|
+
if skill_name not in active_names:
|
|
376
|
+
agent.append_user_message([TextBlock(text=activation_content)])
|
|
377
|
+
if args.json:
|
|
378
|
+
print(
|
|
379
|
+
json.dumps(
|
|
380
|
+
{
|
|
381
|
+
"kind": "skill",
|
|
382
|
+
"data": {
|
|
383
|
+
"name": skill_name,
|
|
384
|
+
"already_active": skill_name in active_names,
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
default=str,
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
prompt = skill_prompt
|
|
391
|
+
if not prompt:
|
|
392
|
+
if args.json:
|
|
393
|
+
print(json.dumps({"kind": "chunk", "data": {"type": "response", "content": activation_content}}))
|
|
394
|
+
else:
|
|
395
|
+
print(activation_content)
|
|
396
|
+
if args.save or args.session:
|
|
397
|
+
session.history = agent.dump_message_history()
|
|
398
|
+
session.config = summary
|
|
399
|
+
store.save(session)
|
|
400
|
+
await agent.cleanup()
|
|
401
|
+
return 0
|
|
402
|
+
|
|
403
|
+
attachments, unresolved_mentions = build_file_attachments(prompt, project_path)
|
|
404
|
+
for mention in unresolved_mentions:
|
|
405
|
+
print(f"Note: @{mention} not found, sent as plain text", file=sys.stderr)
|
|
406
|
+
|
|
407
|
+
response_chunks: list[dict] = []
|
|
408
|
+
# Pump connection-manager events concurrently so sub-agent activity is
|
|
409
|
+
# reported in real time instead of all at once after streaming finishes.
|
|
410
|
+
pump_task = asyncio.create_task(_pump_ask_events(manager, args.json))
|
|
411
|
+
try:
|
|
412
|
+
stream = agent.process_message_stream(prompt, attachments) if attachments else agent.process_message_stream(prompt)
|
|
413
|
+
async for chunk in stream:
|
|
414
|
+
response_chunks.append(chunk)
|
|
415
|
+
if args.json:
|
|
416
|
+
print(json.dumps({"kind": "chunk", "data": chunk}, default=str))
|
|
417
|
+
elif chunk.get("type") == "response" and chunk.get("content"):
|
|
418
|
+
print(chunk["content"], end="" if not chunk.get("complete") else "\n")
|
|
419
|
+
|
|
420
|
+
if args.save or args.session:
|
|
421
|
+
session.history = agent.dump_message_history()
|
|
422
|
+
session.config = summary
|
|
423
|
+
store.save(session)
|
|
424
|
+
finally:
|
|
425
|
+
pump_task.cancel()
|
|
426
|
+
try:
|
|
427
|
+
await pump_task
|
|
428
|
+
except asyncio.CancelledError:
|
|
429
|
+
pass
|
|
430
|
+
while not manager.events.empty():
|
|
431
|
+
event = manager.events.get_nowait()
|
|
432
|
+
_print_ask_event(event, args.json)
|
|
433
|
+
await agent.cleanup()
|
|
434
|
+
|
|
435
|
+
if args.json:
|
|
436
|
+
print(json.dumps({"kind": "summary", "chunks": len(response_chunks), "session_id": session.session_id}))
|
|
437
|
+
return 0
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
async def _pump_ask_events(manager: CliConnectionManager, json_mode: bool) -> None:
|
|
441
|
+
while True:
|
|
442
|
+
event = await manager.next_event()
|
|
443
|
+
_print_ask_event(event, json_mode)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _print_ask_event(event, json_mode: bool) -> None:
|
|
447
|
+
if json_mode:
|
|
448
|
+
print(json.dumps({"kind": "event", "data": event.model_dump()}, default=str))
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
# Plain mode: keep piped stdout as the pure answer; report concise
|
|
452
|
+
# sub-agent lifecycle and tool activity on stderr.
|
|
453
|
+
info = event.sub_agent_info
|
|
454
|
+
if not info:
|
|
455
|
+
return
|
|
456
|
+
from . import theme
|
|
457
|
+
from .theme import Glyph
|
|
458
|
+
|
|
459
|
+
name = info.get("agent_name", event.sender)
|
|
460
|
+
sep = theme.g(Glyph.BULLET_SEP)
|
|
461
|
+
content = event.content
|
|
462
|
+
status = content.get("status")
|
|
463
|
+
message_type = content.get("message_type")
|
|
464
|
+
if status:
|
|
465
|
+
line = f"{theme.g(Glyph.SUB_AGENT)} {name} {sep} {str(status).lower()} {sep} {content.get('message', '')}"
|
|
466
|
+
_print_styled(line.rstrip(f" {sep}"), style="muted", stderr=True)
|
|
467
|
+
elif message_type in {"tool_call", "tool_error"}:
|
|
468
|
+
tool = content.get("tool_description") or content.get("tool_name") or "tool"
|
|
469
|
+
state = "failed" if message_type == "tool_error" else "running"
|
|
470
|
+
_print_styled(f"{theme.g(Glyph.TOOL)} {tool} {sep} {state}", style="muted", stderr=True)
|
|
471
|
+
# Streamed response chunks are suppressed in plain mode.
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _parse_skill_prompt(prompt: str, catalog: SkillCatalog) -> Optional[tuple[str, str]]:
|
|
475
|
+
stripped = prompt.strip()
|
|
476
|
+
if not stripped.startswith("/"):
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
command_text, _, rest = stripped.partition(" ")
|
|
480
|
+
command = command_text.lower()
|
|
481
|
+
if command == SKILLS_LIST_COMMAND:
|
|
482
|
+
return "skills", rest.strip()
|
|
483
|
+
if command in agent_command_names():
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
skill_name = command.removeprefix("/")
|
|
487
|
+
if catalog.get(skill_name) is None:
|
|
488
|
+
return None
|
|
489
|
+
return skill_name, rest.strip()
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _run_sessions(args: argparse.Namespace) -> int:
|
|
493
|
+
store = _store_from_args(args)
|
|
494
|
+
if args.sessions_command == "list":
|
|
495
|
+
project = args.project.expanduser().resolve() if args.project else None
|
|
496
|
+
records = store.list(project_path=project)
|
|
497
|
+
for record in records:
|
|
498
|
+
print(
|
|
499
|
+
f"{record.session_id}\t{record.thread_id}\t{record.updated_at}\t"
|
|
500
|
+
f"{record.mode}\t{record.project_path}\t{record.title}"
|
|
501
|
+
)
|
|
502
|
+
return 0
|
|
503
|
+
if args.sessions_command == "delete":
|
|
504
|
+
store.delete(args.session_id)
|
|
505
|
+
print(f"Deleted session {args.session_id}")
|
|
506
|
+
return 0
|
|
507
|
+
if args.sessions_command == "export":
|
|
508
|
+
payload = store.export(args.session_id)
|
|
509
|
+
if args.output:
|
|
510
|
+
args.output.write_text(payload, encoding="utf-8")
|
|
511
|
+
else:
|
|
512
|
+
print(payload, end="")
|
|
513
|
+
return 0
|
|
514
|
+
raise ValueError(f"Unknown sessions command: {args.sessions_command}")
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _run_doctor(args: argparse.Namespace) -> int:
|
|
518
|
+
from . import theme
|
|
519
|
+
from .theme import Glyph
|
|
520
|
+
|
|
521
|
+
console = _make_console()
|
|
522
|
+
|
|
523
|
+
def line(label: str, value: object, value_style: Optional[str] = None) -> None:
|
|
524
|
+
if console is None:
|
|
525
|
+
print(f"{label}: {value}")
|
|
526
|
+
return
|
|
527
|
+
from rich.text import Text
|
|
528
|
+
|
|
529
|
+
text = Text()
|
|
530
|
+
text.append(f"{label}: ", style="muted")
|
|
531
|
+
text.append(str(value), style=value_style or "")
|
|
532
|
+
console.print(text, highlight=False, soft_wrap=True)
|
|
533
|
+
|
|
534
|
+
project_path = _validate_project(args.project)
|
|
535
|
+
store = _store_from_args(args)
|
|
536
|
+
settings_store = _settings_store_from_args(args)
|
|
537
|
+
settings = settings_store.load()
|
|
538
|
+
line("Project", project_path)
|
|
539
|
+
line("State dir", store.root)
|
|
540
|
+
textual_installed = importlib.util.find_spec("textual") is not None
|
|
541
|
+
line("Textual installed", textual_installed, "success" if textual_installed else "warning")
|
|
542
|
+
if settings.active_provider and settings.active_model:
|
|
543
|
+
line("Stored active model", f"{settings.active_provider}/{settings.active_model}")
|
|
544
|
+
line("Stored API key", key_status(settings.active_provider, project_path, settings))
|
|
545
|
+
else:
|
|
546
|
+
line("Stored active model", "not configured", "warning")
|
|
547
|
+
|
|
548
|
+
try:
|
|
549
|
+
config = build_agent_config(project_path, _overrides_from_args(args), settings=settings)
|
|
550
|
+
except CliConfigError as exc:
|
|
551
|
+
_print_styled(f"{theme.g(Glyph.CROSS)} Configuration: invalid ({exc})", style="error")
|
|
552
|
+
return 2
|
|
553
|
+
|
|
554
|
+
summary = config_summary(config)
|
|
555
|
+
_print_styled(f"{theme.g(Glyph.CHECK)} Configuration: valid", style="success")
|
|
556
|
+
line("Long model", f"{summary['long_provider']}/{summary['long_model']}")
|
|
557
|
+
line("Fast model", f"{summary['fast_provider']}/{summary['fast_model']}")
|
|
558
|
+
line("Edit model", f"{summary['edit_provider']}/{summary['edit_model']}")
|
|
559
|
+
line("Thinking model", f"{summary['thinking_provider']}/{summary['thinking_model']}")
|
|
560
|
+
return 0
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
if __name__ == "__main__":
|
|
564
|
+
raise SystemExit(main())
|