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.
Files changed (161) hide show
  1. {micro_cc-0.2.32 → micro_cc-0.2.35}/PKG-INFO +10 -11
  2. {micro_cc-0.2.32 → micro_cc-0.2.35}/README.md +9 -10
  3. {micro_cc-0.2.32 → micro_cc-0.2.35}/pyproject.toml +1 -1
  4. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/models/anthropic.py +8 -15
  5. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/models/litellm.py +7 -19
  6. micro_cc-0.2.35/src/micro_cc/models/registry.py +67 -0
  7. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/screens/window_overlay_.py +11 -10
  8. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/start_live_.py +35 -10
  9. micro_cc-0.2.35/src/micro_cc/tools/bash_tool.py +255 -0
  10. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/msg_store_.py +2 -2
  11. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc.egg-info/PKG-INFO +10 -11
  12. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc.egg-info/SOURCES.txt +1 -0
  13. micro_cc-0.2.32/src/micro_cc/tools/bash_tool.py +0 -92
  14. {micro_cc-0.2.32 → micro_cc-0.2.35}/setup.cfg +0 -0
  15. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/__init__.py +0 -0
  16. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/browser/__init__.py +0 -0
  17. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/browser/_cookies.py +0 -0
  18. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/browser/_md_convert.py +0 -0
  19. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/browser/browser_manager.py +0 -0
  20. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/browser/simpletextbrowser.py +0 -0
  21. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/cache/__init__.py +0 -0
  22. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/cache/redis_cache.py +0 -0
  23. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/claude_loop_.py +0 -0
  24. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/code_test/__init__.py +0 -0
  25. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/code_test/run_token_cutter.py +0 -0
  26. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/code_test/test_messages.py +0 -0
  27. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/execute_tool.py +0 -0
  28. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/models/__init__.py +0 -0
  29. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/models/ollama.py +0 -0
  30. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/models/schema.py +0 -0
  31. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/screens/__init__.py +0 -0
  32. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/screens/md_render_.py +0 -0
  33. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/screens/microcc-styles.tcss +0 -0
  34. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/SKILL.md +0 -0
  35. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +0 -0
  36. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  37. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  38. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +0 -0
  39. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  40. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +0 -0
  41. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
  42. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  43. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +0 -0
  44. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  45. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  46. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  47. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +0 -0
  48. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  49. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/DMMono-OFL.txt +0 -0
  50. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
  51. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +0 -0
  52. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
  53. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
  54. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +0 -0
  55. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
  56. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Gloock-OFL.txt +0 -0
  57. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
  58. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  59. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +0 -0
  60. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  61. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  62. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  63. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  64. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  65. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  66. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  67. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  68. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +0 -0
  69. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  70. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  71. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  72. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Italiana-OFL.txt +0 -0
  73. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
  74. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  75. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +0 -0
  76. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  77. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
  78. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
  79. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Jura-OFL.txt +0 -0
  80. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +0 -0
  81. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  82. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
  83. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  84. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
  85. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-OFL.txt +0 -0
  86. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
  87. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
  88. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +0 -0
  89. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
  90. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +0 -0
  91. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  92. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
  93. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +0 -0
  94. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
  95. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  96. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +0 -0
  97. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +0 -0
  98. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  99. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  100. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +0 -0
  101. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  102. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +0 -0
  103. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  104. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  105. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +0 -0
  106. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
  107. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +0 -0
  108. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
  109. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
  110. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  111. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
  112. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +0 -0
  113. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
  114. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +0 -0
  115. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  116. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/github/SKILL.md +0 -0
  117. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/SKILL.md +0 -0
  118. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/editing.md +0 -0
  119. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/pptxgenjs.md +0 -0
  120. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/__init__.py +0 -0
  121. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/add_slide.py +0 -0
  122. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/clean.py +0 -0
  123. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/helpers/__init__.py +0 -0
  124. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/helpers/merge_runs.py +0 -0
  125. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/helpers/simplify_redlines.py +0 -0
  126. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/pack.py +0 -0
  127. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/soffice.py +0 -0
  128. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/unpack.py +0 -0
  129. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validate.py +0 -0
  130. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validators/__init__.py +0 -0
  131. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validators/base.py +0 -0
  132. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validators/docx.py +0 -0
  133. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validators/pptx.py +0 -0
  134. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/office/validators/redlining.py +0 -0
  135. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/pptx/scripts/thumbnail.py +0 -0
  136. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/skill_loader.py +0 -0
  137. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/skills/xlsx/SKILL.md +0 -0
  138. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/__init__.py +0 -0
  139. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/browser_tool_.py +0 -0
  140. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/computer_tool_.py +0 -0
  141. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/file_tools_.py +0 -0
  142. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/mcp_client_.py +0 -0
  143. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/memory_tool_.py +0 -0
  144. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/search_tool_.py +0 -0
  145. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/skill_tools_.py +0 -0
  146. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/todo_tools_.py +0 -0
  147. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/vision_tools_.py +0 -0
  148. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/tools/web_tools_.py +0 -0
  149. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/__init__.py +0 -0
  150. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/annotate_.py +0 -0
  151. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/claude_md_loader.py +0 -0
  152. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/file_watcher_.py +0 -0
  153. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/helpers.py +0 -0
  154. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/memory_store_.py +0 -0
  155. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/process_tracker.py +0 -0
  156. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/utils/tokenization_simple.py +0 -0
  157. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc/web_.py +0 -0
  158. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc.egg-info/dependency_links.txt +0 -0
  159. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc.egg-info/entry_points.txt +0 -0
  160. {micro_cc-0.2.32 → micro_cc-0.2.35}/src/micro_cc.egg-info/requires.txt +0 -0
  161. {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.32
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
- c o g n i t i v e c o m p u t e
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 — running directly on the metal. Verbose, thorough planning paired with unrestricted file operations means micro·cc can take on complex multi-step work autonomously. Point it at any project directory and go.
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
- c o g n i t i v e c o m p u t e
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 — running directly on the metal. Verbose, thorough planning paired with unrestricted file operations means micro·cc can take on complex multi-step work autonomously. Point it at any project directory and go.
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.32"
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="claude-4.6",
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
- MODEL_MAP = {
100
- "sonnet-5": "claude-sonnet-5",
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 model != "claude-haiku-4-5-20251001":
227
+ if thinking and supports_thinking(alias):
235
228
  api_parameters["thinking"] = {"type": "adaptive"}
236
- if model == "claude-opus-4-7" or model == "claude-opus-4-8" or model == "claude-sonnet-5":
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="bedrock.anthropic.claude-sonnet-4-6",
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
- MODEL_MAP = {
319
- "sonnet-5": "bedrock.anthropic.claude-sonnet-5",
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 model != "bedrock.anthropic.claude-haiku-4-5":
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
- [bold white]███╗ ███╗ ██████╗ ██████╗ ███████╗██╗[/]
9
- [bold white]████╗ ████║██╔═══██╗██╔══██╗██╔════╝██║[/]
10
- [bold white]██╔████╔██║██║ ██║██║ ██║█████╗ ██║[/]
11
- [dim]██║╚██╔╝██║██║ ██║██║ ██║██╔══╝ ██║[/]
12
- [dim]██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗███████╗[/]
13
- [dim]╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝[/]
14
- [dim]───────[/] [bold]on the metal[/] [dim]─────────────────────[/]
15
- [dim italic]Knowledge work is by extension a coding problem.[/]
16
-
17
- [white]enter[/] [dim]submit[/] [dim]·[/] [white]esc[/] [dim]interrupt[/] [dim]·[/] [white]/[/] [dim]commands[/]
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 = ["sonnet-5", "opus-4.8", "opus-4.7", "opus-4.6", "sonnet-4.6", "haiku-4.5"]
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 = "sonnet-4.6"
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
- if not latest or latest == __version__:
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
- # --quiet/--disable-pip-version-check trim chatter, --no-input stops
618
- # any prompt from hanging a headless run. Piped (not inherited) so
619
- # pip can never write straight to the tty mid-render and corrupt the
620
- # Textual alt-screen a background task drains the pipe as it fills
621
- # so a chatty pip can't deadlock the subprocess either.
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", "--no-cache-dir",
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
- self._term_busy(False) # clear the terminal progress indicator on exit
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