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.
Files changed (171) hide show
  1. kolega_code/__init__.py +151 -0
  2. kolega_code/agent/__init__.py +42 -0
  3. kolega_code/agent/baseagent.py +998 -0
  4. kolega_code/agent/browseragent.py +123 -0
  5. kolega_code/agent/coder.py +157 -0
  6. kolega_code/agent/common.py +41 -0
  7. kolega_code/agent/compression.py +81 -0
  8. kolega_code/agent/context.py +112 -0
  9. kolega_code/agent/conversation.py +408 -0
  10. kolega_code/agent/generalagent.py +146 -0
  11. kolega_code/agent/investigationagent.py +123 -0
  12. kolega_code/agent/planningagent.py +187 -0
  13. kolega_code/agent/prompt_provider.py +196 -0
  14. kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
  15. kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
  16. kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
  17. kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
  18. kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
  19. kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
  20. kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
  21. kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
  22. kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
  23. kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
  24. kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
  25. kolega_code/agent/prompts.py +192 -0
  26. kolega_code/agent/tests/__init__.py +0 -0
  27. kolega_code/agent/tests/llm/__init__.py +0 -0
  28. kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
  29. kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
  30. kolega_code/agent/tests/llm/test_client.py +773 -0
  31. kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
  32. kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
  33. kolega_code/agent/tests/llm/test_exceptions.py +249 -0
  34. kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
  35. kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
  36. kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
  37. kolega_code/agent/tests/llm/test_model_specs.py +17 -0
  38. kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
  39. kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
  40. kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
  41. kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
  42. kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
  43. kolega_code/agent/tests/services/__init__.py +1 -0
  44. kolega_code/agent/tests/services/test_browser.py +447 -0
  45. kolega_code/agent/tests/services/test_browser_parity.py +353 -0
  46. kolega_code/agent/tests/services/test_file_system.py +699 -0
  47. kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
  48. kolega_code/agent/tests/services/test_terminal.py +154 -0
  49. kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
  50. kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
  51. kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
  52. kolega_code/agent/tests/test_base_agent.py +1942 -0
  53. kolega_code/agent/tests/test_coder_attachments.py +330 -0
  54. kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
  55. kolega_code/agent/tests/test_commands.py +179 -0
  56. kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
  57. kolega_code/agent/tests/test_empty_message_handling.py +48 -0
  58. kolega_code/agent/tests/test_general_agent.py +242 -0
  59. kolega_code/agent/tests/test_html.py +320 -0
  60. kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
  61. kolega_code/agent/tests/test_planning_agent.py +227 -0
  62. kolega_code/agent/tests/test_prompt_provider.py +271 -0
  63. kolega_code/agent/tests/test_tool_registry.py +102 -0
  64. kolega_code/agent/tests/test_tools.py +549 -0
  65. kolega_code/agent/tests/tool_backend/__init__.py +0 -0
  66. kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
  67. kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
  68. kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
  69. kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
  70. kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
  71. kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
  72. kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
  73. kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
  74. kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
  75. kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
  76. kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
  77. kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
  78. kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
  79. kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
  80. kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
  81. kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
  82. kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
  83. kolega_code/agent/tool_backend/agent_tool.py +414 -0
  84. kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
  85. kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
  86. kolega_code/agent/tool_backend/base_tool.py +217 -0
  87. kolega_code/agent/tool_backend/browser_tool.py +271 -0
  88. kolega_code/agent/tool_backend/build_tool.py +93 -0
  89. kolega_code/agent/tool_backend/create_file_tool.py +52 -0
  90. kolega_code/agent/tool_backend/glob_tool.py +323 -0
  91. kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
  92. kolega_code/agent/tool_backend/memory_tool.py +79 -0
  93. kolega_code/agent/tool_backend/read_file_tool.py +119 -0
  94. kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
  95. kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
  96. kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
  97. kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
  98. kolega_code/agent/tool_backend/streaming_tool.py +47 -0
  99. kolega_code/agent/tool_backend/terminal_tool.py +643 -0
  100. kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
  101. kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
  102. kolega_code/agent/tools.py +1704 -0
  103. kolega_code/agent/utils/commands.py +94 -0
  104. kolega_code/cli/__init__.py +1 -0
  105. kolega_code/cli/app.py +2756 -0
  106. kolega_code/cli/config.py +280 -0
  107. kolega_code/cli/connection.py +49 -0
  108. kolega_code/cli/file_index.py +147 -0
  109. kolega_code/cli/main.py +564 -0
  110. kolega_code/cli/mentions.py +155 -0
  111. kolega_code/cli/messages.py +89 -0
  112. kolega_code/cli/provider_registry.py +96 -0
  113. kolega_code/cli/session_store.py +207 -0
  114. kolega_code/cli/settings.py +87 -0
  115. kolega_code/cli/skills.py +409 -0
  116. kolega_code/cli/slash_commands.py +108 -0
  117. kolega_code/cli/tests/__init__.py +1 -0
  118. kolega_code/cli/tests/test_app.py +4251 -0
  119. kolega_code/cli/tests/test_cli_config.py +171 -0
  120. kolega_code/cli/tests/test_connection.py +26 -0
  121. kolega_code/cli/tests/test_file_index.py +103 -0
  122. kolega_code/cli/tests/test_main.py +455 -0
  123. kolega_code/cli/tests/test_mentions.py +108 -0
  124. kolega_code/cli/tests/test_session_store.py +67 -0
  125. kolega_code/cli/tests/test_settings.py +62 -0
  126. kolega_code/cli/tests/test_skills.py +157 -0
  127. kolega_code/cli/tests/test_slash_commands.py +88 -0
  128. kolega_code/cli/theme.py +180 -0
  129. kolega_code/config.py +154 -0
  130. kolega_code/events.py +202 -0
  131. kolega_code/llm/client.py +300 -0
  132. kolega_code/llm/exceptions.py +285 -0
  133. kolega_code/llm/instrumented_client.py +520 -0
  134. kolega_code/llm/models.py +1368 -0
  135. kolega_code/llm/providers/__init__.py +0 -0
  136. kolega_code/llm/providers/anthropic.py +387 -0
  137. kolega_code/llm/providers/base.py +71 -0
  138. kolega_code/llm/providers/google.py +157 -0
  139. kolega_code/llm/providers/models.py +37 -0
  140. kolega_code/llm/providers/openai.py +363 -0
  141. kolega_code/llm/ratelimit.py +40 -0
  142. kolega_code/llm/specs.py +67 -0
  143. kolega_code/llm/tool_execution_ids.py +18 -0
  144. kolega_code/models/__init__.py +9 -0
  145. kolega_code/models/sandbox_terminal_state.py +47 -0
  146. kolega_code/runtime.py +50 -0
  147. kolega_code/sandbox/README.md +200 -0
  148. kolega_code/sandbox/__init__.py +21 -0
  149. kolega_code/sandbox/async_filesystem.py +475 -0
  150. kolega_code/sandbox/base.py +297 -0
  151. kolega_code/sandbox/browser.py +25 -0
  152. kolega_code/sandbox/event_loop.py +43 -0
  153. kolega_code/sandbox/filesystem.py +341 -0
  154. kolega_code/sandbox/local.py +118 -0
  155. kolega_code/sandbox/serializer.py +175 -0
  156. kolega_code/sandbox/terminal.py +868 -0
  157. kolega_code/sandbox/utils.py +216 -0
  158. kolega_code/services/base.py +255 -0
  159. kolega_code/services/browser.py +444 -0
  160. kolega_code/services/file_system.py +749 -0
  161. kolega_code/services/html.py +221 -0
  162. kolega_code/services/terminal.py +903 -0
  163. kolega_code/tools/__init__.py +22 -0
  164. kolega_code/tools/core.py +33 -0
  165. kolega_code/tools/definitions.py +81 -0
  166. kolega_code/tools/registry.py +73 -0
  167. kolega_code-0.1.0.dist-info/METADATA +157 -0
  168. kolega_code-0.1.0.dist-info/RECORD +171 -0
  169. kolega_code-0.1.0.dist-info/WHEEL +4 -0
  170. kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
  171. kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -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())