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.
- cecli/__init__.py +20 -0
- cecli/__main__.py +4 -0
- cecli/_version.py +34 -0
- cecli/args.py +1092 -0
- cecli/args_formatter.py +228 -0
- cecli/change_tracker.py +133 -0
- cecli/coders/__init__.py +38 -0
- cecli/coders/agent_coder.py +1872 -0
- cecli/coders/architect_coder.py +63 -0
- cecli/coders/ask_coder.py +8 -0
- cecli/coders/base_coder.py +3993 -0
- cecli/coders/chat_chunks.py +116 -0
- cecli/coders/context_coder.py +52 -0
- cecli/coders/copypaste_coder.py +269 -0
- cecli/coders/editblock_coder.py +656 -0
- cecli/coders/editblock_fenced_coder.py +9 -0
- cecli/coders/editblock_func_coder.py +140 -0
- cecli/coders/editor_diff_fenced_coder.py +8 -0
- cecli/coders/editor_editblock_coder.py +8 -0
- cecli/coders/editor_whole_coder.py +8 -0
- cecli/coders/help_coder.py +15 -0
- cecli/coders/patch_coder.py +705 -0
- cecli/coders/search_replace.py +757 -0
- cecli/coders/shell.py +37 -0
- cecli/coders/single_wholefile_func_coder.py +101 -0
- cecli/coders/udiff_coder.py +428 -0
- cecli/coders/udiff_simple.py +12 -0
- cecli/coders/wholefile_coder.py +143 -0
- cecli/coders/wholefile_func_coder.py +133 -0
- cecli/commands/__init__.py +192 -0
- cecli/commands/add.py +226 -0
- cecli/commands/agent.py +51 -0
- cecli/commands/architect.py +46 -0
- cecli/commands/ask.py +44 -0
- cecli/commands/chat_mode.py +0 -0
- cecli/commands/clear.py +37 -0
- cecli/commands/code.py +46 -0
- cecli/commands/command_prefix.py +44 -0
- cecli/commands/commit.py +52 -0
- cecli/commands/context.py +47 -0
- cecli/commands/context_blocks.py +124 -0
- cecli/commands/context_management.py +51 -0
- cecli/commands/copy.py +62 -0
- cecli/commands/copy_context.py +81 -0
- cecli/commands/core.py +287 -0
- cecli/commands/diff.py +68 -0
- cecli/commands/drop.py +217 -0
- cecli/commands/editor.py +78 -0
- cecli/commands/exit.py +55 -0
- cecli/commands/git.py +57 -0
- cecli/commands/help.py +140 -0
- cecli/commands/history_search.py +40 -0
- cecli/commands/lint.py +109 -0
- cecli/commands/list_sessions.py +56 -0
- cecli/commands/load.py +85 -0
- cecli/commands/load_session.py +48 -0
- cecli/commands/load_skill.py +68 -0
- cecli/commands/ls.py +75 -0
- cecli/commands/map.py +37 -0
- cecli/commands/map_refresh.py +35 -0
- cecli/commands/model.py +118 -0
- cecli/commands/models.py +41 -0
- cecli/commands/multiline_mode.py +38 -0
- cecli/commands/paste.py +91 -0
- cecli/commands/quit.py +32 -0
- cecli/commands/read_only.py +267 -0
- cecli/commands/read_only_stub.py +270 -0
- cecli/commands/reasoning_effort.py +70 -0
- cecli/commands/remove_skill.py +68 -0
- cecli/commands/report.py +40 -0
- cecli/commands/reset.py +88 -0
- cecli/commands/run.py +99 -0
- cecli/commands/save.py +49 -0
- cecli/commands/save_session.py +43 -0
- cecli/commands/settings.py +69 -0
- cecli/commands/test.py +58 -0
- cecli/commands/think_tokens.py +74 -0
- cecli/commands/tokens.py +207 -0
- cecli/commands/undo.py +145 -0
- cecli/commands/utils/__init__.py +0 -0
- cecli/commands/utils/base_command.py +131 -0
- cecli/commands/utils/helpers.py +142 -0
- cecli/commands/utils/registry.py +53 -0
- cecli/commands/utils/save_load_manager.py +98 -0
- cecli/commands/voice.py +78 -0
- cecli/commands/weak_model.py +123 -0
- cecli/commands/web.py +87 -0
- cecli/deprecated_args.py +185 -0
- cecli/diffs.py +129 -0
- cecli/dump.py +29 -0
- cecli/editor.py +147 -0
- cecli/exceptions.py +115 -0
- cecli/format_settings.py +26 -0
- cecli/help.py +119 -0
- cecli/help_pats.py +19 -0
- cecli/helpers/__init__.py +9 -0
- cecli/helpers/copypaste.py +123 -0
- cecli/helpers/coroutines.py +8 -0
- cecli/helpers/file_searcher.py +142 -0
- cecli/helpers/model_providers.py +552 -0
- cecli/helpers/plugin_manager.py +81 -0
- cecli/helpers/profiler.py +162 -0
- cecli/helpers/requests.py +77 -0
- cecli/helpers/similarity.py +98 -0
- cecli/helpers/skills.py +577 -0
- cecli/history.py +186 -0
- cecli/io.py +1782 -0
- cecli/linter.py +304 -0
- cecli/llm.py +101 -0
- cecli/main.py +1280 -0
- cecli/mcp/__init__.py +154 -0
- cecli/mcp/oauth.py +250 -0
- cecli/mcp/server.py +278 -0
- cecli/mdstream.py +243 -0
- cecli/models.py +1255 -0
- cecli/onboarding.py +301 -0
- cecli/prompts/__init__.py +0 -0
- cecli/prompts/agent.yml +71 -0
- cecli/prompts/architect.yml +35 -0
- cecli/prompts/ask.yml +31 -0
- cecli/prompts/base.yml +99 -0
- cecli/prompts/context.yml +60 -0
- cecli/prompts/copypaste.yml +5 -0
- cecli/prompts/editblock.yml +143 -0
- cecli/prompts/editblock_fenced.yml +106 -0
- cecli/prompts/editblock_func.yml +25 -0
- cecli/prompts/editor_diff_fenced.yml +115 -0
- cecli/prompts/editor_editblock.yml +121 -0
- cecli/prompts/editor_whole.yml +46 -0
- cecli/prompts/help.yml +37 -0
- cecli/prompts/patch.yml +110 -0
- cecli/prompts/single_wholefile_func.yml +24 -0
- cecli/prompts/udiff.yml +106 -0
- cecli/prompts/udiff_simple.yml +13 -0
- cecli/prompts/utils/__init__.py +0 -0
- cecli/prompts/utils/prompt_registry.py +167 -0
- cecli/prompts/utils/system.py +56 -0
- cecli/prompts/wholefile.yml +50 -0
- cecli/prompts/wholefile_func.yml +24 -0
- cecli/queries/tree-sitter-language-pack/README.md +7 -0
- cecli/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/c-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- cecli/queries/tree-sitter-language-pack/clojure-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/commonlisp-tags.scm +127 -0
- cecli/queries/tree-sitter-language-pack/cpp-tags.scm +18 -0
- cecli/queries/tree-sitter-language-pack/csharp-tags.scm +32 -0
- cecli/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- cecli/queries/tree-sitter-language-pack/dart-tags.scm +97 -0
- cecli/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/elixir-tags.scm +59 -0
- cecli/queries/tree-sitter-language-pack/elm-tags.scm +22 -0
- cecli/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- cecli/queries/tree-sitter-language-pack/go-tags.scm +49 -0
- cecli/queries/tree-sitter-language-pack/java-tags.scm +26 -0
- cecli/queries/tree-sitter-language-pack/javascript-tags.scm +96 -0
- cecli/queries/tree-sitter-language-pack/lua-tags.scm +39 -0
- cecli/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- cecli/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- cecli/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +101 -0
- cecli/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- cecli/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/python-tags.scm +24 -0
- cecli/queries/tree-sitter-language-pack/r-tags.scm +27 -0
- cecli/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/ruby-tags.scm +69 -0
- cecli/queries/tree-sitter-language-pack/rust-tags.scm +63 -0
- cecli/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- cecli/queries/tree-sitter-language-pack/swift-tags.scm +54 -0
- cecli/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- cecli/queries/tree-sitter-languages/README.md +24 -0
- cecli/queries/tree-sitter-languages/c-tags.scm +12 -0
- cecli/queries/tree-sitter-languages/c_sharp-tags.scm +52 -0
- cecli/queries/tree-sitter-languages/cpp-tags.scm +18 -0
- cecli/queries/tree-sitter-languages/dart-tags.scm +92 -0
- cecli/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- cecli/queries/tree-sitter-languages/elixir-tags.scm +59 -0
- cecli/queries/tree-sitter-languages/elm-tags.scm +22 -0
- cecli/queries/tree-sitter-languages/fortran-tags.scm +18 -0
- cecli/queries/tree-sitter-languages/go-tags.scm +36 -0
- cecli/queries/tree-sitter-languages/haskell-tags.scm +5 -0
- cecli/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- cecli/queries/tree-sitter-languages/java-tags.scm +26 -0
- cecli/queries/tree-sitter-languages/javascript-tags.scm +96 -0
- cecli/queries/tree-sitter-languages/julia-tags.scm +60 -0
- cecli/queries/tree-sitter-languages/kotlin-tags.scm +30 -0
- cecli/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- cecli/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- cecli/queries/tree-sitter-languages/ocaml_interface-tags.scm +104 -0
- cecli/queries/tree-sitter-languages/php-tags.scm +32 -0
- cecli/queries/tree-sitter-languages/python-tags.scm +22 -0
- cecli/queries/tree-sitter-languages/ql-tags.scm +26 -0
- cecli/queries/tree-sitter-languages/ruby-tags.scm +69 -0
- cecli/queries/tree-sitter-languages/rust-tags.scm +63 -0
- cecli/queries/tree-sitter-languages/scala-tags.scm +64 -0
- cecli/queries/tree-sitter-languages/typescript-tags.scm +44 -0
- cecli/queries/tree-sitter-languages/zig-tags.scm +20 -0
- cecli/reasoning_tags.py +82 -0
- cecli/repo.py +626 -0
- cecli/repomap.py +1368 -0
- cecli/report.py +260 -0
- cecli/resources/__init__.py +3 -0
- cecli/resources/model-metadata.json +25751 -0
- cecli/resources/model-settings.yml +2394 -0
- cecli/resources/providers.json +67 -0
- cecli/run_cmd.py +143 -0
- cecli/scrape.py +295 -0
- cecli/sendchat.py +250 -0
- cecli/sessions.py +281 -0
- cecli/special.py +203 -0
- cecli/tools/__init__.py +72 -0
- cecli/tools/command.py +103 -0
- cecli/tools/command_interactive.py +113 -0
- cecli/tools/context_manager.py +175 -0
- cecli/tools/delete_block.py +154 -0
- cecli/tools/delete_line.py +120 -0
- cecli/tools/delete_lines.py +144 -0
- cecli/tools/extract_lines.py +281 -0
- cecli/tools/finished.py +35 -0
- cecli/tools/git_branch.py +132 -0
- cecli/tools/git_diff.py +49 -0
- cecli/tools/git_log.py +43 -0
- cecli/tools/git_remote.py +39 -0
- cecli/tools/git_show.py +37 -0
- cecli/tools/git_status.py +32 -0
- cecli/tools/grep.py +242 -0
- cecli/tools/indent_lines.py +195 -0
- cecli/tools/insert_block.py +263 -0
- cecli/tools/list_changes.py +71 -0
- cecli/tools/load_skill.py +51 -0
- cecli/tools/ls.py +77 -0
- cecli/tools/remove_skill.py +51 -0
- cecli/tools/replace_all.py +113 -0
- cecli/tools/replace_line.py +135 -0
- cecli/tools/replace_lines.py +180 -0
- cecli/tools/replace_text.py +186 -0
- cecli/tools/show_numbered_context.py +137 -0
- cecli/tools/thinking.py +52 -0
- cecli/tools/undo_change.py +82 -0
- cecli/tools/update_todo_list.py +148 -0
- cecli/tools/utils/base_tool.py +64 -0
- cecli/tools/utils/helpers.py +359 -0
- cecli/tools/utils/output.py +119 -0
- cecli/tools/utils/registry.py +145 -0
- cecli/tools/view_files_matching.py +138 -0
- cecli/tools/view_files_with_symbol.py +117 -0
- cecli/tui/__init__.py +83 -0
- cecli/tui/app.py +971 -0
- cecli/tui/io.py +566 -0
- cecli/tui/styles.tcss +117 -0
- cecli/tui/widgets/__init__.py +19 -0
- cecli/tui/widgets/completion_bar.py +331 -0
- cecli/tui/widgets/file_list.py +76 -0
- cecli/tui/widgets/footer.py +165 -0
- cecli/tui/widgets/input_area.py +320 -0
- cecli/tui/widgets/key_hints.py +16 -0
- cecli/tui/widgets/output.py +354 -0
- cecli/tui/widgets/status_bar.py +279 -0
- cecli/tui/worker.py +160 -0
- cecli/urls.py +16 -0
- cecli/utils.py +499 -0
- cecli/versioncheck.py +90 -0
- cecli/voice.py +90 -0
- cecli/waiting.py +38 -0
- cecli/watch.py +316 -0
- cecli/watch_prompts.py +12 -0
- cecli/website/Gemfile +8 -0
- cecli/website/_includes/blame.md +162 -0
- cecli/website/_includes/get-started.md +22 -0
- cecli/website/_includes/help-tip.md +5 -0
- cecli/website/_includes/help.md +24 -0
- cecli/website/_includes/install.md +5 -0
- cecli/website/_includes/keys.md +4 -0
- cecli/website/_includes/model-warnings.md +67 -0
- cecli/website/_includes/multi-line.md +22 -0
- cecli/website/_includes/python-m-aider.md +5 -0
- cecli/website/_includes/recording.css +228 -0
- cecli/website/_includes/recording.md +34 -0
- cecli/website/_includes/replit-pipx.md +9 -0
- cecli/website/_includes/works-best.md +1 -0
- cecli/website/_sass/custom/custom.scss +103 -0
- cecli/website/docs/config/adv-model-settings.md +2498 -0
- cecli/website/docs/config/agent-mode.md +320 -0
- cecli/website/docs/config/aider_conf.md +548 -0
- cecli/website/docs/config/api-keys.md +90 -0
- cecli/website/docs/config/custom-commands.md +187 -0
- cecli/website/docs/config/dotenv.md +493 -0
- cecli/website/docs/config/editor.md +127 -0
- cecli/website/docs/config/mcp.md +210 -0
- cecli/website/docs/config/model-aliases.md +173 -0
- cecli/website/docs/config/options.md +890 -0
- cecli/website/docs/config/reasoning.md +210 -0
- cecli/website/docs/config/skills.md +172 -0
- cecli/website/docs/config/tui.md +126 -0
- cecli/website/docs/config.md +44 -0
- cecli/website/docs/faq.md +379 -0
- cecli/website/docs/git.md +76 -0
- cecli/website/docs/index.md +47 -0
- cecli/website/docs/install/codespaces.md +39 -0
- cecli/website/docs/install/docker.md +48 -0
- cecli/website/docs/install/optional.md +100 -0
- cecli/website/docs/install/replit.md +8 -0
- cecli/website/docs/install.md +115 -0
- cecli/website/docs/languages.md +264 -0
- cecli/website/docs/legal/contributor-agreement.md +111 -0
- cecli/website/docs/legal/privacy.md +104 -0
- cecli/website/docs/llms/anthropic.md +77 -0
- cecli/website/docs/llms/azure.md +48 -0
- cecli/website/docs/llms/bedrock.md +132 -0
- cecli/website/docs/llms/cohere.md +34 -0
- cecli/website/docs/llms/deepseek.md +32 -0
- cecli/website/docs/llms/gemini.md +49 -0
- cecli/website/docs/llms/github.md +111 -0
- cecli/website/docs/llms/groq.md +36 -0
- cecli/website/docs/llms/lm-studio.md +39 -0
- cecli/website/docs/llms/ollama.md +75 -0
- cecli/website/docs/llms/openai-compat.md +39 -0
- cecli/website/docs/llms/openai.md +58 -0
- cecli/website/docs/llms/openrouter.md +78 -0
- cecli/website/docs/llms/other.md +117 -0
- cecli/website/docs/llms/vertex.md +50 -0
- cecli/website/docs/llms/warnings.md +10 -0
- cecli/website/docs/llms/xai.md +53 -0
- cecli/website/docs/llms.md +54 -0
- cecli/website/docs/more/analytics.md +127 -0
- cecli/website/docs/more/edit-formats.md +116 -0
- cecli/website/docs/more/infinite-output.md +192 -0
- cecli/website/docs/more-info.md +8 -0
- cecli/website/docs/recordings/auto-accept-architect.md +31 -0
- cecli/website/docs/recordings/dont-drop-original-read-files.md +35 -0
- cecli/website/docs/recordings/index.md +21 -0
- cecli/website/docs/recordings/model-accepts-settings.md +69 -0
- cecli/website/docs/recordings/tree-sitter-language-pack.md +80 -0
- cecli/website/docs/repomap.md +112 -0
- cecli/website/docs/scripting.md +100 -0
- cecli/website/docs/sessions.md +213 -0
- cecli/website/docs/troubleshooting/aider-not-found.md +24 -0
- cecli/website/docs/troubleshooting/edit-errors.md +76 -0
- cecli/website/docs/troubleshooting/imports.md +62 -0
- cecli/website/docs/troubleshooting/models-and-keys.md +54 -0
- cecli/website/docs/troubleshooting/support.md +79 -0
- cecli/website/docs/troubleshooting/token-limits.md +96 -0
- cecli/website/docs/troubleshooting/warnings.md +12 -0
- cecli/website/docs/troubleshooting.md +11 -0
- cecli/website/docs/usage/browser.md +57 -0
- cecli/website/docs/usage/caching.md +49 -0
- cecli/website/docs/usage/commands.md +133 -0
- cecli/website/docs/usage/conventions.md +119 -0
- cecli/website/docs/usage/copypaste.md +136 -0
- cecli/website/docs/usage/images-urls.md +48 -0
- cecli/website/docs/usage/lint-test.md +118 -0
- cecli/website/docs/usage/modes.md +211 -0
- cecli/website/docs/usage/not-code.md +179 -0
- cecli/website/docs/usage/notifications.md +87 -0
- cecli/website/docs/usage/tips.md +79 -0
- cecli/website/docs/usage/tutorials.md +30 -0
- cecli/website/docs/usage/voice.md +121 -0
- cecli/website/docs/usage/watch.md +294 -0
- cecli/website/docs/usage.md +102 -0
- cecli/website/share/index.md +101 -0
- cecli_dev-0.95.5.dist-info/METADATA +549 -0
- cecli_dev-0.95.5.dist-info/RECORD +366 -0
- cecli_dev-0.95.5.dist-info/WHEEL +5 -0
- cecli_dev-0.95.5.dist-info/entry_points.txt +4 -0
- cecli_dev-0.95.5.dist-info/licenses/LICENSE.txt +202 -0
- 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)
|