micro-cc 0.2.32__tar.gz → 0.2.35__tar.gz
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.
- {micro_cc-0.2.32 → micro_cc-0.2.35}/PKG-INFO +10 -11
- {micro_cc-0.2.32 → micro_cc-0.2.35}/README.md +9 -10
- {micro_cc-0.2.32 → micro_cc-0.2.35}/pyproject.toml +1 -1
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/models/anthropic.py +8 -15
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/models/litellm.py +7 -19
- micro_cc-0.2.35/src/micro_cc/models/registry.py +67 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/screens/window_overlay_.py +11 -10
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/start_live_.py +35 -10
- micro_cc-0.2.35/src/micro_cc/tools/bash_tool.py +255 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/msg_store_.py +2 -2
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc.egg-info/PKG-INFO +10 -11
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc.egg-info/SOURCES.txt +1 -0
- micro_cc-0.2.32/src/micro_cc/tools/bash_tool.py +0 -92
- {micro_cc-0.2.32 → micro_cc-0.2.35}/setup.cfg +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/__init__.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/browser/__init__.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/browser/_cookies.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/browser/_md_convert.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/browser/browser_manager.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/browser/simpletextbrowser.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/cache/__init__.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/cache/redis_cache.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/claude_loop_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/code_test/__init__.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/code_test/run_token_cutter.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/code_test/test_messages.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/execute_tool.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/models/__init__.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/models/ollama.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/models/schema.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/screens/__init__.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/screens/md_render_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/screens/microcc-styles.tcss +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/SKILL.md +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/DMMono-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Gloock-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Italiana-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Jura-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/github/SKILL.md +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/SKILL.md +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/editing.md +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/pptxgenjs.md +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/__init__.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/add_slide.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/clean.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/helpers/__init__.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/helpers/merge_runs.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/helpers/simplify_redlines.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/pack.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/soffice.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/unpack.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validate.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validators/__init__.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validators/base.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validators/docx.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validators/pptx.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validators/redlining.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/thumbnail.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/skill_loader.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/xlsx/SKILL.md +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/__init__.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/browser_tool_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/computer_tool_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/file_tools_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/mcp_client_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/memory_tool_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/search_tool_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/skill_tools_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/todo_tools_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/vision_tools_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/web_tools_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/__init__.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/annotate_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/claude_md_loader.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/file_watcher_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/helpers.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/memory_store_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/process_tracker.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/tokenization_simple.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/web_.py +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc.egg-info/dependency_links.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc.egg-info/entry_points.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc.egg-info/requires.txt +0 -0
- {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: micro-cc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.35
|
|
4
4
|
Summary: Harness that gives frontier models full system access — shell, filesystem, browser, MCP — running directly on the metal.
|
|
5
5
|
Author: George Salapa
|
|
6
6
|
License-Expression: MIT
|
|
@@ -44,18 +44,19 @@ Requires-Dist: youtube-transcript-api>=1.2
|
|
|
44
44
|
# micro · cc — cognitive compute
|
|
45
45
|
|
|
46
46
|
```
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
88
|
|
48
|
+
""
|
|
49
|
+
|
|
50
|
+
88,dPYba,,adPYba, 88 ,adPPYba, 8b,dPPYba, ,adPPYba, ,adPPYba, ,adPPYba,
|
|
51
|
+
88P' "88" "8a 88 a8" "" 88P' "Y8 a8" "8a a8" "" a8" ""
|
|
52
|
+
88 88 88 88 8b 88 8b d8 · 8b 8b
|
|
53
|
+
88 88 88 88 "8a, ,aa 88 "8a, ,a8" "8a, ,aa "8a, ,aa
|
|
54
|
+
88 88 88 88 `"Ybbd8"' 88 `"YbbdP"' `"Ybbd8"' `"Ybbd8"'
|
|
54
55
|
```
|
|
55
56
|
|
|
56
57
|
> Knowledge work is, by extension, a coding problem.
|
|
57
58
|
|
|
58
|
-
Frontier models with full system access — shell, filesystem, browser, MCP
|
|
59
|
+
Frontier models with full system access — shell, filesystem, browser, MCP. Verbose, thorough planning paired with unrestricted file operations lets micro·cc take on complex multi-step work autonomously. Point it at any project directory and go.
|
|
59
60
|
|
|
60
61
|
## Install
|
|
61
62
|
|
|
@@ -65,8 +66,6 @@ pip install micro-cc
|
|
|
65
66
|
|
|
66
67
|
On first launch, micro·cc creates `~/.micro-cc/` with an example `.env` — add your API keys there and restart. That directory is also where conversation history and project state live.
|
|
67
68
|
|
|
68
|
-
Recommended terminal: **iTerm2** (macOS).
|
|
69
|
-
|
|
70
69
|
## Platform
|
|
71
70
|
|
|
72
71
|
Built and tested on macOS. Works on Linux. Windows should work via `pip install` + any modern terminal — iTerm2 is macOS-only but micro·cc doesn't depend on it, it just looks best there.
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# micro · cc — cognitive compute
|
|
2
2
|
|
|
3
3
|
```
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
88
|
|
5
|
+
""
|
|
6
|
+
|
|
7
|
+
88,dPYba,,adPYba, 88 ,adPPYba, 8b,dPPYba, ,adPPYba, ,adPPYba, ,adPPYba,
|
|
8
|
+
88P' "88" "8a 88 a8" "" 88P' "Y8 a8" "8a a8" "" a8" ""
|
|
9
|
+
88 88 88 88 8b 88 8b d8 · 8b 8b
|
|
10
|
+
88 88 88 88 "8a, ,aa 88 "8a, ,a8" "8a, ,aa "8a, ,aa
|
|
11
|
+
88 88 88 88 `"Ybbd8"' 88 `"YbbdP"' `"Ybbd8"' `"Ybbd8"'
|
|
11
12
|
```
|
|
12
13
|
|
|
13
14
|
> Knowledge work is, by extension, a coding problem.
|
|
14
15
|
|
|
15
|
-
Frontier models with full system access — shell, filesystem, browser, MCP
|
|
16
|
+
Frontier models with full system access — shell, filesystem, browser, MCP. Verbose, thorough planning paired with unrestricted file operations lets micro·cc take on complex multi-step work autonomously. Point it at any project directory and go.
|
|
16
17
|
|
|
17
18
|
## Install
|
|
18
19
|
|
|
@@ -22,8 +23,6 @@ pip install micro-cc
|
|
|
22
23
|
|
|
23
24
|
On first launch, micro·cc creates `~/.micro-cc/` with an example `.env` — add your API keys there and restart. That directory is also where conversation history and project state live.
|
|
24
25
|
|
|
25
|
-
Recommended terminal: **iTerm2** (macOS).
|
|
26
|
-
|
|
27
26
|
## Platform
|
|
28
27
|
|
|
29
28
|
Built and tested on macOS. Works on Linux. Windows should work via `pip install` + any modern terminal — iTerm2 is macOS-only but micro·cc doesn't depend on it, it just looks best there.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "micro-cc"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.35"
|
|
8
8
|
description = "Harness that gives frontier models full system access — shell, filesystem, browser, MCP — running directly on the metal."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -7,6 +7,9 @@ import json
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
|
+
|
|
11
|
+
from micro_cc.models.registry import resolve, supports_thinking, wants_summarized_display
|
|
12
|
+
|
|
10
13
|
load_dotenv(os.path.expanduser("~/.micro-cc/.env"))
|
|
11
14
|
load_dotenv()
|
|
12
15
|
|
|
@@ -82,7 +85,7 @@ async def _wrap_stream(raw_stream):
|
|
|
82
85
|
|
|
83
86
|
async def a_model_call(
|
|
84
87
|
input: Union[List[Dict[str, Any]], str],
|
|
85
|
-
model="
|
|
88
|
+
model="sonnet-4.6",
|
|
86
89
|
encoded_image: Optional[Union[str, List[str]]] = None,
|
|
87
90
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
88
91
|
mcp_servers: list = None,
|
|
@@ -96,18 +99,8 @@ async def a_model_call(
|
|
|
96
99
|
client = AsyncAnthropic(timeout=client_timeout)
|
|
97
100
|
base_sleep = 2
|
|
98
101
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"opus-4.8": "claude-opus-4-8",
|
|
102
|
-
"opus-4.7": "claude-opus-4-7",
|
|
103
|
-
"opus-4.6": "claude-opus-4-6",
|
|
104
|
-
"sonnet-4.6": "claude-sonnet-4-6",
|
|
105
|
-
"haiku-4.5": "claude-haiku-4-5-20251001",
|
|
106
|
-
# legacy aliases
|
|
107
|
-
"claude-4.6": "claude-sonnet-4-6",
|
|
108
|
-
"claude-4.5-haiku": "claude-haiku-4-5-20251001",
|
|
109
|
-
}
|
|
110
|
-
model = MODEL_MAP.get(model, "claude-sonnet-4-6")
|
|
102
|
+
alias = model
|
|
103
|
+
model = resolve(alias, "anthropic")
|
|
111
104
|
|
|
112
105
|
system_prompts = []
|
|
113
106
|
messages = []
|
|
@@ -231,9 +224,9 @@ async def a_model_call(
|
|
|
231
224
|
|
|
232
225
|
api_parameters["max_tokens"] = max_tokens
|
|
233
226
|
|
|
234
|
-
if thinking and
|
|
227
|
+
if thinking and supports_thinking(alias):
|
|
235
228
|
api_parameters["thinking"] = {"type": "adaptive"}
|
|
236
|
-
if
|
|
229
|
+
if wants_summarized_display(alias):
|
|
237
230
|
api_parameters["thinking"]["display"] = "summarized"
|
|
238
231
|
api_parameters["output_config"] = {"effort": "high"}
|
|
239
232
|
|
|
@@ -17,6 +17,8 @@ import asyncio
|
|
|
17
17
|
import json
|
|
18
18
|
import os
|
|
19
19
|
|
|
20
|
+
from micro_cc.models.registry import resolve, supports_thinking, wants_summarized_display
|
|
21
|
+
|
|
20
22
|
load_dotenv(os.path.expanduser("~/.micro-cc/.env"))
|
|
21
23
|
load_dotenv()
|
|
22
24
|
|
|
@@ -295,7 +297,7 @@ async def _wrap_stream(openai_stream):
|
|
|
295
297
|
|
|
296
298
|
async def l_model_call(
|
|
297
299
|
input: Union[List[Dict[str, Any]], str],
|
|
298
|
-
model="
|
|
300
|
+
model="sonnet-4.6",
|
|
299
301
|
encoded_image: Optional[Union[str, List[str]]] = None,
|
|
300
302
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
301
303
|
mcp_openai_tools: list = None,
|
|
@@ -315,18 +317,8 @@ async def l_model_call(
|
|
|
315
317
|
)
|
|
316
318
|
base_sleep = 2
|
|
317
319
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
"opus-4.8": "bedrock.anthropic.claude-opus-4-8",
|
|
321
|
-
"opus-4.7": "bedrock.anthropic.claude-opus-4-7",
|
|
322
|
-
"opus-4.6": "bedrock.anthropic.claude-opus-4-6",
|
|
323
|
-
"sonnet-4.6": "bedrock.anthropic.claude-sonnet-4-6",
|
|
324
|
-
"haiku-4.5": "bedrock.anthropic.claude-haiku-4-5",
|
|
325
|
-
# legacy aliases
|
|
326
|
-
"claude-4.6": "bedrock.anthropic.claude-sonnet-4-6",
|
|
327
|
-
"claude-4.5-haiku": "bedrock.anthropic.claude-haiku-4-5",
|
|
328
|
-
}
|
|
329
|
-
model = MODEL_MAP.get(model, "bedrock.anthropic.claude-sonnet-4-6")
|
|
320
|
+
alias = model
|
|
321
|
+
model = resolve(alias, "litellm")
|
|
330
322
|
|
|
331
323
|
# ---- Build messages ----
|
|
332
324
|
messages = []
|
|
@@ -398,16 +390,12 @@ async def l_model_call(
|
|
|
398
390
|
if stream:
|
|
399
391
|
api_params["stream_options"] = {"include_usage": True}
|
|
400
392
|
|
|
401
|
-
if thinking and
|
|
393
|
+
if thinking and supports_thinking(alias):
|
|
402
394
|
api_params["extra_body"] = {
|
|
403
395
|
"thinking": {"type": "adaptive"},
|
|
404
396
|
"output_config": {"effort": "high"},
|
|
405
397
|
}
|
|
406
|
-
if (
|
|
407
|
-
model == "bedrock.anthropic.claude-opus-4-7"
|
|
408
|
-
or model == "bedrock.anthropic.claude-opus-4-8"
|
|
409
|
-
or model == "bedrock.anthropic.claude-sonnet-5"
|
|
410
|
-
):
|
|
398
|
+
if wants_summarized_display(alias):
|
|
411
399
|
api_params["extra_body"]["thinking"]["display"] = "summarized"
|
|
412
400
|
|
|
413
401
|
if tools:
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Single source of truth for model aliases.
|
|
2
|
+
|
|
3
|
+
Every backend (anthropic.py = direct API, litellm.py = proxy) used to keep
|
|
4
|
+
its own MODEL_MAP: alias -> backend-specific id, plus its own copy of the
|
|
5
|
+
"does this model support thinking / summarized display" conditionals. The
|
|
6
|
+
two drifted — e.g. msg_store_.py called litellm.py with a raw bedrock id
|
|
7
|
+
that wasn't a key in its MODEL_MAP, so it silently fell back to Sonnet
|
|
8
|
+
instead of Haiku. Add a model here once and every caller picks it up.
|
|
9
|
+
|
|
10
|
+
GPT-5.6 entries have no "anthropic" key — they only exist on the LiteLLM
|
|
11
|
+
proxy (Azure/OpenAI backend), there's no direct-Anthropic-API equivalent.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
MODELS = {
|
|
15
|
+
"sonnet-5": {"anthropic": "claude-sonnet-5", "litellm": "bedrock.anthropic.claude-sonnet-5", "thinking": True, "summarized_display": True},
|
|
16
|
+
"opus-4.8": {"anthropic": "claude-opus-4-8", "litellm": "bedrock.anthropic.claude-opus-4-8", "thinking": True, "summarized_display": True},
|
|
17
|
+
"opus-4.7": {"anthropic": "claude-opus-4-7", "litellm": "bedrock.anthropic.claude-opus-4-7", "thinking": True, "summarized_display": True},
|
|
18
|
+
"opus-4.6": {"anthropic": "claude-opus-4-6", "litellm": "bedrock.anthropic.claude-opus-4-6", "thinking": True, "summarized_display": False},
|
|
19
|
+
"sonnet-4.6": {"anthropic": "claude-sonnet-4-6", "litellm": "bedrock.anthropic.claude-sonnet-4-6", "thinking": True, "summarized_display": False},
|
|
20
|
+
"haiku-4.5": {"anthropic": "claude-haiku-4-5-20251001", "litellm": "bedrock.anthropic.claude-haiku-4-5", "thinking": False, "summarized_display": False},
|
|
21
|
+
"gpt-5.6-terra": {"litellm": "openai.gpt-5.6-terra", "thinking": False, "summarized_display": False},
|
|
22
|
+
"gpt-5.6-luna": {"litellm": "openai.gpt-5.6-luna", "thinking": False, "summarized_display": False},
|
|
23
|
+
"gpt-5.6-sol": {"litellm": "openai.gpt-5.6-sol", "thinking": False, "summarized_display": False},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Old alias spellings some call sites still use — resolve to a MODELS key.
|
|
27
|
+
LEGACY_ALIASES = {
|
|
28
|
+
"claude-4.6": "sonnet-4.6",
|
|
29
|
+
"claude-4.5-haiku": "haiku-4.5",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
DEFAULT_MODEL = "sonnet-4.6"
|
|
33
|
+
|
|
34
|
+
# Aliases shown in the /model picker UI, in display order.
|
|
35
|
+
MODEL_OPTIONS = [
|
|
36
|
+
"sonnet-5", "opus-4.8", "opus-4.7", "opus-4.6", "sonnet-4.6", "haiku-4.5",
|
|
37
|
+
"gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.6-sol",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve(alias: str, backend: str) -> str:
|
|
42
|
+
"""Alias -> backend-specific model id ('anthropic' or 'litellm').
|
|
43
|
+
|
|
44
|
+
Unknown aliases pass through unchanged rather than silently substituting
|
|
45
|
+
the default model — a typo'd or not-yet-registered name should surface
|
|
46
|
+
as an API error, not silently run (and bill) a different model. Aliases
|
|
47
|
+
with no entry for the requested backend (e.g. a gpt-5.6-* model asked
|
|
48
|
+
for on 'anthropic') also pass through unchanged, so the direct Anthropic
|
|
49
|
+
API call fails loudly instead of silently downgrading to Sonnet.
|
|
50
|
+
"""
|
|
51
|
+
canonical = LEGACY_ALIASES.get(alias, alias)
|
|
52
|
+
entry = MODELS.get(canonical)
|
|
53
|
+
if entry is None:
|
|
54
|
+
return alias
|
|
55
|
+
return entry.get(backend, alias)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def supports_thinking(alias: str) -> bool:
|
|
59
|
+
canonical = LEGACY_ALIASES.get(alias, alias)
|
|
60
|
+
entry = MODELS.get(canonical)
|
|
61
|
+
return entry["thinking"] if entry else True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def wants_summarized_display(alias: str) -> bool:
|
|
65
|
+
canonical = LEGACY_ALIASES.get(alias, alias)
|
|
66
|
+
entry = MODELS.get(canonical)
|
|
67
|
+
return entry["summarized_display"] if entry else False
|
|
@@ -5,16 +5,17 @@ from rich.console import Group
|
|
|
5
5
|
from micro_cc.screens.md_render_ import render_md
|
|
6
6
|
|
|
7
7
|
BANNER = """\
|
|
8
|
-
[
|
|
9
|
-
[
|
|
10
|
-
|
|
11
|
-
[
|
|
12
|
-
[
|
|
13
|
-
[
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
[#4a8a68]88[/]
|
|
9
|
+
[grey42]""[/]
|
|
10
|
+
|
|
11
|
+
[#4a8a68]88[/][grey42],[/][#4a8a68]dPYba[/][grey42],,[/][#4a8a68]adPYba[/][grey42],[/] [#4a8a68]88[/] [grey42],[/][#4a8a68]adPPYba[/][grey42],[/] [#4a8a68]8b[/][grey42],[/][#4a8a68]dPPYba[/][grey42],[/] [grey42],[/][#4a8a68]adPPYba[/][grey42],[/] [grey42],[/][#4a8a68]adPPYba[/][grey42],[/] [grey42],[/][#4a8a68]adPPYba[/][grey42],[/]
|
|
12
|
+
[#4a8a68]88P[/][grey42]'[/] [grey42]"[/][#4a8a68]88[/][grey42]"[/] [grey42]"[/][#4a8a68]8a[/] [#4a8a68]88[/] [#4a8a68]a8[/][grey42]"[/] [grey42]""[/] [#4a8a68]88P[/][grey42]'[/] [grey42]"[/][#4a8a68]Y8[/] [#4a8a68]a8[/][grey42]"[/] [grey42]"[/][#4a8a68]8a[/] [#4a8a68]a8[/][grey42]"[/] [grey42]""[/] [#4a8a68]a8[/][grey42]"[/] [grey42]""[/]
|
|
13
|
+
[#4a8a68]88[/] [#4a8a68]88[/] [#4a8a68]88[/] [#4a8a68]88[/] [#4a8a68]8b[/] [#4a8a68]88[/] [#4a8a68]8b[/] [#4a8a68]d8[/] [bold #8fd9b6]·[/] [#4a8a68]8b[/] [#4a8a68]8b[/]
|
|
14
|
+
[#4a8a68]88[/] [#4a8a68]88[/] [#4a8a68]88[/] [#4a8a68]88[/] [grey42]"[/][#4a8a68]8a[/][grey42],[/] [grey42],[/][#4a8a68]aa[/] [#4a8a68]88[/] [grey42]"[/][#4a8a68]8a[/][grey42],[/] [grey42],[/][#4a8a68]a8[/][grey42]"[/] [grey42]"[/][#4a8a68]8a[/][grey42],[/] [grey42],[/][#4a8a68]aa[/] [grey42]"[/][#4a8a68]8a[/][grey42],[/] [grey42],[/][#4a8a68]aa[/]
|
|
15
|
+
[#4a8a68]88[/] [#4a8a68]88[/] [#4a8a68]88[/] [#4a8a68]88[/] [grey42]`"[/][#4a8a68]Ybbd8[/][grey42]"'[/] [#4a8a68]88[/] [grey42]`"[/][#4a8a68]YbbdP[/][grey42]"'[/] [grey42]`"[/][#4a8a68]Ybbd8[/][grey42]"'[/] [grey42]`"[/][#4a8a68]Ybbd8[/][grey42]"'[/]
|
|
16
|
+
[grey42 italic]Knowledge work is, by extension, a coding problem.[/]
|
|
17
|
+
|
|
18
|
+
[#4a8a68]enter[/] [grey42]submit[/] [grey42]·[/] [#4a8a68]esc[/] [grey42]interrupt[/] [grey42]·[/] [#4a8a68]/[/] [grey42]commands[/]
|
|
18
19
|
"""
|
|
19
20
|
|
|
20
21
|
|
|
@@ -15,6 +15,7 @@ from micro_cc.utils.process_tracker import init as init_process_tracker
|
|
|
15
15
|
from micro_cc.cache.redis_cache import RedisStateManager
|
|
16
16
|
from micro_cc.screens.window_overlay_ import MessageList, MessageRow, StreamingRow, BANNER
|
|
17
17
|
from micro_cc.utils.file_watcher_ import FileWatcher
|
|
18
|
+
from micro_cc.models import registry as model_registry
|
|
18
19
|
from textual.app import App, ComposeResult
|
|
19
20
|
from textual.containers import Vertical, VerticalScroll
|
|
20
21
|
from textual.widgets import Static
|
|
@@ -131,7 +132,7 @@ class PromptInput(TextArea):
|
|
|
131
132
|
|
|
132
133
|
class MicroApp(App):
|
|
133
134
|
|
|
134
|
-
MODEL_OPTIONS =
|
|
135
|
+
MODEL_OPTIONS = model_registry.MODEL_OPTIONS
|
|
135
136
|
|
|
136
137
|
LOGIN_SEQUENCE = {
|
|
137
138
|
"Anthropic": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "SERPAPI_KEY"],
|
|
@@ -147,7 +148,7 @@ class MicroApp(App):
|
|
|
147
148
|
"SERPAPI_KEY": "SERPAPI_KEY — optional, Enter to skip",
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
_current_model =
|
|
151
|
+
_current_model = model_registry.DEFAULT_MODEL
|
|
151
152
|
_trim_budget = 80000
|
|
152
153
|
|
|
153
154
|
ALLOW_SELECT = True
|
|
@@ -603,7 +604,17 @@ class MicroApp(App):
|
|
|
603
604
|
except Exception:
|
|
604
605
|
return # offline or PyPI unreachable — silently retry next start
|
|
605
606
|
|
|
606
|
-
|
|
607
|
+
def _version_tuple(v):
|
|
608
|
+
parts = []
|
|
609
|
+
for p in v.split("."):
|
|
610
|
+
digits = "".join(ch for ch in p if ch.isdigit())
|
|
611
|
+
parts.append(int(digits) if digits else 0)
|
|
612
|
+
return tuple(parts)
|
|
613
|
+
|
|
614
|
+
# Strict > only — PyPI's JSON endpoint can lag behind a just-published
|
|
615
|
+
# version for a few minutes, and a stale read here must never look
|
|
616
|
+
# like a downgrade target.
|
|
617
|
+
if not latest or _version_tuple(latest) <= _version_tuple(__version__):
|
|
607
618
|
return
|
|
608
619
|
|
|
609
620
|
statusbar = self.query_one("#statusbar", Static)
|
|
@@ -614,14 +625,21 @@ class MicroApp(App):
|
|
|
614
625
|
prompt.disabled = True
|
|
615
626
|
prompt.placeholder = f"Updating micro-cc {__version__} → {latest}…"
|
|
616
627
|
|
|
617
|
-
# --
|
|
618
|
-
#
|
|
619
|
-
#
|
|
620
|
-
#
|
|
621
|
-
# so
|
|
628
|
+
# --force-reinstall guarantees the package (and its deps) actually
|
|
629
|
+
# get overwritten rather than skipped as "already satisfied" — that's
|
|
630
|
+
# what makes this the real update mechanism on other machines, so it
|
|
631
|
+
# stays. No --no-cache-dir though: pip's wheel cache is keyed by
|
|
632
|
+
# exact version+hash, so reusing it is still a full, correct
|
|
633
|
+
# reinstall — it just skips redundant network fetches for deps whose
|
|
634
|
+
# version didn't change. --quiet/--disable-pip-version-check trim
|
|
635
|
+
# chatter, --no-input stops any prompt from hanging a headless run.
|
|
636
|
+
# Piped (not inherited) so pip can never write straight to the tty
|
|
637
|
+
# mid-render and corrupt the Textual alt-screen — a background task
|
|
638
|
+
# drains the pipe as it fills so a chatty pip can't deadlock the
|
|
639
|
+
# subprocess either.
|
|
622
640
|
proc = await asyncio.create_subprocess_exec(
|
|
623
641
|
sys.executable, "-m", "pip", "install",
|
|
624
|
-
"--upgrade", "--force-reinstall",
|
|
642
|
+
"--upgrade", "--force-reinstall",
|
|
625
643
|
"--quiet", "--no-input", "--disable-pip-version-check",
|
|
626
644
|
"micro-cc",
|
|
627
645
|
stdout=asyncio.subprocess.PIPE,
|
|
@@ -1152,7 +1170,14 @@ class MicroApp(App):
|
|
|
1152
1170
|
self.run_worker(self.do_query(next_q), exclusive=True)
|
|
1153
1171
|
|
|
1154
1172
|
def on_unmount(self):
|
|
1155
|
-
|
|
1173
|
+
# No raw /dev/tty write here — on_unmount fires before Textual's own
|
|
1174
|
+
# driver restores the terminal (exits alt-screen, pops the keyboard
|
|
1175
|
+
# enhancement mode, disables mouse tracking), so a write racing that
|
|
1176
|
+
# teardown risks corrupting it. _set_busy(False) already clears the
|
|
1177
|
+
# OSC 9;4 progress indicator at the end of every turn; exiting while
|
|
1178
|
+
# idle needs no extra write, and exiting mid-turn relies on the
|
|
1179
|
+
# terminal clearing per-tab state on process exit, same as every
|
|
1180
|
+
# other OSC 9;4-aware terminal already does.
|
|
1156
1181
|
self._watcher.stop()
|
|
1157
1182
|
self._state_mgr.stop_cleanup_task()
|
|
1158
1183
|
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import shlex
|
|
4
|
+
import shutil
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_SHELL = shutil.which("bash") or "/bin/bash"
|
|
11
|
+
|
|
12
|
+
# Sticky cwd per project_dir: lets `cd` inside one bash_ call carry over to
|
|
13
|
+
# the next, like a real shell session, instead of resetting to project_dir
|
|
14
|
+
# on every call.
|
|
15
|
+
_cwd_state: dict[str, str] = {}
|
|
16
|
+
|
|
17
|
+
# One-time snapshot of the user's interactive aliases/functions, so bash_
|
|
18
|
+
# commands behave like a real terminal instead of a bare, alias-less `bash -c`.
|
|
19
|
+
_snapshot_path: str | None = None
|
|
20
|
+
_snapshot_attempted = False
|
|
21
|
+
_snapshot_lock = asyncio.Lock()
|
|
22
|
+
|
|
23
|
+
# No command-level restrictions here by design: this tool has no visibility
|
|
24
|
+
# into whether it's gated (TUI's dangerous_tools/approval_request already
|
|
25
|
+
# covers bash_ end-to-end at the tool-call level -- see claude_loop_.py).
|
|
26
|
+
# Baking a second, command-level check in here would either duplicate that
|
|
27
|
+
# gate or silently override it, so it doesn't live here.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def _ensure_shell_snapshot() -> str | None:
|
|
31
|
+
"""Capture aliases/functions from an interactive shell once per process
|
|
32
|
+
and reuse them on every bash_ call. Cached including failures, so a
|
|
33
|
+
broken snapshot attempt isn't retried on every single call."""
|
|
34
|
+
global _snapshot_path, _snapshot_attempted
|
|
35
|
+
async with _snapshot_lock:
|
|
36
|
+
if _snapshot_attempted:
|
|
37
|
+
return _snapshot_path
|
|
38
|
+
_snapshot_attempted = True
|
|
39
|
+
fd, path = tempfile.mkstemp(prefix="micro_cc_shell_snapshot_")
|
|
40
|
+
os.close(fd)
|
|
41
|
+
try:
|
|
42
|
+
proc = await asyncio.create_subprocess_exec(
|
|
43
|
+
_SHELL, "-i", "-c",
|
|
44
|
+
f"{{ alias -p; declare -f; }} > {shlex.quote(path)} 2>/dev/null",
|
|
45
|
+
stdin=subprocess.DEVNULL,
|
|
46
|
+
stdout=subprocess.DEVNULL,
|
|
47
|
+
stderr=subprocess.DEVNULL,
|
|
48
|
+
)
|
|
49
|
+
await asyncio.wait_for(proc.wait(), timeout=5)
|
|
50
|
+
if os.path.getsize(path) > 0:
|
|
51
|
+
_snapshot_path = path
|
|
52
|
+
else:
|
|
53
|
+
os.unlink(path)
|
|
54
|
+
except Exception:
|
|
55
|
+
try:
|
|
56
|
+
os.unlink(path)
|
|
57
|
+
except OSError:
|
|
58
|
+
pass
|
|
59
|
+
return _snapshot_path
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _kill_tree(proc):
|
|
63
|
+
"""SIGKILL the whole process group, not just the direct bash process.
|
|
64
|
+
|
|
65
|
+
start_new_session=True makes proc.pid its own process-group id, so this
|
|
66
|
+
also reaps anything the command piped into or backgrounded — a plain
|
|
67
|
+
proc.kill() only hits bash itself and orphans the rest.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
71
|
+
except ProcessLookupError:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def _drain_loop(pipe, chunks: list):
|
|
76
|
+
"""Continuously copy whatever's available on `pipe` into `chunks`.
|
|
77
|
+
|
|
78
|
+
Runs concurrently with the process, not after it — without this, a
|
|
79
|
+
command producing more than the OS pipe buffer (~64KB) blocks on
|
|
80
|
+
write() forever since nothing reads until wait() returns. Left running
|
|
81
|
+
until cancelled; a backgrounded `cmd &` child inheriting this pipe
|
|
82
|
+
would otherwise keep it open past bash's own exit, so we never wait for
|
|
83
|
+
EOF here — the caller cancels once bash itself has exited.
|
|
84
|
+
"""
|
|
85
|
+
fd = pipe.fileno()
|
|
86
|
+
os.set_blocking(fd, False)
|
|
87
|
+
while True:
|
|
88
|
+
try:
|
|
89
|
+
chunk = os.read(fd, 65536)
|
|
90
|
+
except BlockingIOError:
|
|
91
|
+
await asyncio.sleep(0.01)
|
|
92
|
+
continue
|
|
93
|
+
if not chunk:
|
|
94
|
+
return # real EOF: every holder of the write end has closed it
|
|
95
|
+
chunks.append(chunk)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _cancel(task):
|
|
99
|
+
task.cancel()
|
|
100
|
+
try:
|
|
101
|
+
await task
|
|
102
|
+
except asyncio.CancelledError:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def bash_(
|
|
107
|
+
command: str,
|
|
108
|
+
*,
|
|
109
|
+
project_dir: str,
|
|
110
|
+
path: str = None,
|
|
111
|
+
timeout: int = 120
|
|
112
|
+
) -> str:
|
|
113
|
+
"""Execute bash command.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
command: The bash command to execute
|
|
117
|
+
path: Working directory (default: last known cwd for this project, initially project_dir). Relative paths resolve to project_dir.
|
|
118
|
+
timeout: Max seconds before killing process (default 120)
|
|
119
|
+
"""
|
|
120
|
+
max_output_chars = 60000
|
|
121
|
+
|
|
122
|
+
# Determine cwd. `cd` inside a previous command call carries over via
|
|
123
|
+
# _cwd_state, so this behaves like one continuous shell session rather
|
|
124
|
+
# than resetting to project_dir on every call.
|
|
125
|
+
if path:
|
|
126
|
+
if os.path.isabs(path):
|
|
127
|
+
cwd = path
|
|
128
|
+
else:
|
|
129
|
+
cwd = os.path.normpath(os.path.join(project_dir, path))
|
|
130
|
+
else:
|
|
131
|
+
cwd = _cwd_state.get(project_dir, project_dir)
|
|
132
|
+
|
|
133
|
+
if not os.path.isdir(cwd):
|
|
134
|
+
return f"[Directory not found: {cwd}]"
|
|
135
|
+
|
|
136
|
+
if _SHELL is None:
|
|
137
|
+
return "[No bash found on this machine]"
|
|
138
|
+
|
|
139
|
+
snapshot_path = await _ensure_shell_snapshot()
|
|
140
|
+
|
|
141
|
+
cwd_fd, cwd_file = tempfile.mkstemp(prefix="micro_cc_cwd_")
|
|
142
|
+
os.close(cwd_fd)
|
|
143
|
+
|
|
144
|
+
# Wrap the command so we can (a) source captured aliases/functions and
|
|
145
|
+
# (b) capture the resulting cwd for the next call, regardless of the
|
|
146
|
+
# command's own exit code. Newline-separated (not &&) so `pwd -P` still
|
|
147
|
+
# runs even if `command` exits non-zero.
|
|
148
|
+
script_lines = []
|
|
149
|
+
if snapshot_path:
|
|
150
|
+
script_lines.append(f"source {shlex.quote(snapshot_path)} 2>/dev/null || true")
|
|
151
|
+
script_lines.append("shopt -s expand_aliases 2>/dev/null || true")
|
|
152
|
+
script_lines.append(command)
|
|
153
|
+
script_lines.append("__mcc_ec=$?")
|
|
154
|
+
script_lines.append(f"pwd -P > {shlex.quote(cwd_file)}")
|
|
155
|
+
script_lines.append("exit $__mcc_ec")
|
|
156
|
+
full_command = "\n".join(script_lines)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
# Plain subprocess.Popen, not asyncio.create_subprocess_exec: asyncio's
|
|
160
|
+
# subprocess transport ties proc.wait()/returncode to ALL stdio pipes
|
|
161
|
+
# reaching EOF, not just the process exiting. A backgrounded `cmd &`
|
|
162
|
+
# child inherits those same pipes and can hold them open indefinitely,
|
|
163
|
+
# which made the asyncio version hang for the full timeout on every
|
|
164
|
+
# backgrounded launch. Popen.wait() (run in an executor thread below)
|
|
165
|
+
# reflects the shell's own exit immediately, independent of who still
|
|
166
|
+
# holds the pipes.
|
|
167
|
+
proc = subprocess.Popen(
|
|
168
|
+
[_SHELL, "-c", full_command],
|
|
169
|
+
cwd=cwd,
|
|
170
|
+
stdin=subprocess.DEVNULL,
|
|
171
|
+
stdout=subprocess.PIPE,
|
|
172
|
+
stderr=subprocess.PIPE,
|
|
173
|
+
env={**os.environ, "TERM": "dumb"},
|
|
174
|
+
# Detach into a new session: no controlling terminal, own
|
|
175
|
+
# process group. Without this, a child that opens /dev/tty
|
|
176
|
+
# directly (ssh -t, vim, another Textual app, ...) reaches the
|
|
177
|
+
# REAL terminal the host TUI owns — writes its own raw-mode/
|
|
178
|
+
# alt-screen escape codes onto it and steals keyboard input,
|
|
179
|
+
# which is exactly the "bleeds onto terminal and breaks the
|
|
180
|
+
# app" failure. New session also means proc.pid is a process
|
|
181
|
+
# group id, so a timeout can kill the whole tree, not just bash.
|
|
182
|
+
start_new_session=True,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
stdout_chunks: list = []
|
|
186
|
+
stderr_chunks: list = []
|
|
187
|
+
stdout_task = asyncio.ensure_future(_drain_loop(proc.stdout, stdout_chunks))
|
|
188
|
+
stderr_task = asyncio.ensure_future(_drain_loop(proc.stderr, stderr_chunks))
|
|
189
|
+
|
|
190
|
+
loop = asyncio.get_running_loop()
|
|
191
|
+
try:
|
|
192
|
+
await asyncio.wait_for(loop.run_in_executor(None, proc.wait), timeout=timeout)
|
|
193
|
+
except asyncio.TimeoutError:
|
|
194
|
+
_kill_tree(proc)
|
|
195
|
+
await _cancel(stdout_task)
|
|
196
|
+
await _cancel(stderr_task)
|
|
197
|
+
return f"[Command timed out after {timeout}s]"
|
|
198
|
+
except asyncio.CancelledError:
|
|
199
|
+
_kill_tree(proc)
|
|
200
|
+
await _cancel(stdout_task)
|
|
201
|
+
await _cancel(stderr_task)
|
|
202
|
+
raise # re-raise so gather/task cancellation propagates
|
|
203
|
+
|
|
204
|
+
# bash exited — one more beat for the drain loops to pick up
|
|
205
|
+
# anything already flushed to the pipe, then stop. Don't wait for
|
|
206
|
+
# true EOF: a backgrounded child may still hold a pipe open forever.
|
|
207
|
+
await asyncio.sleep(0.05)
|
|
208
|
+
await _cancel(stdout_task)
|
|
209
|
+
await _cancel(stderr_task)
|
|
210
|
+
|
|
211
|
+
stdout = b"".join(stdout_chunks).decode("utf-8", errors="replace")
|
|
212
|
+
stderr = b"".join(stderr_chunks).decode("utf-8", errors="replace")
|
|
213
|
+
|
|
214
|
+
# Build output
|
|
215
|
+
if proc.returncode != 0:
|
|
216
|
+
output = f"[Exit code: {proc.returncode}]\n"
|
|
217
|
+
if stderr:
|
|
218
|
+
output += f"STDERR:\n{stderr}\n"
|
|
219
|
+
if stdout:
|
|
220
|
+
output += f"STDOUT:\n{stdout}"
|
|
221
|
+
else:
|
|
222
|
+
output = stdout
|
|
223
|
+
if stderr:
|
|
224
|
+
output += f"\n[STDERR]: {stderr}"
|
|
225
|
+
|
|
226
|
+
# Truncate if too long (keep head + tail)
|
|
227
|
+
if len(output) > max_output_chars:
|
|
228
|
+
half = max_output_chars // 2
|
|
229
|
+
output = (
|
|
230
|
+
output[:half] +
|
|
231
|
+
f"\n\n... [TRUNCATED {len(output) - max_output_chars} chars] ...\n\n" +
|
|
232
|
+
output[-half:]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return output.strip() or "[No output]"
|
|
236
|
+
|
|
237
|
+
except asyncio.CancelledError:
|
|
238
|
+
raise # always propagate
|
|
239
|
+
except Exception as e:
|
|
240
|
+
return f"[Bash error: {type(e).__name__}: {e}]"
|
|
241
|
+
finally:
|
|
242
|
+
# Pick up wherever `command` left the shell (via `cd`), so the next
|
|
243
|
+
# call starts there instead of resetting to project_dir. Empty/missing
|
|
244
|
+
# on timeout or a crash before `pwd -P` ran -- just skip the update.
|
|
245
|
+
try:
|
|
246
|
+
with open(cwd_file, "r") as f:
|
|
247
|
+
new_cwd = f.read().strip()
|
|
248
|
+
if new_cwd:
|
|
249
|
+
_cwd_state[project_dir] = new_cwd
|
|
250
|
+
except OSError:
|
|
251
|
+
pass
|
|
252
|
+
try:
|
|
253
|
+
os.unlink(cwd_file)
|
|
254
|
+
except OSError:
|
|
255
|
+
pass
|