acra-cli 0.1.4__tar.gz → 0.1.5__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 (191) hide show
  1. {acra_cli-0.1.4/acra_cli.egg-info → acra_cli-0.1.5}/PKG-INFO +34 -11
  2. {acra_cli-0.1.4 → acra_cli-0.1.5}/README.md +33 -10
  3. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/__init__.py +1 -1
  4. acra_cli-0.1.5/acra/agents/__pycache__/llm.cpython-312.pyc +0 -0
  5. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/llm.py +24 -8
  6. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/ask.py +9 -2
  7. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/keys.py +6 -2
  8. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/research.py +4 -1
  9. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/config/__pycache__/defaults.cpython-312.pyc +0 -0
  10. acra_cli-0.1.5/acra/config/__pycache__/profile_manager.cpython-312.pyc +0 -0
  11. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/config/profile_manager.py +26 -4
  12. acra_cli-0.1.5/acra/ui/__pycache__/components.cpython-312.pyc +0 -0
  13. acra_cli-0.1.5/acra/ui/__pycache__/shell.cpython-312.pyc +0 -0
  14. acra_cli-0.1.5/acra/ui/__pycache__/spinner.cpython-312.pyc +0 -0
  15. acra_cli-0.1.5/acra/ui/components.py +78 -0
  16. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/ui/shell.py +6 -10
  17. acra_cli-0.1.5/acra/ui/spinner.py +97 -0
  18. acra_cli-0.1.5/acra/utils/keyring_manager.py +179 -0
  19. acra_cli-0.1.5/acra/utils/output_formatter.py +113 -0
  20. {acra_cli-0.1.4 → acra_cli-0.1.5/acra_cli.egg-info}/PKG-INFO +34 -11
  21. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra_cli.egg-info/SOURCES.txt +2 -0
  22. {acra_cli-0.1.4 → acra_cli-0.1.5}/pyproject.toml +1 -1
  23. acra_cli-0.1.4/acra/agents/__pycache__/llm.cpython-312.pyc +0 -0
  24. acra_cli-0.1.4/acra/config/__pycache__/profile_manager.cpython-312.pyc +0 -0
  25. acra_cli-0.1.4/acra/ui/__pycache__/components.cpython-312.pyc +0 -0
  26. acra_cli-0.1.4/acra/ui/__pycache__/shell.cpython-312.pyc +0 -0
  27. acra_cli-0.1.4/acra/ui/components.py +0 -32
  28. acra_cli-0.1.4/acra/utils/keyring_manager.py +0 -83
  29. acra_cli-0.1.4/acra/utils/output_formatter.py +0 -18
  30. {acra_cli-0.1.4 → acra_cli-0.1.5}/LICENSE +0 -0
  31. {acra_cli-0.1.4 → acra_cli-0.1.5}/MANIFEST.in +0 -0
  32. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/__main__.py +0 -0
  33. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/__init__.py +0 -0
  34. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/__pycache__/__init__.cpython-312.pyc +0 -0
  35. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/__pycache__/llm_utils.cpython-312.pyc +0 -0
  36. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/coder/__init__.py +0 -0
  37. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/coder/__pycache__/__init__.cpython-312.pyc +0 -0
  38. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/coder/__pycache__/coder_agent.cpython-312.pyc +0 -0
  39. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/coder/__pycache__/coder_chain.cpython-312.pyc +0 -0
  40. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/coder/coder_agent.py +0 -0
  41. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/coder/coder_chain.py +0 -0
  42. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/critic/__init__.py +0 -0
  43. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/critic/__pycache__/__init__.cpython-312.pyc +0 -0
  44. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/critic/__pycache__/critic_agent.cpython-312.pyc +0 -0
  45. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/critic/__pycache__/critic_chain.cpython-312.pyc +0 -0
  46. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/critic/critic_agent.py +0 -0
  47. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/critic/critic_chain.py +0 -0
  48. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/executor/__init__.py +0 -0
  49. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/executor/__pycache__/__init__.cpython-312.pyc +0 -0
  50. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/executor/__pycache__/docker_runner.cpython-312.pyc +0 -0
  51. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/executor/__pycache__/executor_agent.cpython-312.pyc +0 -0
  52. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/executor/__pycache__/sandbox_runner.cpython-312.pyc +0 -0
  53. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/executor/docker_runner.py +0 -0
  54. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/executor/executor_agent.py +0 -0
  55. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/executor/sandbox_runner.py +0 -0
  56. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/human/__init__.py +0 -0
  57. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/human/__pycache__/__init__.cpython-312.pyc +0 -0
  58. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/human/__pycache__/human_node.cpython-312.pyc +0 -0
  59. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/human/human_node.py +0 -0
  60. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/llm_utils.py +0 -0
  61. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/memory/__init__.py +0 -0
  62. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/memory/__pycache__/__init__.cpython-312.pyc +0 -0
  63. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/memory/__pycache__/memory_agent.cpython-312.pyc +0 -0
  64. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/memory/__pycache__/memory_manager.cpython-312.pyc +0 -0
  65. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/memory/__pycache__/retrieval.cpython-312.pyc +0 -0
  66. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/memory/memory_agent.py +0 -0
  67. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/memory/memory_manager.py +0 -0
  68. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/memory/retrieval.py +0 -0
  69. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/planner/__init__.py +0 -0
  70. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/planner/__pycache__/__init__.cpython-312.pyc +0 -0
  71. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/planner/__pycache__/planner_agent.cpython-312.pyc +0 -0
  72. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/planner/__pycache__/planner_chain.cpython-312.pyc +0 -0
  73. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/planner/planner_agent.py +0 -0
  74. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/planner/planner_chain.py +0 -0
  75. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/researcher/__init__.py +0 -0
  76. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/researcher/__pycache__/__init__.cpython-312.pyc +0 -0
  77. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/researcher/__pycache__/researcher_agent.cpython-312.pyc +0 -0
  78. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/researcher/__pycache__/researcher_chain.cpython-312.pyc +0 -0
  79. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/researcher/researcher_agent.py +0 -0
  80. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/agents/researcher/researcher_chain.py +0 -0
  81. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/brain/__init__.py +0 -0
  82. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/brain/model_registry.py +0 -0
  83. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/brain/provider_manager.py +0 -0
  84. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/cli.py +0 -0
  85. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/__init__.py +0 -0
  86. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/brain.py +0 -0
  87. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/config.py +0 -0
  88. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/context.py +0 -0
  89. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/graph.py +0 -0
  90. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/logs.py +0 -0
  91. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/memory.py +0 -0
  92. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/plugin.py +0 -0
  93. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/serve.py +0 -0
  94. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/session.py +0 -0
  95. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/commands/utils.py +0 -0
  96. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/config/__init__.py +0 -0
  97. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/config/__pycache__/__init__.cpython-312.pyc +0 -0
  98. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/config/defaults.py +0 -0
  99. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/execution/__init__.py +0 -0
  100. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/execution/logs/__init__.py +0 -0
  101. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/execution/logs/error_logs/__init__.py +0 -0
  102. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/execution/logs/execution_logs/__init__.py +0 -0
  103. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/execution/sandbox/__init__.py +0 -0
  104. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/execution/sandbox/docker_executor.py +0 -0
  105. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/execution/sandbox/isolated_runner.py +0 -0
  106. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/execution/sandbox/sandbox_manager.py +0 -0
  107. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/graph/__init__.py +0 -0
  108. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/graph/checkpoint.py +0 -0
  109. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/graph/conditional_edges.py +0 -0
  110. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/graph/edges.py +0 -0
  111. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/graph/graph_visualizer.py +0 -0
  112. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/graph/nodes.py +0 -0
  113. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/graph/router.py +0 -0
  114. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/graph/state.py +0 -0
  115. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/graph/workflow.py +0 -0
  116. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/__init__.py +0 -0
  117. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/checkpoints/__init__.py +0 -0
  118. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/checkpoints/data/__init__.py +0 -0
  119. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/checkpoints/postgres_checkpoint.py +0 -0
  120. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/checkpoints/sqlite_checkpoint.py +0 -0
  121. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/chroma_db/__init__.py +0 -0
  122. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/conversation_memory/__init__.py +0 -0
  123. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/conversation_memory/long_term.py +0 -0
  124. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/conversation_memory/short_term.py +0 -0
  125. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/storage/__init__.py +0 -0
  126. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/vector_store/__init__.py +0 -0
  127. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/vector_store/chroma_store.py +0 -0
  128. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/memory/vector_store/embeddings.py +0 -0
  129. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/observability/__init__.py +0 -0
  130. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/observability/langsmith_tracing.py +0 -0
  131. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/observability/metrics.py +0 -0
  132. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/observability/monitoring.py +0 -0
  133. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/observability/token_tracking.py +0 -0
  134. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/prompts/__init__.py +0 -0
  135. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/__init__.py +0 -0
  136. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/__pycache__/__init__.cpython-312.pyc +0 -0
  137. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/__pycache__/coder_schema.cpython-312.pyc +0 -0
  138. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/__pycache__/critic_schema.cpython-312.pyc +0 -0
  139. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/__pycache__/execution_schema.cpython-312.pyc +0 -0
  140. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/__pycache__/planner_schema.cpython-312.pyc +0 -0
  141. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/__pycache__/researcher_schema.cpython-312.pyc +0 -0
  142. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/__pycache__/shared_schema.cpython-312.pyc +0 -0
  143. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/coder_schema.py +0 -0
  144. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/critic_schema.py +0 -0
  145. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/execution_schema.py +0 -0
  146. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/planner_schema.py +0 -0
  147. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/researcher_schema.py +0 -0
  148. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/schemas/shared_schema.py +0 -0
  149. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/__init__.py +0 -0
  150. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/code_tools/__init__.py +0 -0
  151. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/code_tools/file_reader.py +0 -0
  152. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/code_tools/file_writer.py +0 -0
  153. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/code_tools/python_repl.py +0 -0
  154. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/code_tools/terminal_runner.py +0 -0
  155. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/github_tools/__init__.py +0 -0
  156. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/github_tools/commit_generator.py +0 -0
  157. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/github_tools/github_search.py +0 -0
  158. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/github_tools/repo_analyzer.py +0 -0
  159. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/integration_test_helper.py +0 -0
  160. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/validation_tools/__init__.py +0 -0
  161. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/validation_tools/dependency_checker.py +0 -0
  162. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/validation_tools/environment_config_validator.py +0 -0
  163. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/validation_tools/http_validator.py +0 -0
  164. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/validation_tools/response_validator.py +0 -0
  165. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/validation_tools/security_checker.py +0 -0
  166. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/validation_tools/syntax_checker.py +0 -0
  167. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/validation_tools/web_framework_validator.py +0 -0
  168. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/web_tools/__init__.py +0 -0
  169. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/web_tools/arxiv_search.py +0 -0
  170. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/web_tools/serpapi_search.py +0 -0
  171. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/tools/web_tools/tavily_search.py +0 -0
  172. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/ui/__init__.py +0 -0
  173. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/ui/__pycache__/__init__.cpython-312.pyc +0 -0
  174. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/ui/__pycache__/animated_banner.cpython-312.pyc +0 -0
  175. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/ui/__pycache__/banner.cpython-312.pyc +0 -0
  176. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/ui/__pycache__/theme.cpython-312.pyc +0 -0
  177. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/ui/animated_banner.py +0 -0
  178. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/ui/banner.py +0 -0
  179. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/ui/theme.py +0 -0
  180. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/utils/__init__.py +0 -0
  181. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/utils/context_manager.py +0 -0
  182. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra/utils/session_manager.py +0 -0
  183. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra_cli.egg-info/dependency_links.txt +0 -0
  184. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra_cli.egg-info/entry_points.txt +0 -0
  185. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra_cli.egg-info/requires.txt +0 -0
  186. {acra_cli-0.1.4 → acra_cli-0.1.5}/acra_cli.egg-info/top_level.txt +0 -0
  187. {acra_cli-0.1.4 → acra_cli-0.1.5}/requirements.txt +0 -0
  188. {acra_cli-0.1.4 → acra_cli-0.1.5}/setup.cfg +0 -0
  189. {acra_cli-0.1.4 → acra_cli-0.1.5}/setup.py +0 -0
  190. {acra_cli-0.1.4 → acra_cli-0.1.5}/tests/test_cli.py +0 -0
  191. {acra_cli-0.1.4 → acra_cli-0.1.5}/tests/test_interactive_routing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acra-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Agentic CLI for LLM task execution, research, and workflow automation.
