cecli-dev 0.95.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. cecli/__init__.py +20 -0
  2. cecli/__main__.py +4 -0
  3. cecli/_version.py +34 -0
  4. cecli/args.py +1092 -0
  5. cecli/args_formatter.py +228 -0
  6. cecli/change_tracker.py +133 -0
  7. cecli/coders/__init__.py +38 -0
  8. cecli/coders/agent_coder.py +1872 -0
  9. cecli/coders/architect_coder.py +63 -0
  10. cecli/coders/ask_coder.py +8 -0
  11. cecli/coders/base_coder.py +3993 -0
  12. cecli/coders/chat_chunks.py +116 -0
  13. cecli/coders/context_coder.py +52 -0
  14. cecli/coders/copypaste_coder.py +269 -0
  15. cecli/coders/editblock_coder.py +656 -0
  16. cecli/coders/editblock_fenced_coder.py +9 -0
  17. cecli/coders/editblock_func_coder.py +140 -0
  18. cecli/coders/editor_diff_fenced_coder.py +8 -0
  19. cecli/coders/editor_editblock_coder.py +8 -0
  20. cecli/coders/editor_whole_coder.py +8 -0
  21. cecli/coders/help_coder.py +15 -0
  22. cecli/coders/patch_coder.py +705 -0
  23. cecli/coders/search_replace.py +757 -0
  24. cecli/coders/shell.py +37 -0
  25. cecli/coders/single_wholefile_func_coder.py +101 -0
  26. cecli/coders/udiff_coder.py +428 -0
  27. cecli/coders/udiff_simple.py +12 -0
  28. cecli/coders/wholefile_coder.py +143 -0
  29. cecli/coders/wholefile_func_coder.py +133 -0
  30. cecli/commands/__init__.py +192 -0
  31. cecli/commands/add.py +226 -0
  32. cecli/commands/agent.py +51 -0
  33. cecli/commands/architect.py +46 -0
  34. cecli/commands/ask.py +44 -0
  35. cecli/commands/chat_mode.py +0 -0
  36. cecli/commands/clear.py +37 -0
  37. cecli/commands/code.py +46 -0
  38. cecli/commands/command_prefix.py +44 -0
  39. cecli/commands/commit.py +52 -0
  40. cecli/commands/context.py +47 -0
  41. cecli/commands/context_blocks.py +124 -0
  42. cecli/commands/context_management.py +51 -0
  43. cecli/commands/copy.py +62 -0
  44. cecli/commands/copy_context.py +81 -0
  45. cecli/commands/core.py +287 -0
  46. cecli/commands/diff.py +68 -0
  47. cecli/commands/drop.py +217 -0
  48. cecli/commands/editor.py +78 -0
  49. cecli/commands/exit.py +55 -0
  50. cecli/commands/git.py +57 -0
  51. cecli/commands/help.py +140 -0
  52. cecli/commands/history_search.py +40 -0
  53. cecli/commands/lint.py +109 -0
  54. cecli/commands/list_sessions.py +56 -0
  55. cecli/commands/load.py +85 -0
  56. cecli/commands/load_session.py +48 -0
  57. cecli/commands/load_skill.py +68 -0
  58. cecli/commands/ls.py +75 -0
  59. cecli/commands/map.py +37 -0
  60. cecli/commands/map_refresh.py +35 -0
  61. cecli/commands/model.py +118 -0
  62. cecli/commands/models.py +41 -0
  63. cecli/commands/multiline_mode.py +38 -0
  64. cecli/commands/paste.py +91 -0
  65. cecli/commands/quit.py +32 -0
  66. cecli/commands/read_only.py +267 -0
  67. cecli/commands/read_only_stub.py +270 -0
  68. cecli/commands/reasoning_effort.py +70 -0
  69. cecli/commands/remove_skill.py +68 -0
  70. cecli/commands/report.py +40 -0
  71. cecli/commands/reset.py +88 -0
  72. cecli/commands/run.py +99 -0
  73. cecli/commands/save.py +49 -0
  74. cecli/commands/save_session.py +43 -0
  75. cecli/commands/settings.py +69 -0
  76. cecli/commands/test.py +58 -0
  77. cecli/commands/think_tokens.py +74 -0
  78. cecli/commands/tokens.py +207 -0
  79. cecli/commands/undo.py +145 -0
  80. cecli/commands/utils/__init__.py +0 -0
  81. cecli/commands/utils/base_command.py +131 -0
  82. cecli/commands/utils/helpers.py +142 -0
  83. cecli/commands/utils/registry.py +53 -0
  84. cecli/commands/utils/save_load_manager.py +98 -0
  85. cecli/commands/voice.py +78 -0
  86. cecli/commands/weak_model.py +123 -0
  87. cecli/commands/web.py +87 -0
  88. cecli/deprecated_args.py +185 -0
  89. cecli/diffs.py +129 -0
  90. cecli/dump.py +29 -0
  91. cecli/editor.py +147 -0
  92. cecli/exceptions.py +115 -0
  93. cecli/format_settings.py +26 -0
  94. cecli/help.py +119 -0
  95. cecli/help_pats.py +19 -0
  96. cecli/helpers/__init__.py +9 -0
  97. cecli/helpers/copypaste.py +123 -0
  98. cecli/helpers/coroutines.py +8 -0
  99. cecli/helpers/file_searcher.py +142 -0
  100. cecli/helpers/model_providers.py +552 -0
  101. cecli/helpers/plugin_manager.py +81 -0
  102. cecli/helpers/profiler.py +162 -0
  103. cecli/helpers/requests.py +77 -0
  104. cecli/helpers/similarity.py +98 -0
  105. cecli/helpers/skills.py +577 -0
  106. cecli/history.py +186 -0
  107. cecli/io.py +1782 -0
  108. cecli/linter.py +304 -0
  109. cecli/llm.py +101 -0
  110. cecli/main.py +1280 -0
  111. cecli/mcp/__init__.py +154 -0
  112. cecli/mcp/oauth.py +250 -0
  113. cecli/mcp/server.py +278 -0
  114. cecli/mdstream.py +243 -0
  115. cecli/models.py +1255 -0
  116. cecli/onboarding.py +301 -0
  117. cecli/prompts/__init__.py +0 -0
  118. cecli/prompts/agent.yml +71 -0
  119. cecli/prompts/architect.yml +35 -0
  120. cecli/prompts/ask.yml +31 -0
  121. cecli/prompts/base.yml +99 -0
  122. cecli/prompts/context.yml +60 -0
  123. cecli/prompts/copypaste.yml +5 -0
  124. cecli/prompts/editblock.yml +143 -0
  125. cecli/prompts/editblock_fenced.yml +106 -0
  126. cecli/prompts/editblock_func.yml +25 -0
  127. cecli/prompts/editor_diff_fenced.yml +115 -0
  128. cecli/prompts/editor_editblock.yml +121 -0
  129. cecli/prompts/editor_whole.yml +46 -0
  130. cecli/prompts/help.yml +37 -0
  131. cecli/prompts/patch.yml +110 -0
  132. cecli/prompts/single_wholefile_func.yml +24 -0
  133. cecli/prompts/udiff.yml +106 -0
  134. cecli/prompts/udiff_simple.yml +13 -0
  135. cecli/prompts/utils/__init__.py +0 -0
  136. cecli/prompts/utils/prompt_registry.py +167 -0
  137. cecli/prompts/utils/system.py +56 -0
  138. cecli/prompts/wholefile.yml +50 -0
  139. cecli/prompts/wholefile_func.yml +24 -0
  140. cecli/queries/tree-sitter-language-pack/README.md +7 -0
  141. cecli/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  142. cecli/queries/tree-sitter-language-pack/c-tags.scm +12 -0
  143. cecli/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  144. cecli/queries/tree-sitter-language-pack/clojure-tags.scm +12 -0
  145. cecli/queries/tree-sitter-language-pack/commonlisp-tags.scm +127 -0
  146. cecli/queries/tree-sitter-language-pack/cpp-tags.scm +18 -0
  147. cecli/queries/tree-sitter-language-pack/csharp-tags.scm +32 -0
  148. cecli/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  149. cecli/queries/tree-sitter-language-pack/dart-tags.scm +97 -0
  150. cecli/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  151. cecli/queries/tree-sitter-language-pack/elixir-tags.scm +59 -0
  152. cecli/queries/tree-sitter-language-pack/elm-tags.scm +22 -0
  153. cecli/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  154. cecli/queries/tree-sitter-language-pack/go-tags.scm +49 -0
  155. cecli/queries/tree-sitter-language-pack/java-tags.scm +26 -0
  156. cecli/queries/tree-sitter-language-pack/javascript-tags.scm +96 -0
  157. cecli/queries/tree-sitter-language-pack/lua-tags.scm +39 -0
  158. cecli/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  159. cecli/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  160. cecli/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +101 -0
  161. cecli/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  162. cecli/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  163. cecli/queries/tree-sitter-language-pack/python-tags.scm +24 -0
  164. cecli/queries/tree-sitter-language-pack/r-tags.scm +27 -0
  165. cecli/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  166. cecli/queries/tree-sitter-language-pack/ruby-tags.scm +69 -0
  167. cecli/queries/tree-sitter-language-pack/rust-tags.scm +63 -0
  168. cecli/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  169. cecli/queries/tree-sitter-language-pack/swift-tags.scm +54 -0
  170. cecli/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  171. cecli/queries/tree-sitter-languages/README.md +24 -0
  172. cecli/queries/tree-sitter-languages/c-tags.scm +12 -0
  173. cecli/queries/tree-sitter-languages/c_sharp-tags.scm +52 -0
  174. cecli/queries/tree-sitter-languages/cpp-tags.scm +18 -0
  175. cecli/queries/tree-sitter-languages/dart-tags.scm +92 -0
  176. cecli/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  177. cecli/queries/tree-sitter-languages/elixir-tags.scm +59 -0
  178. cecli/queries/tree-sitter-languages/elm-tags.scm +22 -0
  179. cecli/queries/tree-sitter-languages/fortran-tags.scm +18 -0
  180. cecli/queries/tree-sitter-languages/go-tags.scm +36 -0
  181. cecli/queries/tree-sitter-languages/haskell-tags.scm +5 -0
  182. cecli/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  183. cecli/queries/tree-sitter-languages/java-tags.scm +26 -0
  184. cecli/queries/tree-sitter-languages/javascript-tags.scm +96 -0
  185. cecli/queries/tree-sitter-languages/julia-tags.scm +60 -0
  186. cecli/queries/tree-sitter-languages/kotlin-tags.scm +30 -0
  187. cecli/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  188. cecli/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  189. cecli/queries/tree-sitter-languages/ocaml_interface-tags.scm +104 -0
  190. cecli/queries/tree-sitter-languages/php-tags.scm +32 -0
  191. cecli/queries/tree-sitter-languages/python-tags.scm +22 -0
  192. cecli/queries/tree-sitter-languages/ql-tags.scm +26 -0
  193. cecli/queries/tree-sitter-languages/ruby-tags.scm +69 -0
  194. cecli/queries/tree-sitter-languages/rust-tags.scm +63 -0
  195. cecli/queries/tree-sitter-languages/scala-tags.scm +64 -0
  196. cecli/queries/tree-sitter-languages/typescript-tags.scm +44 -0
  197. cecli/queries/tree-sitter-languages/zig-tags.scm +20 -0
  198. cecli/reasoning_tags.py +82 -0
  199. cecli/repo.py +626 -0
  200. cecli/repomap.py +1368 -0
  201. cecli/report.py +260 -0
  202. cecli/resources/__init__.py +3 -0
  203. cecli/resources/model-metadata.json +25751 -0
  204. cecli/resources/model-settings.yml +2394 -0
  205. cecli/resources/providers.json +67 -0
  206. cecli/run_cmd.py +143 -0
  207. cecli/scrape.py +295 -0
  208. cecli/sendchat.py +250 -0
  209. cecli/sessions.py +281 -0
  210. cecli/special.py +203 -0
  211. cecli/tools/__init__.py +72 -0
  212. cecli/tools/command.py +103 -0
  213. cecli/tools/command_interactive.py +113 -0
  214. cecli/tools/context_manager.py +175 -0
  215. cecli/tools/delete_block.py +154 -0
  216. cecli/tools/delete_line.py +120 -0
  217. cecli/tools/delete_lines.py +144 -0
  218. cecli/tools/extract_lines.py +281 -0
  219. cecli/tools/finished.py +35 -0
  220. cecli/tools/git_branch.py +132 -0
  221. cecli/tools/git_diff.py +49 -0
  222. cecli/tools/git_log.py +43 -0
  223. cecli/tools/git_remote.py +39 -0
  224. cecli/tools/git_show.py +37 -0
  225. cecli/tools/git_status.py +32 -0
  226. cecli/tools/grep.py +242 -0
  227. cecli/tools/indent_lines.py +195 -0
  228. cecli/tools/insert_block.py +263 -0
  229. cecli/tools/list_changes.py +71 -0
  230. cecli/tools/load_skill.py +51 -0
  231. cecli/tools/ls.py +77 -0
  232. cecli/tools/remove_skill.py +51 -0
  233. cecli/tools/replace_all.py +113 -0
  234. cecli/tools/replace_line.py +135 -0
  235. cecli/tools/replace_lines.py +180 -0
  236. cecli/tools/replace_text.py +186 -0
  237. cecli/tools/show_numbered_context.py +137 -0
  238. cecli/tools/thinking.py +52 -0
  239. cecli/tools/undo_change.py +82 -0
  240. cecli/tools/update_todo_list.py +148 -0
  241. cecli/tools/utils/base_tool.py +64 -0
  242. cecli/tools/utils/helpers.py +359 -0
  243. cecli/tools/utils/output.py +119 -0
  244. cecli/tools/utils/registry.py +145 -0
  245. cecli/tools/view_files_matching.py +138 -0
  246. cecli/tools/view_files_with_symbol.py +117 -0
  247. cecli/tui/__init__.py +83 -0
  248. cecli/tui/app.py +971 -0
  249. cecli/tui/io.py +566 -0
  250. cecli/tui/styles.tcss +117 -0
  251. cecli/tui/widgets/__init__.py +19 -0
  252. cecli/tui/widgets/completion_bar.py +331 -0
  253. cecli/tui/widgets/file_list.py +76 -0
  254. cecli/tui/widgets/footer.py +165 -0
  255. cecli/tui/widgets/input_area.py +320 -0
  256. cecli/tui/widgets/key_hints.py +16 -0
  257. cecli/tui/widgets/output.py +354 -0
  258. cecli/tui/widgets/status_bar.py +279 -0
  259. cecli/tui/worker.py +160 -0
  260. cecli/urls.py +16 -0
  261. cecli/utils.py +499 -0
  262. cecli/versioncheck.py +90 -0
  263. cecli/voice.py +90 -0
  264. cecli/waiting.py +38 -0
  265. cecli/watch.py +316 -0
  266. cecli/watch_prompts.py +12 -0
  267. cecli/website/Gemfile +8 -0
  268. cecli/website/_includes/blame.md +162 -0
  269. cecli/website/_includes/get-started.md +22 -0
  270. cecli/website/_includes/help-tip.md +5 -0
  271. cecli/website/_includes/help.md +24 -0
  272. cecli/website/_includes/install.md +5 -0
  273. cecli/website/_includes/keys.md +4 -0
  274. cecli/website/_includes/model-warnings.md +67 -0
  275. cecli/website/_includes/multi-line.md +22 -0
  276. cecli/website/_includes/python-m-aider.md +5 -0
  277. cecli/website/_includes/recording.css +228 -0
  278. cecli/website/_includes/recording.md +34 -0
  279. cecli/website/_includes/replit-pipx.md +9 -0
  280. cecli/website/_includes/works-best.md +1 -0
  281. cecli/website/_sass/custom/custom.scss +103 -0
  282. cecli/website/docs/config/adv-model-settings.md +2498 -0
  283. cecli/website/docs/config/agent-mode.md +320 -0
  284. cecli/website/docs/config/aider_conf.md +548 -0
  285. cecli/website/docs/config/api-keys.md +90 -0
  286. cecli/website/docs/config/custom-commands.md +187 -0
  287. cecli/website/docs/config/dotenv.md +493 -0
  288. cecli/website/docs/config/editor.md +127 -0
  289. cecli/website/docs/config/mcp.md +210 -0
  290. cecli/website/docs/config/model-aliases.md +173 -0
  291. cecli/website/docs/config/options.md +890 -0
  292. cecli/website/docs/config/reasoning.md +210 -0
  293. cecli/website/docs/config/skills.md +172 -0
  294. cecli/website/docs/config/tui.md +126 -0
  295. cecli/website/docs/config.md +44 -0
  296. cecli/website/docs/faq.md +379 -0
  297. cecli/website/docs/git.md +76 -0
  298. cecli/website/docs/index.md +47 -0
  299. cecli/website/docs/install/codespaces.md +39 -0
  300. cecli/website/docs/install/docker.md +48 -0
  301. cecli/website/docs/install/optional.md +100 -0
  302. cecli/website/docs/install/replit.md +8 -0
  303. cecli/website/docs/install.md +115 -0
  304. cecli/website/docs/languages.md +264 -0
  305. cecli/website/docs/legal/contributor-agreement.md +111 -0
  306. cecli/website/docs/legal/privacy.md +104 -0
  307. cecli/website/docs/llms/anthropic.md +77 -0
  308. cecli/website/docs/llms/azure.md +48 -0
  309. cecli/website/docs/llms/bedrock.md +132 -0
  310. cecli/website/docs/llms/cohere.md +34 -0
  311. cecli/website/docs/llms/deepseek.md +32 -0
  312. cecli/website/docs/llms/gemini.md +49 -0
  313. cecli/website/docs/llms/github.md +111 -0
  314. cecli/website/docs/llms/groq.md +36 -0
  315. cecli/website/docs/llms/lm-studio.md +39 -0
  316. cecli/website/docs/llms/ollama.md +75 -0
  317. cecli/website/docs/llms/openai-compat.md +39 -0
  318. cecli/website/docs/llms/openai.md +58 -0
  319. cecli/website/docs/llms/openrouter.md +78 -0
  320. cecli/website/docs/llms/other.md +117 -0
  321. cecli/website/docs/llms/vertex.md +50 -0
  322. cecli/website/docs/llms/warnings.md +10 -0
  323. cecli/website/docs/llms/xai.md +53 -0
  324. cecli/website/docs/llms.md +54 -0
  325. cecli/website/docs/more/analytics.md +127 -0
  326. cecli/website/docs/more/edit-formats.md +116 -0
  327. cecli/website/docs/more/infinite-output.md +192 -0
  328. cecli/website/docs/more-info.md +8 -0
  329. cecli/website/docs/recordings/auto-accept-architect.md +31 -0
  330. cecli/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  331. cecli/website/docs/recordings/index.md +21 -0
  332. cecli/website/docs/recordings/model-accepts-settings.md +69 -0
  333. cecli/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  334. cecli/website/docs/repomap.md +112 -0
  335. cecli/website/docs/scripting.md +100 -0
  336. cecli/website/docs/sessions.md +213 -0
  337. cecli/website/docs/troubleshooting/aider-not-found.md +24 -0
  338. cecli/website/docs/troubleshooting/edit-errors.md +76 -0
  339. cecli/website/docs/troubleshooting/imports.md +62 -0
  340. cecli/website/docs/troubleshooting/models-and-keys.md +54 -0
  341. cecli/website/docs/troubleshooting/support.md +79 -0
  342. cecli/website/docs/troubleshooting/token-limits.md +96 -0
  343. cecli/website/docs/troubleshooting/warnings.md +12 -0
  344. cecli/website/docs/troubleshooting.md +11 -0
  345. cecli/website/docs/usage/browser.md +57 -0
  346. cecli/website/docs/usage/caching.md +49 -0
  347. cecli/website/docs/usage/commands.md +133 -0
  348. cecli/website/docs/usage/conventions.md +119 -0
  349. cecli/website/docs/usage/copypaste.md +136 -0
  350. cecli/website/docs/usage/images-urls.md +48 -0
  351. cecli/website/docs/usage/lint-test.md +118 -0
  352. cecli/website/docs/usage/modes.md +211 -0
  353. cecli/website/docs/usage/not-code.md +179 -0
  354. cecli/website/docs/usage/notifications.md +87 -0
  355. cecli/website/docs/usage/tips.md +79 -0
  356. cecli/website/docs/usage/tutorials.md +30 -0
  357. cecli/website/docs/usage/voice.md +121 -0
  358. cecli/website/docs/usage/watch.md +294 -0
  359. cecli/website/docs/usage.md +102 -0
  360. cecli/website/share/index.md +101 -0
  361. cecli_dev-0.95.5.dist-info/METADATA +549 -0
  362. cecli_dev-0.95.5.dist-info/RECORD +366 -0
  363. cecli_dev-0.95.5.dist-info/WHEEL +5 -0
  364. cecli_dev-0.95.5.dist-info/entry_points.txt +4 -0
  365. cecli_dev-0.95.5.dist-info/licenses/LICENSE.txt +202 -0
  366. cecli_dev-0.95.5.dist-info/top_level.txt +1 -0