5
5
  Author: Raj Tembe
6
6
  License: GNU Affero General Public License v3
@@ -79,6 +79,23 @@ It is built for developers who want a local CLI for asking an LLM-powered agent
79
79
  - ChromaDB-backed vector memory and JSON workflow memory modules
80
80
  - Optional extras for provider-specific dependencies and checkpoint backends
81
81
 
82
+ ## What's New in 0.1.5
83
+
84
+ ### New features
85
+
86
+ - Provider credentials can now be stored with `acra keys` and used directly by Gemini, OpenAI, Groq, and HuggingFace Cloud workflows.
87
+ - The configuration wizard stores provider and research credentials outside the plaintext profile file, using the OS keyring when available and a permission-restricted local fallback otherwise.
88
+ - Long-running task and research commands show a live progress spinner.
89
+ - Task and research results are rendered as readable summaries, findings, sources, and generated-file lists instead of raw workflow state.
90
+ - Commands launched from the interactive shell now stream their output live.
91
+
92
+ ### Bug fixes
93
+
94
+ - Fixed saved provider keys not being read by LLM initialization.
95
+ - Fixed profile setup writing API and research keys to plaintext configuration.
96
+ - Fixed interactive-shell commands buffering output until completion.
97
+ - Fixed unwieldy raw dictionary and message-object output after task or research runs.
98
+
82
99
  ## Requirements
83
100
 
84
101
  - Python `>=3.11`
@@ -157,7 +174,7 @@ acra config show
157
174
  Store a key in your OS keyring:
158
175
 
159
176
  ```bash
160
- acra keys set provider
177
+ acra keys set GEMINI_API_KEY
161
178
  ```
162
179
 
163
180
  Run a research workflow:
@@ -304,13 +321,13 @@ The setup wizard prompts for:
304
321
  Set a key interactively:
305
322
 
306
323
  ```bash
307
- acra keys set provider
324
+ acra keys set OPENAI_API_KEY
308
325
  ```
309
326
 
310
327
  Set a key directly:
311
328
 
312
329
  ```bash
313
- acra keys set provider "your-key"
330
+ acra keys set OPENAI_API_KEY "your-key"
314
331
  ```
315
332
 
316
333
  List key status:
@@ -322,9 +339,11 @@ acra keys list
322
339
  Delete a key:
323
340
 
324
341
  ```bash
325
- acra keys delete provider
342
+ acra keys delete OPENAI_API_KEY
326
343
  ```
327
344
 
345
+ Supported provider key names are `GEMINI_API_KEY`, `OPENAI_API_KEY`, `GROQ_API_KEY`, and `HF_API_KEY`. Keys are stored in your OS keyring when it is available. On systems without a keyring backend, acra uses a local credentials file with owner-only permissions.
346
+
328
347
  Research key names:
329
348
 