cecli/main.py ADDED
@@ -0,0 +1,1280 @@
1
+ import os
2
+
3
+ from cecli.helpers.file_searcher import handle_core_files
4
+
5
+ try:
6
+ if not os.getenv("CECLI_DEFAULT_TLS"):
7
+ import truststore
8
+
9
+ truststore.inject_into_ssl()
10
+ except Exception as e:
11
+ print(e)
12
+ pass
13
+ import asyncio
14
+ import json
15
+ import os
16
+ import re
17
+ import sys
18
+ import threading
19
+ import time
20
+ import traceback
21
+ import webbrowser
22
+ from dataclasses import fields
23
+ from pathlib import Path
24
+
25
+ try:
26
+ import git
27
+ except ImportError:
28
+ git = None
29
+ import importlib_resources
30
+ import shtab
31
+ from dotenv import load_dotenv
32
+
33
+ if sys.platform == "win32":
34
+ if hasattr(asyncio, "set_event_loop_policy"):
35
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
36
+ from prompt_toolkit.enums import EditingMode
37
+
38
+ from cecli import __version__, models, urls, utils
39
+ from cecli.args import get_parser
40
+ from cecli.coders import Coder
41
+ from cecli.coders.base_coder import UnknownEditFormat
42
+ from cecli.commands import Commands, SwitchCoderSignal
43
+ from cecli.deprecated_args import handle_deprecated_model_args
44
+ from cecli.format_settings import format_settings, scrub_sensitive_info
45
+ from cecli.helpers.copypaste import ClipboardWatcher
46
+ from cecli.helpers.file_searcher import generate_search_path_list
47
+ from cecli.history import ChatSummary
48
+ from cecli.io import InputOutput
49
+ from cecli.llm import litellm
50
+ from cecli.mcp import load_mcp_servers
51
+ from cecli.models import ModelSettings
52
+ from cecli.onboarding import offer_openrouter_oauth, select_default_model
53
+ from cecli.repo import ANY_GIT_ERROR, GitRepo
54
+ from cecli.report import report_uncaught_exceptions, set_args_error_data
55
+ from cecli.versioncheck import check_version, install_from_main_branch, install_upgrade
56
+ from cecli.watch import FileWatcher
57
+
58
+ from .dump import dump # noqa
59
+
60
+
61
+ def convert_yaml_to_json_string(value):
62
+ """
63
+ Convert YAML dict/list values to JSON strings for compatibility.
64
+
65
+ configargparse.YAMLConfigFileParser converts YAML to Python objects,
66
+ but some arguments expect JSON strings. This function handles:
67
+ - Direct dict/list objects
68
+ - String representations of dicts/lists (Python literals)
69
+ - Already JSON strings (passed through unchanged)
70
+
71
+ Args:
72
+ value: The value to convert
73
+
74
+ Returns:
75
+ str: JSON string if value is a dict/list, otherwise the original value
76
+ """
77
+ if value is None:
78
+ return None
79
+ if isinstance(value, (dict, list)):
80
+ return json.dumps(value)
81
+ if isinstance(value, str):
82
+ try:
83
+ import ast
84
+
85
+ parsed = ast.literal_eval(value)
86
+ if isinstance(parsed, (dict, list)):
87
+ return json.dumps(parsed)
88
+ except (SyntaxError, ValueError):
89
+ pass
90
+ return value
91
+
92
+
93
+ def check_config_files_for_yes(config_files):
94
+ found = False
95
+ for config_file in config_files:
96
+ if Path(config_file).exists():
97
+ try:
98
+ with open(config_file, "r") as f:
99
+ for line in f:
100
+ if line.strip().startswith("yes:"):
101
+ print("Configuration error detected.")
102
+ print(f"The file {config_file} contains a line starting with 'yes:'")
103
+ print("Please replace 'yes:' with 'yes-always:' in this file.")
104
+ found = True
105
+ except Exception:
106
+ pass
107
+ return found
108
+
109
+
110
+ def get_git_root():
111
+ """Try and guess the git repo, since the conf.yml can be at the repo root"""
112
+ try:
113
+ repo = git.Repo(search_parent_directories=True)
114
+ return repo.working_tree_dir
115
+ except (git.InvalidGitRepositoryError, FileNotFoundError):
116
+ return None
117
+
118
+
119
+ def guessed_wrong_repo(io, git_root, fnames, git_dname):
120
+ """After we parse the args, we can determine the real repo. Did we guess wrong?"""
121
+ try:
122
+ check_repo = Path(GitRepo(io, fnames, git_dname).root).resolve()
123
+ except (OSError,) + ANY_GIT_ERROR:
124
+ return
125
+ if not git_root:
126
+ return str(check_repo)
127
+ git_root = Path(git_root).resolve()
128
+ if check_repo == git_root:
129
+ return
130
+ return str(check_repo)
131
+
132
+
133
+ def validate_tui_args(args):
134
+ """Validate that incompatible flags aren't used with --tui"""
135
+ if not args.tui:
136
+ return
137
+ incompatible = []
138
+ if args.vim:
139
+ incompatible.append("--vim")
140
+ if not args.fancy_input:
141
+ incompatible.append("--no-fancy-input")
142
+ if incompatible:
143
+ print(f"Error: --tui is incompatible with: {', '.join(incompatible)}")
144
+ print("Remove these flags or use standard CLI mode.")
145
+ sys.exit(1)
146
+
147
+
148
+ async def make_new_repo(git_root, io):
149
+ try:
150
+ repo = git.Repo.init(git_root)
151
+ await check_gitignore(git_root, io, False)
152
+ except ANY_GIT_ERROR as err:
153
+ io.tool_error(f"Unable to create git repo in {git_root}")
154
+ io.tool_output(str(err))
155
+ return
156
+ io.tool_output(f"Git repository created in {git_root}")
157
+ return repo
158
+
159
+
160
+ async def setup_git(git_root, io):
161
+ if git is None:
162
+ return
163
+ try:
164
+ cwd = Path.cwd()
165
+ except OSError:
166
+ cwd = None
167
+ repo = None
168
+ if git_root:
169
+ try:
170
+ repo = git.Repo(git_root)
171
+ except ANY_GIT_ERROR:
172
+ pass
173
+ elif cwd == Path.home():
174
+ io.tool_warning(
175
+ "You should probably run cecli in your project's directory, not your home dir."
176
+ )
177
+ return
178
+ elif cwd and await io.confirm_ask(
179
+ "No git repo found, create one to track cecli's changes (recommended)?", acknowledge=True
180
+ ):
181
+ git_root = str(cwd.resolve())
182
+ repo = await make_new_repo(git_root, io)
183
+ if not repo:
184
+ return
185
+ try:
186
+ user_name = repo.git.config("--get", "user.name") or None
187
+ except git.exc.GitCommandError:
188
+ user_name = None
189
+ try:
190
+ user_email = repo.git.config("--get", "user.email") or None
191
+ except git.exc.GitCommandError:
192
+ user_email = None
193
+ if user_name and user_email:
194
+ return repo.working_tree_dir
195
+ with repo.config_writer() as git_config:
196
+ if not user_name:
197
+ git_config.set_value("user", "name", "Your Name")
198
+ io.tool_warning('Update git name with: git config user.name "Your Name"')
199
+ if not user_email:
200
+ git_config.set_value("user", "email", "you@example.com")
201
+ io.tool_warning('Update git email with: git config user.email "you@example.com"')
202
+ return repo.working_tree_dir
203
+
204
+
205
+ async def check_gitignore(git_root, io, ask=True):
206
+ if not git_root:
207
+ return
208
+ try:
209
+ repo = git.Repo(git_root)
210
+ patterns_to_add = []
211
+ if not repo.ignored(".cecli"):
212
+ patterns_to_add.append(".cecli*")
213
+ env_path = Path(git_root) / ".env"
214
+ if env_path.exists() and not repo.ignored(".env"):
215
+ patterns_to_add.append(".env")
216
+ if not patterns_to_add:
217
+ return
218
+ gitignore_file = Path(git_root) / ".gitignore"
219
+ if gitignore_file.exists():
220
+ try:
221
+ content = io.read_text(gitignore_file)
222
+ if content is None:
223
+ return
224
+ if not content.endswith("\n"):
225
+ content += "\n"
226
+ except OSError as e:
227
+ io.tool_error(f"Error when trying to read {gitignore_file}: {e}")
228
+ return
229
+ else:
230
+ content = ""
231
+ except ANY_GIT_ERROR:
232
+ return
233
+ if ask:
234
+ io.tool_output("You can skip this check with --no-gitignore")
235
+ if not await io.confirm_ask(
236
+ f"Add {', '.join(patterns_to_add)} to .gitignore (recommended)?", acknowledge=True
237
+ ):
238
+ return
239
+ content += "\n".join(patterns_to_add) + "\n"
240
+ try:
241
+ io.write_text(gitignore_file, content)
242
+ io.tool_output(f"Added {', '.join(patterns_to_add)} to .gitignore")
243
+ except OSError as e:
244
+ io.tool_error(f"Error when trying to write to {gitignore_file}: {e}")
245
+ io.tool_output(
246
+ "Try running with appropriate permissions or manually add these patterns to .gitignore:"
247
+ )
248
+ for pattern in patterns_to_add:
249
+ io.tool_output(f" {pattern}")
250
+
251
+
252
+ def parse_lint_cmds(lint_cmds, io):
253
+ err = False
254
+ res = dict()
255
+ for lint_cmd in lint_cmds:
256
+ if re.match("^[a-z]+:.*", lint_cmd):
257
+ pieces = lint_cmd.split(":")
258
+ lang = pieces[0]
259
+ cmd = lint_cmd[len(lang) + 1 :]
260
+ lang = lang.strip()
261
+ else:
262
+ lang = None
263
+ cmd = lint_cmd
264
+ cmd = cmd.strip()
265
+ if cmd:
266
+ res[lang] = cmd
267
+ else:
268
+ io.tool_error(f'Unable to parse --lint-cmd "{lint_cmd}"')
269
+ io.tool_output('The arg should be "language: cmd --args ..."')
270
+ io.tool_output('For example: --lint-cmd "python: flake8 --select=E9"')
271
+ err = True
272
+ if err:
273
+ return
274
+ return res
275
+
276
+
277
+ def register_models(git_root, model_settings_fname, io, verbose=False):
278
+ model_settings_files = generate_search_path_list(
279
+ ".cecli.model.settings.yml", git_root, model_settings_fname
280
+ )
281
+ try:
282
+ files_loaded = models.register_models(model_settings_files)
283
+ if len(files_loaded) > 0:
284
+ if verbose:
285
+ io.tool_output("Loaded model settings from:")
286
+ for file_loaded in files_loaded:
287
+ io.tool_output(f" - {file_loaded}")
288
+ elif verbose:
289
+ io.tool_output("No model settings files loaded")
290
+ if (
291
+ model_settings_fname
292
+ and model_settings_fname not in files_loaded
293
+ and model_settings_fname != ".cecli.model.settings.yml"
294
+ ):
295
+ io.tool_warning(f"Model Settings File Not Found: {model_settings_fname}")
296
+ except Exception as e:
297
+ io.tool_error(f"Error loading cecli model settings: {e}")
298
+ return 1
299
+ if verbose:
300
+ io.tool_output("Searched for model settings files:")
301
+ for file in model_settings_files:
302
+ io.tool_output(f" - {file}")
303
+ return None
304
+
305
+
306
+ def load_dotenv_files(git_root, dotenv_fname, encoding="utf-8"):
307
+ dotenv_files = generate_search_path_list(".env", git_root, dotenv_fname)
308
+ oauth_keys_file = handle_core_files(Path.home() / ".cecli" / "oauth-keys.env")
309
+ if oauth_keys_file.exists():
310
+ dotenv_files.insert(0, str(oauth_keys_file.resolve()))
311
+ dotenv_files = list(dict.fromkeys(dotenv_files))
312
+ loaded = []
313
+ for fname in dotenv_files:
314
+ try:
315
+ if Path(fname).exists():
316
+ load_dotenv(fname, override=True, encoding=encoding)
317
+ loaded.append(fname)
318
+ except OSError as e:
319
+ print(f"OSError loading {fname}: {e}")
320
+ except Exception as e:
321
+ print(f"Error loading {fname}: {e}")
322
+ return loaded
323
+
324
+
325
+ def register_litellm_models(git_root, model_metadata_fname, io, verbose=False):
326
+ model_metadata_files = []
327
+ resource_metadata = importlib_resources.files("cecli.resources").joinpath("model-metadata.json")
328
+ model_metadata_files.append(str(resource_metadata))
329
+ model_metadata_files += generate_search_path_list(
330
+ ".cecli.model.metadata.json", git_root, model_metadata_fname
331
+ )
332
+ try:
333
+ model_metadata_files_loaded = models.register_litellm_models(model_metadata_files)
334
+ if len(model_metadata_files_loaded) > 0 and verbose:
335
+ io.tool_output("Loaded model metadata from:")
336
+ for model_metadata_file in model_metadata_files_loaded:
337
+ io.tool_output(f" - {model_metadata_file}")
338
+ if (
339
+ model_metadata_fname
340
+ and model_metadata_fname not in model_metadata_files_loaded
341
+ and model_metadata_fname != ".cecli.model.metadata.json"
342
+ ):
343
+ io.tool_warning(f"Model Metadata File Not Found: {model_metadata_fname}")
344
+ except Exception as e:
345
+ io.tool_error(f"Error loading model metadata models: {e}")
346
+ return 1
347
+
348
+
349
+ def load_model_overrides(git_root, model_overrides_fname, io, verbose=False):
350
+ """Load model tag overrides from a YAML file."""
351
+ from pathlib import Path
352
+
353
+ import yaml
354
+
355
+ model_overrides_files = generate_search_path_list(
356
+ ".cecli.model.overrides.yml", git_root, model_overrides_fname
357
+ )
358
+ overrides = {}
359
+ files_loaded = []
360
+ for fname in model_overrides_files:
361
+ try:
362
+ if Path(fname).exists():
363
+ with open(fname, "r") as f:
364
+ content = yaml.safe_load(f)
365
+ if content:
366
+ for model_name, tags in content.items():
367
+ if model_name not in overrides:
368
+ overrides[model_name] = {}
369
+ overrides[model_name].update(tags)
370
+ files_loaded.append(fname)
371
+ except Exception as e:
372
+ io.tool_error(f"Error loading model overrides from {fname}: {e}")
373
+ if len(files_loaded) > 0 and verbose:
374
+ io.tool_output("Loaded model overrides from:")
375
+ for file_loaded in files_loaded:
376
+ io.tool_output(f" - {file_loaded}")
377
+ if (
378
+ model_overrides_fname
379
+ and model_overrides_fname not in files_loaded
380
+ and model_overrides_fname != ".cecli.model.overrides.yml"
381
+ ):
382
+ io.tool_warning(f"Model Overrides File Not Found: {model_overrides_fname}")
383
+ return overrides
384
+
385
+
386
+ def load_model_overrides_from_string(model_overrides_str, io):
387
+ """Load model tag overrides from a JSON/YAML string."""
388
+ import json
389
+
390
+ import yaml
391
+
392
+ overrides = {}
393
+ if not model_overrides_str:
394
+ return overrides
395
+ try:
396
+ try:
397
+ content = json.loads(model_overrides_str)
398
+ except json.JSONDecodeError:
399
+ content = yaml.safe_load(model_overrides_str)
400
+ if content and isinstance(content, dict):
401
+ for model_name, tags in content.items():
402
+ if model_name not in overrides:
403
+ overrides[model_name] = {}
404
+ overrides[model_name].update(tags)
405
+ return overrides
406
+ except Exception as e:
407
+ io.tool_error(f"Error parsing model overrides string: {e}")
408
+ return {}
409
+
410
+
411
+ async def sanity_check_repo(repo, io):
412
+ if not repo:
413
+ return True
414
+ if not repo.repo.working_tree_dir:
415
+ io.tool_error("The git repo does not seem to have a working tree?")
416
+ return False
417
+ bad_ver = False
418
+ try:
419
+ repo.get_tracked_files()
420
+ if not repo.git_repo_error:
421
+ return True
422
+ error_msg = str(repo.git_repo_error)
423
+ except UnicodeDecodeError as exc:
424
+ error_msg = (
425
+ "Failed to read the Git repository. "
426
+ "This issue is likely caused by a path encoded in a format different from "
427
+ f'the expected encoding "{sys.getfilesystemencoding()}"\n'
428
+ f"Internal error: {str(exc)}"
429
+ )
430
+ except ANY_GIT_ERROR as exc:
431
+ error_msg = str(exc)
432
+ bad_ver = "version in (1, 2)" in error_msg
433
+ except AssertionError as exc:
434
+ error_msg = str(exc)
435
+ bad_ver = True
436
+ if bad_ver:
437
+ io.tool_error("cecli only works with git repos with version number 1 or 2.")
438
+ io.tool_output("You may be able to convert your repo: git update-index --index-version=2")
439
+ io.tool_output("Or run cecli --no-git to proceed without using git.")
440
+ await io.offer_url(
441
+ urls.git_index_version, "Open documentation url for more info?", acknowledge=True
442
+ )
443
+ return False
444
+ io.tool_error("Unable to read git repository, it may be corrupt?")
445
+ io.tool_output(error_msg)
446
+ return False
447
+
448
+
449
+ PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
450
+ log_file = None
451
+ file_excludelist = {
452
+ "get_bottom_toolbar": True,
453
+ "<genexpr>": True,
454
+ "is_active": True,
455
+ "auto_save_session": True,
456
+ "input_task": True,
457
+ "output_task": True,
458
+ "check_output_queue": True,
459
+ "_animate_spinner": True,
460
+ "handle_output_message": True,
461
+ "update_spinner": True,
462
+ }
463
+
464
+
465
+ def custom_tracer(frame, event, arg):
466
+ try:
467
+ import os
468
+ except Exception:
469
+ return None
470
+ global log_file
471
+ if not log_file:
472
+ os.makedirs(".cecli/logs/", exist_ok=True)
473
+ log_file = open(".cecli/logs/debug.log", "w", buffering=1)
474
+ filename = os.path.abspath(frame.f_code.co_filename)
475
+ if not filename.startswith(PROJECT_ROOT):
476
+ return None
477
+ if filename.endswith("repo.py"):
478
+ return None
479
+ if event == "call":
480
+ func_name = frame.f_code.co_name
481
+ line_no = frame.f_lineno
482
+ if func_name not in file_excludelist:
483
+ log_file.write(
484
+ f"""-> CALL: {func_name}() in {os.path.basename(filename)}:{line_no} - {time.time()}
485
+ """
486
+ )
487
+ if event == "return":
488
+ func_name = frame.f_code.co_name
489
+ line_no = frame.f_lineno
490
+ if func_name not in file_excludelist:
491
+ log_file.write(
492
+ f"""<- RETURN: {func_name}() in {os.path.basename(filename)}:{line_no} - {time.time()}
493
+ """
494
+ )
495
+ return custom_tracer
496
+
497
+
498
+ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False):
499
+ if sys.platform == "win32":
500
+ if sys.version_info >= (3, 12) and hasattr(asyncio, "SelectorEventLoop"):
501
+ return asyncio.run(
502
+ main_async(argv, input, output, force_git_root, return_coder),
503
+ loop_factory=asyncio.SelectorEventLoop,
504
+ )
505
+ return asyncio.run(main_async(argv, input, output, force_git_root, return_coder))
506
+
507
+
508
+ async def main_async(argv=None, input=None, output=None, force_git_root=None, return_coder=False):
509
+ report_uncaught_exceptions()
510
+ if argv is None:
511
+ argv = sys.argv[1:]
512
+ if git is None:
513
+ git_root = None
514
+ elif force_git_root:
515
+ git_root = force_git_root
516
+ else:
517
+ git_root = get_git_root()
518
+ conf_fname = handle_core_files(Path(".cecli.conf.yml"))
519
+ default_config_files = []
520
+ try:
521
+ default_config_files += [conf_fname.resolve()]
522
+ except OSError:
523
+ pass
524
+ if git_root:
525
+ git_conf = Path(git_root) / conf_fname
526
+ if git_conf not in default_config_files:
527
+ default_config_files.append(git_conf)
528
+ default_config_files.append(Path.home() / conf_fname)
529
+ default_config_files = list(map(str, default_config_files))
530
+ parser = get_parser(default_config_files, git_root)
531
+ try:
532
+ args, unknown = parser.parse_known_args(argv)
533
+ except AttributeError as e:
534
+ if all(word in str(e) for word in ["bool", "object", "has", "no", "attribute", "strip"]):
535
+ if check_config_files_for_yes(default_config_files):
536
+ return await graceful_exit(None, 1)
537
+ raise e
538
+ if args.verbose:
539
+ print("Config files search order, if no --config:")
540
+ for file in default_config_files:
541
+ exists = "(exists)" if Path(file).exists() else ""
542
+ print(f" - {file} {exists}")
543
+ default_config_files.reverse()
544
+ parser = get_parser(default_config_files, git_root)
545
+ args, unknown = parser.parse_known_args(argv)
546
+ loaded_dotenvs = load_dotenv_files(git_root, args.env_file, args.encoding)
547
+ args, unknown = parser.parse_known_args(argv)
548
+ set_args_error_data(args)
549
+ if len(unknown):
550
+ print("Unknown Args: ", unknown)
551
+ if hasattr(args, "agent_config") and args.agent_config is not None:
552
+ args.agent_config = convert_yaml_to_json_string(args.agent_config)
553
+ if hasattr(args, "tui_config") and args.tui_config is not None:
554
+ args.tui_config = convert_yaml_to_json_string(args.tui_config)
555
+ if hasattr(args, "mcp_servers") and args.mcp_servers is not None:
556
+ args.mcp_servers = convert_yaml_to_json_string(args.mcp_servers)
557
+ if hasattr(args, "command_paths") and args.command_paths is not None:
558
+ args.command_paths = convert_yaml_to_json_string(args.command_paths)
559
+ if args.debug:
560
+ global log_file
561
+ os.makedirs(".cecli/logs/", exist_ok=True)
562
+ log_file = open(".cecli/logs/debug.log", "w", buffering=1)
563
+ sys.settrace(custom_tracer)
564
+ if args.shell_completions:
565
+ parser.prog = "cecli"
566
+ print(shtab.complete(parser, shell=args.shell_completions))
567
+ return await graceful_exit(None, 0)
568
+ if git is None:
569
+ args.git = False
570
+ if not args.verify_ssl:
571
+ import httpx
572
+
573
+ os.environ["SSL_VERIFY"] = ""
574
+ litellm._load_litellm()
575
+ litellm._lazy_module.client_session = httpx.Client(verify=False)
576
+ litellm._lazy_module.aclient_session = httpx.AsyncClient(verify=False)
577
+ models.model_info_manager.set_verify_ssl(False)
578
+ if args.timeout:
579
+ models.request_timeout = args.timeout
580
+ if args.dark_mode:
581
+ args.user_input_color = "#32FF32"
582
+ args.tool_error_color = "#FF3333"
583
+ args.tool_warning_color = "#FFFF00"
584
+ args.assistant_output_color = "#00FFFF"
585
+ args.code_theme = "monokai"
586
+ if args.light_mode:
587
+ args.user_input_color = "green"
588
+ args.tool_error_color = "red"
589
+ args.tool_warning_color = "#FFA500"
590
+ args.assistant_output_color = "blue"
591
+ args.code_theme = "default"
592
+ if return_coder and args.yes_always is None:
593
+ args.yes_always = True
594
+ if args.yes_always_commands:
595
+ args.yes_always = True
596
+ editing_mode = EditingMode.VI if args.vim else EditingMode.EMACS
597
+
598
+ def get_io(pretty):
599
+ return InputOutput(
600
+ pretty,
601
+ args.yes_always,
602
+ args.input_history_file,
603
+ args.chat_history_file,
604
+ input=input,
605
+ output=output,
606
+ user_input_color=args.user_input_color,
607
+ tool_output_color=args.tool_output_color,
608
+ tool_warning_color=args.tool_warning_color,
609
+ tool_error_color=args.tool_error_color,
610
+ completion_menu_color=args.completion_menu_color,
611
+ completion_menu_bg_color=args.completion_menu_bg_color,
612
+ completion_menu_current_color=args.completion_menu_current_color,
613
+ completion_menu_current_bg_color=args.completion_menu_current_bg_color,
614
+ assistant_output_color=args.assistant_output_color,
615
+ code_theme=args.code_theme,
616
+ dry_run=args.dry_run,
617
+ encoding=args.encoding,
618
+ line_endings=args.line_endings,
619
+ editingmode=editing_mode,
620
+ fancy_input=args.fancy_input,
621
+ multiline_mode=args.multiline,
622
+ notifications=args.notifications,
623
+ notifications_command=args.notifications_command,
624
+ verbose=args.verbose,
625
+ )
626
+
627
+ validate_tui_args(args)
628
+ output_queue = None
629
+ input_queue = None
630
+ pre_init_io = get_io(args.pretty)
631
+ if args.tui or args.tui is None and not args.linear_output:
632
+ try:
633
+ from cecli.tui import create_tui_io
634
+
635
+ args.tui = True
636
+ args.linear_output = True
637
+ print("Starting cecli TUI...", flush=True)
638
+ io, output_queue, input_queue = create_tui_io(args, editing_mode)
639
+ except ImportError as e:
640
+ print("Error: --tui requires 'textual' package")
641
+ print("Install with: pip install cecli[tui]")
642
+ print(f"Import error: {e}")
643
+ sys.exit(1)
644
+ else:
645
+ io = pre_init_io
646
+ if not args.tui:
647
+ try:
648
+ io.rule()
649
+ except UnicodeEncodeError as err:
650
+ if not io.pretty:
651
+ raise err
652
+ io = get_io(False)
653
+ io.tool_warning("Terminal does not support pretty output (UnicodeDecodeError)")
654
+ if args.set_env:
655
+ for env_setting in args.set_env:
656
+ try:
657
+ name, value = env_setting.split("=", 1)
658
+ os.environ[name.strip()] = value.strip()
659
+ except ValueError:
660
+ io.tool_error(f"Invalid --set-env format: {env_setting}")
661
+ io.tool_output("Format should be: ENV_VAR_NAME=value")
662
+ return await graceful_exit(None, 1)
663
+ if args.api_key:
664
+ for api_setting in args.api_key:
665
+ try:
666
+ provider, key = api_setting.split("=", 1)
667
+ env_var = f"{provider.strip().upper()}_API_KEY"
668
+ os.environ[env_var] = key.strip()
669
+ except ValueError:
670
+ io.tool_error(f"Invalid --api-key format: {api_setting}")
671
+ io.tool_output("Format should be: provider=key")
672
+ return await graceful_exit(None, 1)
673
+ if args.anthropic_api_key:
674
+ os.environ["ANTHROPIC_API_KEY"] = args.anthropic_api_key
675
+ if args.openai_api_key:
676
+ os.environ["OPENAI_API_KEY"] = args.openai_api_key
677
+ handle_deprecated_model_args(args, io)
678
+ if args.openai_api_base:
679
+ os.environ["OPENAI_API_BASE"] = args.openai_api_base
680
+ if args.openai_api_version:
681
+ io.tool_warning(
682
+ "--openai-api-version is deprecated, use --set-env OPENAI_API_VERSION=<value>"
683
+ )
684
+ os.environ["OPENAI_API_VERSION"] = args.openai_api_version
685
+ if args.openai_api_type:
686
+ io.tool_warning("--openai-api-type is deprecated, use --set-env OPENAI_API_TYPE=<value>")
687
+ os.environ["OPENAI_API_TYPE"] = args.openai_api_type
688
+ if args.openai_organization_id:
689
+ io.tool_warning(
690
+ "--openai-organization-id is deprecated, use --set-env OPENAI_ORGANIZATION=<value>"
691
+ )
692
+ os.environ["OPENAI_ORGANIZATION"] = args.openai_organization_id
693
+ if args.verbose:
694
+ for fname in loaded_dotenvs:
695
+ io.tool_output(f"Loaded {fname}")
696
+ all_files = args.files + (args.file or [])
697
+ all_files = utils.expand_glob_patterns(all_files)
698
+ fnames = [str(Path(fn).resolve()) for fn in all_files]
699
+ read_patterns = args.read or []
700
+ read_expanded = utils.expand_glob_patterns(read_patterns)
701
+ read_only_fnames = []
702
+ for fn in read_expanded:
703
+ path = Path(fn).expanduser().resolve()
704
+ if path.is_dir():
705
+ read_only_fnames.extend(str(f) for f in path.rglob("*") if f.is_file())
706
+ else:
707
+ read_only_fnames.append(str(path))
708
+ if len(all_files) > 1:
709
+ good = True
710
+ for fname in all_files:
711
+ if Path(fname).is_dir():
712
+ io.tool_error(f"{fname} is a directory, not provided alone.")
713
+ good = False
714
+ if not good:
715
+ io.tool_output(
716
+ "Provide either a single directory of a git repo, or a list of one or more files."
717
+ )
718
+ return await graceful_exit(None, 1)
719
+ git_dname = None
720
+ if len(all_files) == 1:
721
+ if Path(all_files[0]).is_dir():
722
+ if args.git:
723
+ git_dname = str(Path(all_files[0]).resolve())
724
+ fnames = []
725
+ else:
726
+ io.tool_error(f"{all_files[0]} is a directory, but --no-git selected.")
727
+ return await graceful_exit(None, 1)
728
+ if args.git and not force_git_root and git is not None:
729
+ right_repo_root = guessed_wrong_repo(io, git_root, fnames, git_dname)
730
+ if right_repo_root:
731
+ return await main_async(argv, input, output, right_repo_root, return_coder=return_coder)
732
+ if args.just_check_update:
733
+ update_available = await check_version(io, just_check=True, verbose=args.verbose)
734
+ return await graceful_exit(None, 0 if not update_available else 1)
735
+ if args.install_main_branch:
736
+ success = await install_from_main_branch(io)
737
+ return await graceful_exit(None, 0 if success else 1)
738
+ if args.upgrade:
739
+ success = await install_upgrade(io)
740
+ return await graceful_exit(None, 0 if success else 1)
741
+ if args.check_update:
742
+ await check_version(io, verbose=args.verbose)
743
+ if args.verbose:
744
+ show = format_settings(parser, args)
745
+ io.tool_output(show)
746
+ cmd_line = " ".join(sys.argv)
747
+ cmd_line = scrub_sensitive_info(args, cmd_line)
748
+ io.tool_output(cmd_line, log_only=True)
749
+ is_first_run = is_first_run_of_new_version(io, verbose=args.verbose)
750
+ await check_and_load_imports(io, is_first_run, verbose=args.verbose)
751
+ register_models(git_root, args.model_settings_file, io, verbose=args.verbose)
752
+ register_litellm_models(git_root, args.model_metadata_file, io, verbose=args.verbose)
753
+ if args.list_models:
754
+ models.print_matching_models(io, args.list_models)
755
+ return await graceful_exit(None)
756
+ if args.alias:
757
+ for alias_def in args.alias:
758
+ parts = alias_def.split(":", 1)
759
+ if len(parts) != 2:
760
+ io.tool_error(f"Invalid alias format: {alias_def}")
761
+ io.tool_output("Format should be: alias:model-name")
762
+ return await graceful_exit(None, 1)
763
+ alias, model = parts
764
+ models.MODEL_ALIASES[alias.strip()] = model.strip()
765
+ selected_model_name = await select_default_model(args, io)
766
+ if not selected_model_name:
767
+ return await graceful_exit(None, 1)
768
+ args.model = selected_model_name
769
+ model_overrides = {}
770
+ if args.model_overrides_file:
771
+ model_overrides = load_model_overrides(
772
+ git_root, args.model_overrides_file, io, verbose=args.verbose
773
+ )
774
+ if args.model_overrides:
775
+ direct_overrides = load_model_overrides_from_string(args.model_overrides, io)
776
+ for model_name, tags in direct_overrides.items():
777
+ if model_name not in model_overrides:
778
+ model_overrides[model_name] = {}
779
+ model_overrides[model_name].update(tags)
780
+ override_index = {}
781
+ for base_model, suffixes in model_overrides.items():
782
+ if not isinstance(suffixes, dict):
783
+ continue
784
+ for suffix, cfg in suffixes.items():
785
+ if not isinstance(cfg, dict):
786
+ continue
787
+ full_name = f"{base_model}:{suffix}"
788
+ override_index[full_name] = base_model, cfg
789
+
790
+ def apply_model_overrides(model_name):
791
+ """Return (effective_model_name, override_kwargs) for a given model_name.
792
+
793
+ If model_name exactly matches a configured "base:suffix" override, we
794
+ switch to the base model and apply that override dict. Otherwise we
795
+ leave the name unchanged and return empty overrides.
796
+ """
797
+ if not model_name:
798
+ return model_name, {}
799
+ prefix = ""
800
+ if model_name.startswith(models.COPY_PASTE_PREFIX):
801
+ prefix = models.COPY_PASTE_PREFIX
802
+ model_name = model_name[len(prefix) :]
803
+ entry = override_index.get(model_name)
804
+ if not entry:
805
+ model_name = prefix + model_name
806
+ return model_name, {}
807
+ base_model, cfg = entry
808
+ model_name = prefix + base_model
809
+ return model_name, cfg.copy()
810
+
811
+ main_model_name, main_model_overrides = apply_model_overrides(args.model)
812
+ weak_model_name, weak_model_overrides = apply_model_overrides(args.weak_model)
813
+ editor_model_name, editor_model_overrides = apply_model_overrides(args.editor_model)
814
+ weak_model_obj = None
815
+ if weak_model_name:
816
+ weak_model_obj = models.Model(
817
+ weak_model_name,
818
+ weak_model=False,
819
+ verbose=args.verbose,
820
+ io=io,
821
+ override_kwargs=weak_model_overrides,
822
+ )
823
+ editor_model_obj = None
824
+ if editor_model_name:
825
+ editor_model_obj = models.Model(
826
+ editor_model_name,
827
+ editor_model=False,
828
+ verbose=args.verbose,
829
+ io=io,
830
+ override_kwargs=editor_model_overrides,
831
+ )
832
+ if main_model_name.startswith("openrouter/") and not os.environ.get("OPENROUTER_API_KEY"):
833
+ io.tool_warning(
834
+ f"The specified model '{main_model_name}' requires an OpenRouter API key, which was not"
835
+ " found."
836
+ )
837
+ if await offer_openrouter_oauth(io):
838
+ if os.environ.get("OPENROUTER_API_KEY"):
839
+ io.tool_output("OpenRouter successfully connected.")
840
+ else:
841
+ io.tool_error(
842
+ "OpenRouter authentication seemed successful, but the key is still missing."
843
+ )
844
+ return await graceful_exit(None, 1)
845
+ else:
846
+ io.tool_error(
847
+ f"Unable to proceed without an OpenRouter API key for model '{main_model_name}'."
848
+ )
849
+ await io.offer_url(
850
+ urls.models_and_keys, "Open documentation URL for more info?", acknowledge=True
851
+ )
852
+ return await graceful_exit(None, 1)
853
+ main_model = models.Model(
854
+ main_model_name,
855
+ weak_model=weak_model_obj,
856
+ editor_model=editor_model_obj,
857
+ editor_edit_format=args.editor_edit_format,
858
+ verbose=args.verbose,
859
+ io=io,
860
+ override_kwargs=main_model_overrides,
861
+ )
862
+ if args.copy_paste and main_model.copy_paste_transport == "api":
863
+ main_model.enable_copy_paste_mode()
864
+ if main_model.remove_reasoning is not None:
865
+ io.tool_warning(
866
+ "Model setting 'remove_reasoning' is deprecated, please use 'reasoning_tag' instead."
867
+ )
868
+ if args.reasoning_effort is not None:
869
+ if (
870
+ not args.check_model_accepts_settings
871
+ or main_model.accepts_settings
872
+ and "reasoning_effort" in main_model.accepts_settings
873
+ ):
874
+ main_model.set_reasoning_effort(args.reasoning_effort)
875
+ if args.thinking_tokens is not None:
876
+ if (
877
+ not args.check_model_accepts_settings
878
+ or main_model.accepts_settings
879
+ and "thinking_tokens" in main_model.accepts_settings
880
+ ):
881
+ main_model.set_thinking_tokens(args.thinking_tokens)
882
+ if args.check_model_accepts_settings:
883
+ settings_to_check = [
884
+ {"arg": args.reasoning_effort, "name": "reasoning_effort"},
885
+ {"arg": args.thinking_tokens, "name": "thinking_tokens"},
886
+ ]
887
+ for setting in settings_to_check:
888
+ if setting["arg"] is not None and (
889
+ not main_model.accepts_settings
890
+ or setting["name"] not in main_model.accepts_settings
891
+ ):
892
+ io.tool_warning(
893
+ f"Warning: {main_model.name} does not support '{setting['name']}', ignoring."
894
+ )
895
+ io.tool_output(
896
+ f"Use --no-check-model-accepts-settings to force the '{setting['name']}'"
897
+ " setting."
898
+ )
899
+ if args.copy_paste and args.edit_format is None:
900
+ if main_model.edit_format in ("diff", "whole", "diff-fenced"):
901
+ main_model.edit_format = "editor-" + main_model.edit_format
902
+ if args.verbose:
903
+ io.tool_output("Model metadata:")
904
+ io.tool_output(json.dumps(main_model.info, indent=4))
905
+ io.tool_output("Model settings:")
906
+ for attr in sorted(fields(ModelSettings), key=lambda x: x.name):
907
+ val = getattr(main_model, attr.name)
908
+ val = json.dumps(val, indent=4)
909
+ io.tool_output(f"{attr.name}: {val}")
910
+ lint_cmds = parse_lint_cmds(args.lint_cmd, io)
911
+ if lint_cmds is None:
912
+ return await graceful_exit(None, 1)
913
+ repo = None
914
+ if args.git:
915
+ try:
916
+ repo = GitRepo(
917
+ io,
918
+ fnames,
919
+ git_dname,
920
+ args.cecli_ignore,
921
+ models=main_model.commit_message_models(),
922
+ attribute_author=args.attribute_author,
923
+ attribute_committer=args.attribute_committer,
924
+ attribute_commit_message_author=args.attribute_commit_message_author,
925
+ attribute_commit_message_committer=args.attribute_commit_message_committer,
926
+ commit_prompt=args.commit_prompt,
927
+ subtree_only=args.subtree_only,
928
+ git_commit_verify=args.git_commit_verify,
929
+ attribute_co_authored_by=args.attribute_co_authored_by,
930
+ )
931
+ except FileNotFoundError:
932
+ pass
933
+ if not args.skip_sanity_check_repo:
934
+ if not await sanity_check_repo(repo, io):
935
+ return await graceful_exit(None, 1)
936
+ commands = Commands(
937
+ io,
938
+ None,
939
+ voice_language=args.voice_language,
940
+ voice_input_device=args.voice_input_device,
941
+ voice_format=args.voice_format,
942
+ verify_ssl=args.verify_ssl,
943
+ args=args,
944
+ parser=parser,
945
+ verbose=args.verbose,
946
+ editor=args.editor,
947
+ original_read_only_fnames=read_only_fnames,
948
+ )
949
+ summarizer = ChatSummary(
950
+ [main_model.weak_model, main_model],
951
+ args.max_chat_history_tokens or main_model.max_chat_history_tokens,
952
+ )
953
+ if args.cache_prompts and args.map_refresh == "auto":
954
+ args.map_refresh = "files"
955
+ if not main_model.streaming:
956
+ if args.stream:
957
+ io.tool_warning(
958
+ f"Warning: Streaming is not supported by {main_model.name}. Disabling streaming."
959
+ " Set stream: false in config file or use --no-stream to skip this warning."
960
+ )
961
+ args.stream = False
962
+ if args.map_tokens is None:
963
+ map_tokens = main_model.get_repo_map_tokens()
964
+ else:
965
+ map_tokens = args.map_tokens
966
+ if args.enable_context_compaction and (
967
+ args.context_compaction_max_tokens is None or args.context_compaction_max_tokens < 1
968
+ ):
969
+ max_input_tokens = main_model.info.get("max_input_tokens")
970
+ ratio = 0.8
971
+ if args.context_compaction_max_tokens:
972
+ ratio = args.context_compaction_max_tokens
973
+ if max_input_tokens:
974
+ args.context_compaction_max_tokens = int(max_input_tokens * ratio)
975
+ try:
976
+ mcp_servers = load_mcp_servers(
977
+ args.mcp_servers, args.mcp_servers_file, io, args.verbose, args.mcp_transport
978
+ )
979
+ if not mcp_servers:
980
+ mcp_servers = []
981
+ coder = await Coder.create(
982
+ main_model=main_model,
983
+ edit_format=args.edit_format,
984
+ io=io,
985
+ args=args,
986
+ repo=repo,
987
+ fnames=fnames,
988
+ read_only_fnames=read_only_fnames,
989
+ read_only_stubs_fnames=[],
990
+ show_diffs=args.show_diffs,
991
+ auto_commits=args.auto_commits,
992
+ dirty_commits=args.dirty_commits,
993
+ dry_run=args.dry_run,
994
+ map_tokens=map_tokens,
995
+ verbose=args.verbose,
996
+ stream=args.stream,
997
+ use_git=args.git,
998
+ restore_chat_history=args.restore_chat_history,
999
+ auto_lint=args.auto_lint,
1000
+ auto_test=args.auto_test,
1001
+ lint_cmds=lint_cmds,
1002
+ test_cmd=args.test_cmd,
1003
+ commands=commands,
1004
+ summarizer=summarizer,
1005
+ map_refresh=args.map_refresh,
1006
+ cache_prompts=args.cache_prompts,
1007
+ map_mul_no_files=args.map_multiplier_no_files,
1008
+ map_max_line_length=args.map_max_line_length,
1009
+ num_cache_warming_pings=args.cache_keepalive_pings,
1010
+ suggest_shell_commands=args.suggest_shell_commands,
1011
+ chat_language=args.chat_language,
1012
+ commit_language=args.commit_language,
1013
+ detect_urls=args.detect_urls,
1014
+ auto_copy_context=args.copy_paste,
1015
+ auto_accept_architect=args.auto_accept_architect,
1016
+ mcp_servers=mcp_servers,
1017
+ add_gitignore_files=args.add_gitignore_files,
1018
+ enable_context_compaction=args.enable_context_compaction,
1019
+ context_compaction_max_tokens=args.context_compaction_max_tokens,
1020
+ context_compaction_summary_tokens=args.context_compaction_summary_tokens,
1021
+ map_cache_dir=args.map_cache_dir,
1022
+ repomap_in_memory=args.map_memory_cache,
1023
+ linear_output=args.linear_output,
1024
+ )
1025
+ if args.show_model_warnings:
1026
+ problem = await models.sanity_check_models(pre_init_io, main_model)
1027
+ if problem:
1028
+ pre_init_io.tool_output("You can skip this check with --no-show-model-warnings")
1029
+ try:
1030
+ await pre_init_io.offer_url(
1031
+ urls.model_warnings,
1032
+ "Open documentation url for more info?",
1033
+ acknowledge=True,
1034
+ )
1035
+ pre_init_io.tool_output()
1036
+ except KeyboardInterrupt:
1037
+ return await graceful_exit(coder, 1)
1038
+ if args.git:
1039
+ git_root = await setup_git(git_root, pre_init_io)
1040
+ if args.gitignore:
1041
+ await check_gitignore(git_root, pre_init_io)
1042
+ except UnknownEditFormat as err:
1043
+ pre_init_io.tool_error(str(err))
1044
+ await pre_init_io.offer_url(
1045
+ urls.edit_formats, "Open documentation about edit formats?", acknowledge=True
1046
+ )
1047
+ return await graceful_exit(None, 1)
1048
+ except ValueError as err:
1049
+ pre_init_io.tool_error(str(err))
1050
+ return await graceful_exit(None, 1)
1051
+ if return_coder:
1052
+ return coder
1053
+ ignores = []
1054
+ if git_root:
1055
+ ignores.append(str(Path(git_root) / ".gitignore"))
1056
+ if args.cecli_ignore:
1057
+ ignores.append(args.cecli_ignore)
1058
+ if args.watch_files:
1059
+ file_watcher = FileWatcher(
1060
+ coder,
1061
+ gitignores=ignores,
1062
+ verbose=args.verbose,
1063
+ root=str(Path.cwd()) if args.subtree_only else None,
1064
+ )
1065
+ coder.file_watcher = file_watcher
1066
+ if args.copy_paste:
1067
+ ClipboardWatcher(coder.io, verbose=args.verbose)
1068
+ if args.show_prompts:
1069
+ coder.cur_messages += [dict(role="user", content="Hello!")]
1070
+ messages = coder.format_messages().all_messages()
1071
+ utils.show_messages(messages)
1072
+ return await graceful_exit(coder)
1073
+ if args.lint:
1074
+ await coder.commands.do_run("lint", "")
1075
+ if args.test:
1076
+ if not args.test_cmd:
1077
+ io.tool_error("No --test-cmd provided.")
1078
+ return await graceful_exit(coder, 1)
1079
+ await coder.commands.do_run("test", args.test_cmd)
1080
+ if io.placeholder:
1081
+ await coder.run(io.placeholder)
1082
+ if args.commit:
1083
+ if args.dry_run:
1084
+ io.tool_output("Dry run enabled, skipping commit.")
1085
+ else:
1086
+ await coder.commands.do_run("commit", "")
1087
+ if args.lint or args.test or args.commit:
1088
+ return await graceful_exit(coder)
1089
+ if args.show_repo_map:
1090
+ repo_map = coder.get_repo_map()
1091
+ if repo_map:
1092
+ io.tool_output(repo_map)
1093
+ return await graceful_exit(coder)
1094
+ if args.apply:
1095
+ content = io.read_text(args.apply)
1096
+ if content is None:
1097
+ return await graceful_exit(coder)
1098
+ coder.partial_response_content = content
1099
+ await coder.apply_updates()
1100
+ return await graceful_exit(coder)
1101
+ if args.apply_clipboard_edits:
1102
+ args.edit_format = main_model.editor_edit_format
1103
+ args.message = "/paste"
1104
+ if args.show_release_notes is True:
1105
+ io.tool_output(f"Opening release notes: {urls.release_notes}")
1106
+ io.tool_output()
1107
+ webbrowser.open(urls.release_notes)
1108
+ elif args.show_release_notes is None and is_first_run:
1109
+ io.tool_output()
1110
+ await io.offer_url(
1111
+ urls.release_notes,
1112
+ "Would you like to see what's new in this version?",
1113
+ allow_never=False,
1114
+ acknowledge=True,
1115
+ )
1116
+ if git_root and Path.cwd().resolve() != Path(git_root).resolve():
1117
+ io.tool_warning(
1118
+ "Note: in-chat filenames are always relative to the git working dir, not the current"
1119
+ " working dir."
1120
+ )
1121
+ io.tool_output(f"Cur working dir: {Path.cwd()}")
1122
+ io.tool_output(f"Git working dir: {git_root}")
1123
+ if args.stream and args.cache_prompts:
1124
+ io.tool_warning("Cost estimates may be inaccurate when using streaming and caching.")
1125
+ if args.load:
1126
+ await commands.cmd_load(args.load)
1127
+ if args.message:
1128
+ io.add_to_input_history(args.message)
1129
+ io.tool_output()
1130
+ try:
1131
+ await coder.run(with_message=args.message)
1132
+ except (SwitchCoderSignal, KeyboardInterrupt, SystemExit):
1133
+ pass
1134
+ return await graceful_exit(coder)
1135
+ if args.message_file:
1136
+ try:
1137
+ message_from_file = io.read_text(args.message_file)
1138
+ io.tool_output()
1139
+ await coder.run(with_message=message_from_file)
1140
+ except (SwitchCoderSignal, KeyboardInterrupt, SystemExit):
1141
+ pass
1142
+ except FileNotFoundError:
1143
+ io.tool_error(f"Message file not found: {args.message_file}")
1144
+ return await graceful_exit(coder, 1)
1145
+ except IOError as e:
1146
+ io.tool_error(f"Error reading message file: {e}")
1147
+ return await graceful_exit(coder, 1)
1148
+ return await graceful_exit(coder)
1149
+ if args.exit:
1150
+ return await graceful_exit(coder)
1151
+ if args.auto_load:
1152
+ try:
1153
+ from cecli.sessions import SessionManager
1154
+
1155
+ session_manager = SessionManager(coder, io)
1156
+ session_manager.load_session(
1157
+ args.auto_save_session_name if args.auto_save_session_name else "auto-save"
1158
+ )
1159
+ except Exception:
1160
+ pass
1161
+ if args.tui:
1162
+ from cecli.tui import launch_tui
1163
+
1164
+ del pre_init_io
1165
+ return_code = await launch_tui(coder, output_queue, input_queue, args)
1166
+ return await graceful_exit(coder, return_code)
1167
+ while True:
1168
+ try:
1169
+ coder.ok_to_warm_cache = bool(args.cache_keepalive_pings)
1170
+ await coder.run()
1171
+ return await graceful_exit(coder)
1172
+ except SwitchCoderSignal as switch:
1173
+ coder.ok_to_warm_cache = False
1174
+ if hasattr(switch, "placeholder") and switch.placeholder is not None:
1175
+ io.placeholder = switch.placeholder
1176
+ kwargs = dict(io=io, from_coder=coder)
1177
+ kwargs.update(switch.kwargs)
1178
+ if "show_announcements" in kwargs:
1179
+ del kwargs["show_announcements"]
1180
+ kwargs["num_cache_warming_pings"] = 0
1181
+ kwargs["args"] = coder.args
1182
+ coder = await Coder.create(**kwargs)
1183
+ if switch.kwargs.get("show_announcements") is False:
1184
+ coder.suppress_announcements_for_next_prompt = True
1185
+ except SystemExit:
1186
+ sys.settrace(None)
1187
+ return await graceful_exit(coder)
1188
+
1189
+
1190
+ def is_first_run_of_new_version(io, verbose=False):
1191
+ """Check if this is the first run of a new version/executable combination"""
1192
+ installs_file = handle_core_files(Path.home() / ".cecli" / "installs.json")
1193
+ key = __version__, sys.executable
1194
+ if ".dev" in __version__:
1195
+ return False
1196
+ if verbose:
1197
+ io.tool_output(
1198
+ f"Checking imports for version {__version__} and executable {sys.executable}"
1199
+ )
1200
+ io.tool_output(f"Installs file: {installs_file}")
1201
+ try:
1202
+ if installs_file.exists():
1203
+ with open(installs_file, "r") as f:
1204
+ installs = json.load(f)
1205
+ if verbose:
1206
+ io.tool_output("Installs file exists and loaded")
1207
+ else:
1208
+ installs = {}
1209
+ if verbose:
1210
+ io.tool_output("Installs file does not exist, creating new dictionary")
1211
+ is_first_run = str(key) not in installs
1212
+ if is_first_run:
1213
+ installs[str(key)] = True
1214
+ installs_file.parent.mkdir(parents=True, exist_ok=True)
1215
+ with open(installs_file, "w") as f:
1216
+ json.dump(installs, f, indent=4)
1217
+ return is_first_run
1218
+ except Exception as e:
1219
+ io.tool_warning(f"Error checking version: {e}")
1220
+ if verbose:
1221
+ io.tool_output(f"Full exception details: {traceback.format_exc()}")
1222
+ return True
1223
+
1224
+
1225
+ async def check_and_load_imports(io, is_first_run, verbose=False):
1226
+ try:
1227
+ if is_first_run:
1228
+ if verbose:
1229
+ io.tool_output(
1230
+ "First run for this version and executable, loading imports synchronously"
1231
+ )
1232
+ try:
1233
+ load_slow_imports(swallow=False)
1234
+ except Exception as err:
1235
+ io.tool_error(str(err))
1236
+ io.tool_output("Error loading required imports. Did you install cecli properly?")
1237
+ await io.offer_url(
1238
+ urls.install_properly, "Open documentation url for more info?", acknowledge=True
1239
+ )
1240
+ sys.exit(1)
1241
+ if verbose:
1242
+ io.tool_output("Imports loaded and installs file updated")
1243
+ else:
1244
+ if verbose:
1245
+ io.tool_output("Not first run, loading imports in background thread")
1246
+ thread = threading.Thread(target=load_slow_imports)
1247
+ thread.daemon = True
1248
+ thread.start()
1249
+ except Exception as e:
1250
+ io.tool_warning(f"Error in loading imports: {e}")
1251
+ if verbose:
1252
+ io.tool_output(f"Full exception details: {traceback.format_exc()}")
1253
+
1254
+
1255
+ def load_slow_imports(swallow=True):
1256
+ try:
1257
+ import httpx # noqa
1258
+ import litellm # noqa
1259
+ import numpy # noqa
1260
+ except Exception as e:
1261
+ if not swallow:
1262
+ raise e
1263
+
1264
+
1265
+ async def graceful_exit(coder=None, exit_code=0):
1266
+ sys.settrace(None)
1267
+ if coder:
1268
+ if hasattr(coder, "_autosave_future"):
1269
+ await coder._autosave_future
1270
+ for server in coder.mcp_servers:
1271
+ try:
1272
+ await server.exit_stack.aclose()
1273
+ except Exception:
1274
+ pass
1275
+ return exit_code
1276
+
1277
+
1278
+ if __name__ == "__main__":
1279
+ status = main()
1280
+ sys.exit(status)