330
349
  ```bash
@@ -336,7 +355,10 @@ acra keys set research.arxiv
336
355
 
337
356
  Environment fallback variables:
338
357
 
339
- - `ACRA_PROVIDER_KEY`
358
+ - `GEMINI_API_KEY` or `GOOGLE_GEMINI_API_KEY`
359
+ - `OPENAI_API_KEY`
360
+ - `GROQ_API_KEY`
361
+ - `HF_API_KEY`
340
362
  - `ACRA_RESEARCH_WEB_KEY`
341
363
  - `ACRA_RESEARCH_GITHUB_KEY`
342
364
  - `ACRA_RESEARCH_DOCS_KEY`
@@ -439,9 +461,9 @@ acra config show
439
461
  Keys:
440
462
 
441
463
  ```bash
442
- acra keys set provider
464
+ acra keys set GEMINI_API_KEY
443
465
  acra keys list
444
- acra keys delete provider
466
+ acra keys delete GEMINI_API_KEY
445
467
  ```
446
468
 
447
469
  Memory:
@@ -548,6 +570,7 @@ export OMNIAGENT_DATA_DIR=/path/to/acra-data
548
570
 
549
571
  Important subdirectories:
550
572
 
573
+ - `credentials.json`: permission-restricted credential fallback when no OS keyring is available
551
574
  - `projects/`: generated project files
552
575
  - `memory/storage/`: JSON memory files
553
576
  - `memory/chroma_db/`: ChromaDB vector memory
@@ -555,7 +578,7 @@ Important subdirectories:
555
578
 
556
579
  ## Current Beta Notes
557
580
 
558
- - The package version is `0.1.2`.
581
+ - The package version is `0.1.5`.
559
582
  - The top-level CLI currently attaches `serve`, `config`, `keys`, `research`, `memory`, `session`, `graph`, and `workspace`.
560
583
  - The codebase contains additional command modules such as `brain`, `context`, `logs`, and `plugin`, but they are not currently attached to the top-level CLI.
561
584
  - Some CLI command groups return placeholder output while the Python modules behind them continue to evolve.
@@ -587,7 +610,7 @@ export HF_API_KEY="your-token"
587
610
  Or store it with:
588
611
 
589
612
  ```bash
590
- acra keys set provider
613
+ acra keys set GEMINI_API_KEY
591
614
  ```
592
615
 
593
616
  ### Ollama connection failure
@@ -616,7 +639,7 @@ export OLLAMA_BASE_URL=http://localhost:11434
616
639
  - PyPI package name: `acra-cli`
617
640
  - Console command: `acra`
618
641
  - Python import package: `acra`
619
- - Version: `0.1.2`
642
+ - Version: `0.1.5`
620
643
  - Python: `>=3.11`
621
644
  - Console script: `acra=acra.cli:app_main`
622
645
  - Author: Raj Tembe
@@ -23,6 +23,23 @@ It is built for developers who want a local CLI for asking an LLM-powered agent
23
23
  - ChromaDB-backed vector memory and JSON workflow memory modules
24
24
  - Optional extras for provider-specific dependencies and checkpoint backends
25
25
 
26
+ ## What's New in 0.1.5
27
+
28
+ ### New features
29
+
30
+ - Provider credentials can now be stored with `acra keys` and used directly by Gemini, OpenAI, Groq, and HuggingFace Cloud workflows.
31
+ - The configuration wizard stores provider and research credentials outside the plaintext profile file, using the OS keyring when available and a permission-restricted local fallback otherwise.
32
+ - Long-running task and research commands show a live progress spinner.
33
+ - Task and research results are rendered as readable summaries, findings, sources, and generated-file lists instead of raw workflow state.
34
+ - Commands launched from the interactive shell now stream their output live.
35
+
36
+ ### Bug fixes
37
+
38
+ - Fixed saved provider keys not being read by LLM initialization.
39
+ - Fixed profile setup writing API and research keys to plaintext configuration.
40
+ - Fixed interactive-shell commands buffering output until completion.
41
+ - Fixed unwieldy raw dictionary and message-object output after task or research runs.
42
+
26
43
  ## Requirements
27
44
 
28
45
  - Python `>=3.11`
@@ -101,7 +118,7 @@ acra config show
101
118
  Store a key in your OS keyring:
102
119
 
103
120
  ```bash
104
- acra keys set provider
121
+ acra keys set GEMINI_API_KEY
105
122
  ```
106
123
 
107
124
  Run a research workflow:
@@ -248,13 +265,13 @@ The setup wizard prompts for:
248
265
  Set a key interactively:
249
266
 
250
267
  ```bash
251
- acra keys set provider
268
+ acra keys set OPENAI_API_KEY
252
269
  ```
253
270
 
254
271
  Set a key directly:
255
272
 
256
273
  ```bash
257
- acra keys set provider "your-key"
274
+ acra keys set OPENAI_API_KEY "your-key"
258
275
  ```
259
276
 
260
277
  List key status:
@@ -266,9 +283,11 @@ acra keys list
266
283
  Delete a key:
267
284
 
268
285
  ```bash
269
- acra keys delete provider
286
+ acra keys delete OPENAI_API_KEY
270
287
  ```
271
288
 
289
+ Supported provider key names are `GEMINI_API_KEY`, `OPENAI_API_KEY`, `GROQ_API_KEY`, and `HF_API_KEY`. Keys are stored in your OS keyring when it is available. On systems without a keyring backend, acra uses a local credentials file with owner-only permissions.
290
+
272
291
  Research key names:
273
292
 
274
293
  ```bash
@@ -280,7 +299,10 @@ acra keys set research.arxiv
280
299
 
281
300
  Environment fallback variables:
282
301
 
283
- - `ACRA_PROVIDER_KEY`
302
+ - `GEMINI_API_KEY` or `GOOGLE_GEMINI_API_KEY`
303
+ - `OPENAI_API_KEY`
304
+ - `GROQ_API_KEY`
305
+ - `HF_API_KEY`
284
306
  - `ACRA_RESEARCH_WEB_KEY`
285
307
  - `ACRA_RESEARCH_GITHUB_KEY`
286
308
  - `ACRA_RESEARCH_DOCS_KEY`
@@ -383,9 +405,9 @@ acra config show
383
405
  Keys:
384
406
 
385
407
  ```bash
386
- acra keys set provider
408
+ acra keys set GEMINI_API_KEY
387
409
  acra keys list
388
- acra keys delete provider
410
+ acra keys delete GEMINI_API_KEY
389
411
  ```
390
412
 
391
413
  Memory:
@@ -492,6 +514,7 @@ export OMNIAGENT_DATA_DIR=/path/to/acra-data
492
514
 
493
515
  Important subdirectories:
494
516
 
517
+ - `credentials.json`: permission-restricted credential fallback when no OS keyring is available
495
518
  - `projects/`: generated project files
496
519
  - `memory/storage/`: JSON memory files
497
520
  - `memory/chroma_db/`: ChromaDB vector memory
@@ -499,7 +522,7 @@ Important subdirectories:
499
522
 
500
523
  ## Current Beta Notes
501
524
 
502
- - The package version is `0.1.2`.
525
+ - The package version is `0.1.5`.
503
526
  - The top-level CLI currently attaches `serve`, `config`, `keys`, `research`, `memory`, `session`, `graph`, and `workspace`.
504
527
  - The codebase contains additional command modules such as `brain`, `context`, `logs`, and `plugin`, but they are not currently attached to the top-level CLI.
505
528
  - Some CLI command groups return placeholder output while the Python modules behind them continue to evolve.
@@ -531,7 +554,7 @@ export HF_API_KEY="your-token"
531
554
  Or store it with:
532
555
 
533
556
  ```bash
534
- acra keys set provider
557
+ acra keys set GEMINI_API_KEY
535
558
  ```
536
559
 
537
560
  ### Ollama connection failure
@@ -560,7 +583,7 @@ export OLLAMA_BASE_URL=http://localhost:11434
560
583
  - PyPI package name: `acra-cli`
561
584
  - Console command: `acra`
562
585
  - Python import package: `acra`
563
- - Version: `0.1.2`
586
+ - Version: `0.1.5`
564
587
  - Python: `>=3.11`
565
588
  - Console script: `acra=acra.cli:app_main`
566
589
  - Author: Raj Tembe
@@ -1,6 +1,6 @@
1
1
  """acra package root."""
2
2
 
3
- __version__ = "0.1.4"
3
+ __version__ = "0.1.5"
4
4
 
5
5
  from .cli import main
6
6
 
@@ -73,10 +73,17 @@ class LLMInitializationError(Exception):
73
73
 
74
74
  def _init_gemini():
75
75
  """Initialize Google Gemini LLM."""
76
- gemini_api_key = os.getenv("GOOGLE_GEMINI_API_KEY") or os.getenv("GEMINI_API_KEY")
76
+ from acra.utils.keyring_manager import get_key
77
+
78
+ gemini_api_key = (
79
+ os.getenv("GOOGLE_GEMINI_API_KEY")
80
+ or os.getenv("GEMINI_API_KEY")
81
+ or get_key("GEMINI_API_KEY")
82
+ )
77
83
  if not gemini_api_key or gemini_api_key.startswith("your_"):
78
84
  raise EnvironmentError(
79
- "GEMINI_API_KEY is not set. Add GOOGLE_GEMINI_API_KEY to your .env file."
85
+ "GEMINI_API_KEY is not set. Run 'acra keys set GEMINI_API_KEY' "
86
+ "or add GOOGLE_GEMINI_API_KEY to your .env file."
80
87
  )
81
88
  os.environ["GOOGLE_API_KEY"] = gemini_api_key
82
89
  return ChatGoogleGenerativeAI(
@@ -91,10 +98,13 @@ def _init_openai():
91
98
  raise LLMInitializationError(
92
99
  "OpenAI support is not installed. Install with: pip install langchain-openai"
93
100
  )
94
- openai_api_key = os.getenv("OPENAI_API_KEY")
101
+ from acra.utils.keyring_manager import get_key
102
+
103
+ openai_api_key = os.getenv("OPENAI_API_KEY") or get_key("OPENAI_API_KEY")
95
104
  if not openai_api_key or openai_api_key.startswith("your_"):
96
105
  raise LLMInitializationError(
97
- "OPENAI_API_KEY is not set. Add OPENAI_API_KEY to your .env file."
106
+ "OPENAI_API_KEY is not set. Run 'acra keys set OPENAI_API_KEY' "
107
+ "or add OPENAI_API_KEY to your .env file."
98
108
  )
99
109
  return ChatOpenAI(
100
110
  model=OPENAI_MODEL,
@@ -109,10 +119,13 @@ def _init_groq():
109
119
  raise LLMInitializationError(
110
120
  "Groq support is not installed. Install with: pip install langchain-groq"
111
121
  )
112
- groq_api_key = os.getenv("GROQ_API_KEY")
122
+ from acra.utils.keyring_manager import get_key
123
+
124
+ groq_api_key = os.getenv("GROQ_API_KEY") or get_key("GROQ_API_KEY")
113
125
  if not groq_api_key or groq_api_key.startswith("your_"):
114
126
  raise LLMInitializationError(
115
- "GROQ_API_KEY is not set. Add GROQ_API_KEY to your .env file."
127
+ "GROQ_API_KEY is not set. Run 'acra keys set GROQ_API_KEY' "
128
+ "or add GROQ_API_KEY to your .env file."
116
129
  )
117
130
  return ChatGroq(
118
131
  model=GROQ_MODEL,
@@ -150,10 +163,13 @@ def _init_huggingface_cloud():
150
163
  "HuggingFace not installed. Install with: pip install langchain-huggingface"
151
164
  )
152
165
 
153
- huggingface_api_key = os.getenv("HF_API_KEY")
166
+ from acra.utils.keyring_manager import get_key
167
+
168
+ huggingface_api_key = os.getenv("HF_API_KEY") or get_key("HF_API_KEY")
154
169
  if not huggingface_api_key or huggingface_api_key.startswith("your_"):
155
170
  raise LLMInitializationError(
156
- "HF_API_KEY is not set. Add HF_API_KEY to your .env file. "
171
+ "HF_API_KEY is not set. Run 'acra keys set HF_API_KEY' or add "
172
+ "HF_API_KEY to your .env file. "
157
173
  "Get a token at: https://huggingface.co/settings/tokens"
158
174
  )
159
175
 
@@ -7,6 +7,8 @@ import typer
7
7
 
8
8
  from acra.graph.workflow import OmniAgentCallbacks, omniagent_graph
9
9
  from acra.ui.components import render_panel
10
+ from acra.ui.spinner import run_with_spinner
11
+ from acra.utils.output_formatter import format_task_result
10
12
 
11
13
  app = typer.Typer(help="Agent task commands for acra.")
12
14
 
@@ -35,8 +37,13 @@ def _run_task(
35
37
  "callbacks": [OmniAgentCallbacks()],
36
38
  "configurable": {"thread_id": thread_id},
37
39
  }
38
- result = omniagent_graph.invoke(state, config=config)
39
- render_panel(result, title=task_label)
40
+ result = run_with_spinner(
41
+ omniagent_graph.invoke,
42
+ state,
43
+ config=config,
44
+ message=f"Running {task_label}",
45
+ )
46
+ render_panel(format_task_result(result), title=task_label)
40
47
  return result
41
48
 
42
49
 
@@ -16,7 +16,11 @@ app = typer.Typer(help="Key management commands for acra.")
16
16
 
17
17
  @app.command("set")
18
18
  def set_key_cmd(
19
- name: str = typer.Argument(..., help="Key name, e.g. provider or research.github"),
19
+ name: str = typer.Argument(
20
+ ...,
21
+ help="Key name: GEMINI_API_KEY, OPENAI_API_KEY, GROQ_API_KEY, HF_API_KEY, "
22
+ "or research.<source> (web, github, docs, arxiv).",
23
+ ),
20
24
  value: Optional[str] = typer.Argument(None, help="API key value. If omitted, you will be prompted."),
21
25
  ):
22
26
  """Set a provider or research API key."""
@@ -37,4 +41,4 @@ def list_key_cmd():
37
41
  def delete_key_cmd(name: str = typer.Argument(..., help="Key name to delete.")):
38
42
  """Delete a configured API key."""
39
43
  delete_key(name)
40
- render_panel(f"Deleted key: {name}")
44
+ render_panel(f"Deleted key: {name}")
@@ -10,6 +10,7 @@ from acra.config.profile_manager import ProfileManager
10
10
  from acra.utils.output_formatter import format_research_report
11
11
  from acra.utils.keyring_manager import get_research_key_status
12
12
  from acra.ui.components import render_panel
13
+ from acra.ui.spinner import run_with_spinner
13
14
 
14
15
  app = typer.Typer(help="Research commands for acra.")
15
16
 
@@ -55,12 +56,14 @@ def research(
55
56
  }
56
57
 
57
58
  thread_id = str(uuid.uuid4())
58
- result = workflow.invoke(
59
+ result = run_with_spinner(
60
+ workflow.invoke,
59
61
  state,
60
62
  config={
61
63
  "callbacks": [OmniAgentCallbacks()],
62
64
  "configurable": {"thread_id": thread_id},
63
65
  },
66
+ message="Researching",
64
67
  )
65
68
 
66
69
  report = format_research_report(result, output_format=format)
@@ -10,6 +10,16 @@ CONFIG_DIR = Path.home() / ".acra"
10
10
  CONFIG_FILE = CONFIG_DIR / "config.json"
11
11
  DEFAULT_PROFILE_NAME = "default"
12
12
 
13
+ # Maps provider names to the credential name acra/agents/llm.py actually
14
+ # reads via keyring_manager.get_key(). Ollama is local and needs no key.
15
+ _PROVIDER_KEY_NAMES = {
16
+ "gemini": "GEMINI_API_KEY",
17
+ "openai": "OPENAI_API_KEY",
18
+ "groq": "GROQ_API_KEY",
19
+ "huggingface_cloud": "HF_API_KEY",
20
+ "huggingface_local": "HF_API_KEY",
21
+ }
22
+
13
23
 
14
24
  def _ensure_config_dir() -> None:
15
25
  CONFIG_DIR.mkdir(parents=True, exist_ok=True)
@@ -69,19 +79,31 @@ class ProfileManager:
69
79
  api_key = getpass("Provider API key (hidden): ")
70
80
  theme = input("Theme (dark-orange/dark-blue/dark-green): ").strip() or "dark-orange"
71
81
  workspace = input("Workspace path (default=current directory): ").strip() or os.getcwd()
72
- research_keys = {}
73
82
  print("Configure research keys. Press enter to skip a source.")
83
+ research_key_values = {}
74
84
  for source in ["web", "github", "docs", "arxiv"]:
75
85
  prompt = f"Research {source} API key (if required): "
76
86
  value = getpass(prompt) if source != "arxiv" else input(prompt).strip()
77
- research_keys[source] = value or None
87
+ if value:
88
+ research_key_values[source] = value
89
+
90
+ # Secrets are stored via the OS keyring (falling back to a
91
+ # permission-locked local file when no OS backend is available),
92
+ # never written into the plaintext profile JSON. See
93
+ # acra/utils/keyring_manager.py.
94
+ from acra.utils.keyring_manager import set_key
95
+
96
+ provider_key_name = _PROVIDER_KEY_NAMES.get(provider)
97
+ if api_key and provider_key_name:
98
+ set_key(provider_key_name, api_key)
99
+ for source, value in research_key_values.items():
100
+ set_key(f"research.{source}", value)
78
101
 
79
102
  profile = {
80
103
  "provider": provider,
81
104
  "model": model,
82
105
  "theme": theme,
83
106
  "workspace": workspace,
84
- "research_keys": research_keys,
85
107
  }
86
108
  self.save_profile(profile_name, profile)
87
- return profile_name
109
+ return profile_name
@@ -0,0 +1,78 @@
1
+ """Shared UI render components for acra."""
2
+
3
+ from rich.console import Console, Group
4
+ from rich.panel import Panel
5
+ from rich.table import Table
6
+ from rich.text import Text
7
+ from typing import Any
8
+
9
+ console = Console()
10
+
11
+ _MAX_FIELD_LENGTH = 2000
12
+
13
+
14
+ def _stringify(value: Any) -> str:
15
+ if isinstance(value, list) and value and all(isinstance(v, str) for v in value):
16
+ return "\n".join(f"\u2022 {v}" for v in value)
17
+ if isinstance(value, (list, tuple)):
18
+ return ", ".join(str(v) for v in value)
19
+ if isinstance(value, dict):
20
+ return "\n".join(f"{k}: {v}" for k, v in value.items())
21
+ return str(value)
22
+
23
+
24
+ def _render_dict(data: dict) -> Group:
25
+ """Render a dict as a lead answer (if present) plus a clean field table,
26
+ instead of a raw Python repr. Built for the summaries produced by
27
+ acra.utils.output_formatter, but degrades gracefully for any dict."""
28
+ renderables = []
29
+
30
+ answer = data.get("answer")
31
+ if answer:
32
+ renderables.append(Text(str(answer)))
33
+ renderables.append(Text(""))
34
+
35
+ table = Table(show_header=False, box=None, padding=(0, 1, 0, 0))
36
+ table.add_column("Field", style="bold cyan", no_wrap=True)
37
+ table.add_column("Value")
38
+ for key, value in data.items():
39
+ if key == "answer" or value in (None, "", [], {}):
40
+ continue
41
+ value_str = _stringify(value)
42
+ if len(value_str) > _MAX_FIELD_LENGTH:
43
+ value_str = value_str[:_MAX_FIELD_LENGTH] + "\u2026 (truncated)"
44
+ table.add_row(key.replace("_", " ").title(), value_str)
45
+
46
+ if table.rows:
47
+ renderables.append(table)
48
+
49
+ if not renderables:
50
+ renderables.append(Text("(empty)"))
51
+
52
+ return Group(*renderables)
53
+
54
+
55
+ def render_panel(content: Any, title: str = "acra") -> None:
56
+ if isinstance(content, list):
57
+ table = Table(show_header=True, header_style="bold magenta")
58
+ table.add_column("Name")
59
+ table.add_column("Status")
60
+ table.add_column("Value")
61
+ for item in content:
62
+ if isinstance(item, dict):
63
+ table.add_row(
64
+ str(item.get("name", "")),
65
+ str(item.get("status", "")),
66
+ str(item.get("value", "")),
67
+ )
68
+ else:
69
+ table.add_row(str(item), "", "")
70
+ console.print(Panel(table, title=title))
71
+ elif isinstance(content, dict):
72
+ console.print(Panel(_render_dict(content), title=title))
73
+ else:
74
+ console.print(Panel(str(content), title=title))
75
+
76
+
77
+ def render_message(message: str) -> None:
78
+ console.print(message)
@@ -70,21 +70,17 @@ def launch_shell(profile: str = None, workspace: str = None, party: bool = False
70
70
  console.print(f"[dim]→[/dim] {user_input}")
71
71
 
72
72
  cmd = [sys.executable, "-m", "acra"] + shlex.split(command_text)
73
+ # stdout/stderr are inherited (not captured) so the child process
74
+ # sees a real tty and any live output -- the run_with_spinner
75
+ # progress indicator, the animated banner, streamed agent output
76
+ # -- renders in real time instead of being buffered up and
77
+ # dumped all at once after the command finishes.
73
78
  completed = subprocess.run(
74
79
  cmd,
75
80
  cwd=os.getcwd(),
76
- stdin=sys.stdin,
77
- stdout=subprocess.PIPE,
78
- stderr=subprocess.STDOUT,
79
- text=True,
80
81
  )
81
- if completed.stdout:
82
- console.print(completed.stdout, soft_wrap=True)
83
82
  if completed.returncode != 0:
84
- if completed.stdout and "Missing command" in completed.stdout:
85
- console.print("[dim]Tip:[/dim] try a subcommand such as 'brain models' or 'config init'.")
86
- else:
87
- console.print(f"[bold red]Command failed with exit code {completed.returncode}.[/bold red]")
83
+ console.print(f"[bold red]Command failed with exit code {completed.returncode}.[/bold red]")
88
84
  except KeyboardInterrupt:
89
85
  break
90
86
  except EOFError:
@@ -0,0 +1,97 @@
1
+ """Live 'running' spinner for long agent tasks.
2
+
3
+ `acra ask/build/fix/review/explain/run` can take anywhere from a few
4
+ seconds to a couple of minutes (planner -> researcher -> coder -> executor
5
+ -> critic, each an LLM call), and until now the terminal went completely
6
+ silent between hitting enter and the result panel appearing. This wraps a
7
+ blocking call with the same braille spinner used in the animated startup
8
+ banner (acra/ui/animated_banner.py), showing live progress and elapsed
9
+ time instead.
10
+
11
+ Kept as a small, self-contained module (rather than importing the
12
+ startup-banner internals) so a bug here can't affect the startup banner
13
+ and vice versa; the animation/color gating logic is intentionally
14
+ duplicated in miniature from animated_banner.py, honoring the same
15
+ conventions (NO_COLOR, FORCE_COLOR, TERM=dumb, CI, ACRA_NO_ANIMATION).
16
+ """
17
+
18
+ import os
19
+ import sys
20
+ import threading
21
+ import time
22
+ from typing import Any, Callable
23
+
24
+ RESET = "\033[0m"
25
+ DIM = "\033[2m"
26
+ CYAN = "\033[36m"
27
+ GREEN = "\033[92m"
28
+
29
+ _SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
30
+
31
+
32
+ def _animations_enabled() -> bool:
33
+ if os.environ.get("ACRA_NO_ANIMATION"):
34
+ return False
35
+ if os.environ.get("CI"):
36
+ return False
37
+ return sys.stdout.isatty()
38
+
39
+
40
+ def _supports_color() -> bool:
41
+ if os.environ.get("FORCE_COLOR", "") != "":
42
+ return True
43
+ if os.environ.get("NO_COLOR", "") != "":
44
+ return False
45
+ if os.environ.get("TERM") == "dumb":
46
+ return False
47
+ return True
48
+
49
+
50
+ def run_with_spinner(fn: Callable[..., Any], *args, message: str = "Working", **kwargs) -> Any:
51
+ """Run fn(*args, **kwargs) on a background thread, showing a live spinner
52
+ with elapsed time until it finishes. Returns fn's return value, or
53
+ re-raises whatever exception fn raised.
54
+
55
+ Falls back to a single static line and a plain blocking call when
56
+ stdout isn't a real interactive terminal (piped, CI, ACRA_NO_ANIMATION),
57
+ so scripted/piped invocations pay no animation cost and never see
58
+ raw escape codes leaking into logs.
59
+ """
60
+ if not _animations_enabled():
61
+ print(f"{message}...")
62
+ return fn(*args, **kwargs)
63
+
64
+ outcome: dict = {}
65
+
66
+ def _target():
67
+ try:
68
+ outcome["value"] = fn(*args, **kwargs)
69
+ except BaseException as exc: # re-raised on the main thread below
70
+ outcome["error"] = exc
71
+
72
+ thread = threading.Thread(target=_target, daemon=True)
73
+ thread.start()
74
+
75
+ color = CYAN if _supports_color() else ""
76
+ reset = RESET if _supports_color() else ""
77
+ dim = DIM if _supports_color() else ""
78
+ green = GREEN if _supports_color() else ""
79
+
80
+ start = time.time()
81
+ frame_idx = 0
82
+ while thread.is_alive():
83
+ frame = _SPINNER_FRAMES[frame_idx % len(_SPINNER_FRAMES)]
84
+ elapsed = time.time() - start
85
+ sys.stdout.write(f"\r{color}{frame}{reset} {dim}{message}\u2026 {elapsed:0.1f}s{reset}\033[K")
86
+ sys.stdout.flush()
87
+ time.sleep(0.08)
88
+ frame_idx += 1
89
+ thread.join()
90
+
91
+ elapsed = time.time() - start
92
+ sys.stdout.write(f"\r{green}\u2713{reset} {message} ({elapsed:.1f}s){' ' * 12}\n")
93
+ sys.stdout.flush()
94
+
95
+ if "error" in outcome:
96
+ raise outcome["error"]
97
+ return outcome.get("value")