forgex-cli 0.1.3__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 (198) hide show
  1. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/PKG-INFO +1 -1
  2. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/__init__.py +1 -1
  3. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/agents/architect.py +2 -0
  4. forgex_cli-0.1.5/src/forgex/cli/commands/config.py +424 -0
  5. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/plan.py +25 -8
  6. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/intent.py +1 -1
  7. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/repl.py +4 -0
  8. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/config/loader.py +3 -1
  9. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/models/factory.py +18 -1
  10. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/models/openai_compatible.py +3 -1
  11. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_packaging.py +2 -1
  12. forgex_cli-0.1.3/src/forgex/cli/commands/config.py +0 -295
  13. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/.github/workflows/ci.yml +0 -0
  14. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/.github/workflows/release.yml +0 -0
  15. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/.gitignore +0 -0
  16. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/CHANGELOG.md +0 -0
  17. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/LICENSE +0 -0
  18. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/README.md +0 -0
  19. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/SECURITY.md +0 -0
  20. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/docs/agents.md +0 -0
  21. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/docs/architecture.md +0 -0
  22. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/docs/configuration.md +0 -0
  23. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/docs/decisions.md +0 -0
  24. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/docs/providers.md +0 -0
  25. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/docs/quickstart.md +0 -0
  26. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/docs/sandbox.md +0 -0
  27. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/docs/security.md +0 -0
  28. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/examples/demo-project/README.md +0 -0
  29. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/examples/demo-project/pyproject.toml +0 -0
  30. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/examples/demo-project/src/demo/__init__.py +0 -0
  31. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/examples/demo-project/src/demo/calc.py +0 -0
  32. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/examples/demo-project/tests/test_calc.py +0 -0
  33. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/pyproject.toml +0 -0
  34. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/__main__.py +0 -0
  35. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/agents/__init__.py +0 -0
  36. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/agents/base.py +0 -0
  37. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/agents/builder.py +0 -0
  38. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/agents/ensemble.py +0 -0
  39. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/agents/expert_coder.py +0 -0
  40. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/agents/supervisor.py +0 -0
  41. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/__init__.py +0 -0
  42. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/app.py +0 -0
  43. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/build_views.py +0 -0
  44. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/__init__.py +0 -0
  45. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/build.py +0 -0
  46. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/checkpoint.py +0 -0
  47. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/context.py +0 -0
  48. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/diff.py +0 -0
  49. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/do_cmd.py +0 -0
  50. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/doctor.py +0 -0
  51. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/fix.py +0 -0
  52. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/init.py +0 -0
  53. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/logs.py +0 -0
  54. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/models.py +0 -0
  55. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/review.py +0 -0
  56. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/rollback.py +0 -0
  57. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/run.py +0 -0
  58. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/status.py +0 -0
  59. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/supervise.py +0 -0
  60. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/task.py +0 -0
  61. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/tools.py +0 -0
  62. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/commands/verify.py +0 -0
  63. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/context_memory.py +0 -0
  64. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/progress.py +0 -0
  65. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/runtime.py +0 -0
  66. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/session.py +0 -0
  67. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/cli/ui.py +0 -0
  68. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/config/__init__.py +0 -0
  69. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/config/schema.py +0 -0
  70. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/context/__init__.py +0 -0
  71. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/context/chunker.py +0 -0
  72. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/context/dependency_graph.py +0 -0
  73. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/context/indexer.py +0 -0
  74. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/context/manager.py +0 -0
  75. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/context/retriever.py +0 -0
  76. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/models/__init__.py +0 -0
  77. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/models/base.py +0 -0
  78. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/models/capabilities.py +0 -0
  79. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/models/groq.py +0 -0
  80. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/models/local.py +0 -0
  81. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/models/registry.py +0 -0
  82. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/models/router.py +0 -0
  83. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/orchestration/__init__.py +0 -0
  84. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/orchestration/checkpoints.py +0 -0
  85. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/orchestration/engine.py +0 -0
  86. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/orchestration/events.py +0 -0
  87. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/orchestration/lockfile.py +0 -0
  88. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/orchestration/retry.py +0 -0
  89. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/orchestration/state.py +0 -0
  90. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/orchestration/stores.py +0 -0
  91. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/orchestration/workflow.py +0 -0
  92. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/project.py +0 -0
  93. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/prompts/__init__.py +0 -0
  94. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/prompts/architect.md +0 -0
  95. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/prompts/builder.md +0 -0
  96. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/prompts/expert_coder.md +0 -0
  97. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/prompts/reviewer.md +0 -0
  98. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/prompts/supervisor.md +0 -0
  99. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/research/__init__.py +0 -0
  100. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/research/citations.py +0 -0
  101. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/research/engine.py +0 -0
  102. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/research/sources.py +0 -0
  103. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/sandbox/__init__.py +0 -0
  104. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/sandbox/executor.py +0 -0
  105. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/sandbox/permissions.py +0 -0
  106. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/sandbox/policies.py +0 -0
  107. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/schemas/__init__.py +0 -0
  108. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/schemas/agent.py +0 -0
  109. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/schemas/checkpoint.py +0 -0
  110. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/schemas/error.py +0 -0
  111. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/schemas/event.py +0 -0
  112. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/schemas/plan.py +0 -0
  113. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/schemas/project.py +0 -0
  114. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/schemas/research.py +0 -0
  115. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/schemas/result.py +0 -0
  116. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/schemas/task.py +0 -0
  117. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/storage/__init__.py +0 -0
  118. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/storage/database.py +0 -0
  119. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/storage/migrations.py +0 -0
  120. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/storage/repositories.py +0 -0
  121. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/tools/__init__.py +0 -0
  122. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/tools/base.py +0 -0
  123. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/tools/dependencies.py +0 -0
  124. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/tools/filesystem.py +0 -0
  125. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/tools/git.py +0 -0
  126. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/tools/project.py +0 -0
  127. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/tools/search.py +0 -0
  128. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/tools/terminal.py +0 -0
  129. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/tools/testing.py +0 -0
  130. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/tools/verification.py +0 -0
  131. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/utils/__init__.py +0 -0
  132. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/utils/logging.py +0 -0
  133. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/utils/paths.py +0 -0
  134. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/src/forgex/utils/redact.py +0 -0
  135. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/__init__.py +0 -0
  136. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/conftest.py +0 -0
  137. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/__init__.py +0 -0
  138. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_agents_architect.py +0 -0
  139. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_builder_agent.py +0 -0
  140. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_checkpoints.py +0 -0
  141. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_cli.py +0 -0
  142. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_cli_build.py +0 -0
  143. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_cli_checkpoint.py +0 -0
  144. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_cli_do.py +0 -0
  145. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_cli_models.py +0 -0
  146. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_cli_plan.py +0 -0
  147. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_cli_review.py +0 -0
  148. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_cli_surface.py +0 -0
  149. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_cli_task.py +0 -0
  150. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_cli_tools.py +0 -0
  151. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_cli_verify.py +0 -0
  152. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_config.py +0 -0
  153. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_config_env.py +0 -0
  154. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_config_setup.py +0 -0
  155. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_context_agents.py +0 -0
  156. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_context_manager.py +0 -0
  157. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_context_memory.py +0 -0
  158. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_engine.py +0 -0
  159. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_ensemble.py +0 -0
  160. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_events.py +0 -0
  161. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_executor.py +0 -0
  162. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_expert_coder.py +0 -0
  163. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_fix.py +0 -0
  164. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_indexer.py +0 -0
  165. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_init.py +0 -0
  166. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_intent.py +0 -0
  167. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_large_repo.py +0 -0
  168. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_offline.py +0 -0
  169. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_paths.py +0 -0
  170. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_permissions.py +0 -0
  171. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_plan_schemas.py +0 -0
  172. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_policies.py +0 -0
  173. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_project.py +0 -0
  174. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_providers.py +0 -0
  175. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_recovery.py +0 -0
  176. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_redact.py +0 -0
  177. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_registry.py +0 -0
  178. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_repl.py +0 -0
  179. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_research.py +0 -0
  180. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_retriever.py +0 -0
  181. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_retry.py +0 -0
  182. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_router.py +0 -0
  183. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_run_cli.py +0 -0
  184. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_runtime.py +0 -0
  185. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_security.py +0 -0
  186. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_session.py +0 -0
  187. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_state.py +0 -0
  188. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_storage.py +0 -0
  189. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_stores.py +0 -0
  190. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_supervisor.py +0 -0
  191. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_tasks.py +0 -0
  192. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_tools_dependencies.py +0 -0
  193. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_tools_filesystem.py +0 -0
  194. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_tools_git.py +0 -0
  195. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_tools_search.py +0 -0
  196. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_tools_terminal.py +0 -0
  197. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_verification.py +0 -0
  198. {forgex_cli-0.1.3 → forgex_cli-0.1.5}/tests/unit/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: forgex-cli
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: FORGEX — multi-agent AI software engineering. Plan. Build. Refine. Verify.
5
5
  Project-URL: Homepage, https://github.com/B-max12/Forgex
6
6
  Project-URL: Repository, https://github.com/B-max12/Forgex
@@ -1,3 +1,3 @@
1
1
  """FORGEX — Plan. Build. Refine. Verify."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.1.5"
@@ -67,6 +67,8 @@ class ArchitectAgent(BaseAgent):
67
67
  brief += "\n\nSymbols:\n" + symbols
68
68
  except Exception:
69
69
  logger.warning("symbol map unavailable", exc_info=True)
70
+ if len(brief) > 16_000:
71
+ brief = brief[:16_000] + "\n... [context truncated to stay within token budget]"
70
72
  return brief
71
73
 
72
74
  async def plan(
@@ -0,0 +1,424 @@
1
+ """`forgex config` — view and modify layered configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Annotated, Any
9
+
10
+ import typer
11
+ from rich.prompt import Confirm, Prompt
12
+
13
+ from forgex.cli.ui import JsonFlag, PlainFlag, ui_from
14
+ from forgex.config import (
15
+ CONFIG_FILE_NAME,
16
+ global_config_path,
17
+ load_config,
18
+ project_config_path,
19
+ set_config_value,
20
+ store_secret,
21
+ )
22
+ from forgex.schemas.error import ConfigError
23
+ from forgex.utils.paths import find_project_root
24
+
25
+ app = typer.Typer(no_args_is_help=True, help="View and modify FORGEX configuration.")
26
+
27
+ GlobalFlag = Annotated[
28
+ bool,
29
+ typer.Option("--global", "-g", help="Operate on ~/.forgex/config.toml instead of project config."),
30
+ ]
31
+
32
+
33
+ def _target_path(global_: bool) -> Path:
34
+ if global_:
35
+ return global_config_path()
36
+ root = find_project_root()
37
+ if root is None:
38
+ raise ConfigError("Not inside a FORGEX project. Use --global to edit global configuration.")
39
+ return project_config_path(root)
40
+
41
+
42
+ def _get_dotted(data: dict[str, Any], dotted: str) -> Any:
43
+ node: Any = data
44
+ for part in dotted.split("."):
45
+ if not isinstance(node, dict) or part not in node:
46
+ raise ConfigError(f"Configuration key '{dotted}' not found")
47
+ node = node[part]
48
+ return node
49
+
50
+
51
+ @app.command("show")
52
+ def show_command(
53
+ ctx: typer.Context,
54
+ json_out: JsonFlag = False,
55
+ plain: PlainFlag = False,
56
+ ) -> None:
57
+ """Show the currently active merged configuration."""
58
+ ui = ui_from(ctx, json_out=json_out, plain=plain)
59
+ root = find_project_root()
60
+ config = load_config(root)
61
+ payload = config.model_dump(mode="json", exclude_none=True)
62
+ ui.data(payload)
63
+ if not json_out and not ctx.obj.get("json"):
64
+ ui.text(json.dumps(payload, indent=2))
65
+
66
+
67
+ @app.command("get")
68
+ def get_command(
69
+ ctx: typer.Context,
70
+ key: Annotated[str, typer.Argument(help="Dotted configuration key, e.g. workflow.max_iterations")],
71
+ json_out: JsonFlag = False,
72
+ plain: PlainFlag = False,
73
+ ) -> None:
74
+ """Get a configuration value by dotted key."""
75
+ ui = ui_from(ctx, json_out=json_out, plain=plain)
76
+ root = find_project_root()
77
+ config = load_config(root)
78
+ val = _get_dotted(config.model_dump(mode="json"), key)
79
+ ui.data({"key": key, "value": val})
80
+ if not json_out and not ctx.obj.get("json"):
81
+ ui.text(str(val))
82
+
83
+
84
+ @app.command("set")
85
+ def set_command(
86
+ ctx: typer.Context,
87
+ key: Annotated[str, typer.Argument(help="Dotted configuration key, e.g. models.architect.model")],
88
+ value: Annotated[str, typer.Argument(help="New value")],
89
+ global_: GlobalFlag = False,
90
+ json_out: JsonFlag = False,
91
+ plain: PlainFlag = False,
92
+ ) -> None:
93
+ """Set a configuration value in project (or global) config.toml."""
94
+ ui = ui_from(ctx, json_out=json_out, plain=plain)
95
+ path = _target_path(global_)
96
+ set_config_value(path, key, value)
97
+ ui.data({"key": key, "value": value, "path": str(path)})
98
+ ui.ok(f"Set {key} = {value} in {path}")
99
+
100
+
101
+ @dataclass
102
+ class ProviderSetupInfo:
103
+ name: str
104
+ type: str = "openai_compatible"
105
+ base_url: str | None = None
106
+ api_key_env: str | None = None
107
+ api_key: str | None = None
108
+ timeout_seconds: float = 120.0
109
+
110
+
111
+ @dataclass
112
+ class SetupAnswers:
113
+ provider: str | None = None
114
+ base_url: str | None = None
115
+ api_key_env: str | None = None
116
+ api_key: str | None = None
117
+ models: dict[str, str] = field(default_factory=dict)
118
+ providers: list[ProviderSetupInfo] = field(default_factory=list)
119
+ role_bindings: dict[str, tuple[str, str]] = field(default_factory=dict)
120
+ sandbox_enabled: bool = False
121
+ approval_mode: str = "ask"
122
+
123
+
124
+ def apply_setup(answers: SetupAnswers, *, global_dir: Path) -> None:
125
+ import os
126
+ path = global_dir / CONFIG_FILE_NAME
127
+
128
+ providers = list(answers.providers)
129
+ if answers.provider:
130
+ providers.append(
131
+ ProviderSetupInfo(
132
+ name=answers.provider,
133
+ base_url=answers.base_url,
134
+ api_key_env=answers.api_key_env,
135
+ api_key=answers.api_key,
136
+ )
137
+ )
138
+
139
+ for p in providers:
140
+ set_config_value(path, f"providers.{p.name}.type", p.type)
141
+ if p.base_url:
142
+ set_config_value(path, f"providers.{p.name}.base_url", p.base_url)
143
+ if p.api_key_env:
144
+ set_config_value(path, f"providers.{p.name}.api_key_env", p.api_key_env)
145
+ set_config_value(path, f"providers.{p.name}.timeout_seconds", p.timeout_seconds)
146
+
147
+ if p.api_key and p.api_key_env:
148
+ store_secret(p.api_key_env, p.api_key, global_dir=global_dir)
149
+ os.environ[p.api_key_env] = p.api_key
150
+
151
+ role_bindings = dict(answers.role_bindings)
152
+ if answers.models:
153
+ for r, m in answers.models.items():
154
+ role_bindings[r] = (answers.provider or "groq", str(m))
155
+
156
+ for role, (prov, model) in role_bindings.items():
157
+ set_config_value(path, f"models.{role}.provider", prov)
158
+ set_config_value(path, f"models.{role}.model", str(model))
159
+
160
+ set_config_value(path, "sandbox.enabled", "true" if answers.sandbox_enabled else "false")
161
+ set_config_value(path, "permissions.mode", answers.approval_mode)
162
+
163
+
164
+ def _test_provider_connectivity(provider: str, base_url: str | None, api_key: str | None) -> tuple[bool, str]:
165
+ """Perform a lightweight test request to verify API connectivity."""
166
+ import httpx
167
+
168
+ url = (base_url or ("https://api.groq.com/openai/v1" if provider == "groq" else "https://api.openai.com/v1")).rstrip("/") + "/models"
169
+ headers = {}
170
+ if api_key:
171
+ headers["Authorization"] = f"Bearer {api_key}"
172
+ try:
173
+ with httpx.Client(timeout=5.0) as client:
174
+ resp = client.get(url, headers=headers)
175
+ if resp.status_code in (200, 201):
176
+ return True, "Connection verified (HTTP 200)"
177
+ return False, f"Provider returned HTTP {resp.status_code}"
178
+ except Exception as exc:
179
+ return False, str(exc)
180
+
181
+
182
+ def _strip_quotes(value: str) -> str:
183
+ """Strip surrounding single or double quotes from user input."""
184
+ v = value.strip()
185
+ if len(v) >= 2 and v[0] == v[-1] and v[0] in ("'", '"'):
186
+ return v[1:-1]
187
+ return v
188
+
189
+
190
+ def _fetch_models(base_url: str, api_key: str | None) -> list[str]:
191
+ """Fetch the list of available model IDs from an OpenAI-compatible /models endpoint."""
192
+ import httpx
193
+
194
+ url = base_url.rstrip("/") + "/models"
195
+ headers: dict[str, str] = {}
196
+ if api_key:
197
+ headers["Authorization"] = f"Bearer {api_key}"
198
+ try:
199
+ with httpx.Client(timeout=8.0) as client:
200
+ resp = client.get(url, headers=headers)
201
+ if resp.status_code == 200:
202
+ data = resp.json()
203
+ models = data.get("data", [])
204
+ return sorted([m["id"] for m in models if isinstance(m, dict) and "id" in m])
205
+ except Exception:
206
+ pass
207
+ return []
208
+
209
+
210
+ @app.command("setup")
211
+ def setup_command(
212
+ ctx: typer.Context,
213
+ ) -> None:
214
+ """Interactive setup wizard for provider credentials and defaults."""
215
+ import os
216
+ from rich.console import Console
217
+ from rich.table import Table
218
+
219
+ console = Console()
220
+ ui = ui_from(ctx)
221
+ ui.text("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
222
+ ui.text("FORGEX Configuration Setup Wizard — Welcome!")
223
+ ui.text("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
224
+ ui.text("Security & Terms Notice:")
225
+ ui.text(" • FORGEX operates locally and executes developer tools.")
226
+ ui.text(" • Reversible checkpoints are created before file edits.")
227
+ ui.text(" • API keys are stored with restricted permissions (0600) in ~/.forgex/.env.")
228
+ ui.text(" • You control permissions via mode: ask (default), auto, or safe.\n")
229
+
230
+ # Step 1: Up to 4 providers sequentially
231
+ configured_providers: list[ProviderSetupInfo] = []
232
+ g_dir = global_config_path().parent
233
+
234
+ ui.text("[bold]Step 1: Provider Setup[/bold] — Configure 1 to 4 providers (Groq, NVIDIA, OpenAI, OpenRouter, Local, Custom)")
235
+
236
+ for idx in range(1, 5):
237
+ if idx > 1:
238
+ ui.text("")
239
+ if not Confirm.ask(f"Add another provider? ({idx}/4)", default=False):
240
+ break
241
+
242
+ ui.text(f"\n[cyan]── Provider {idx}/4 ──[/cyan]")
243
+ default_choice = "groq" if idx == 1 else ("nvidia" if idx == 2 else "openai")
244
+ prov = Prompt.ask(
245
+ f"Choose provider #{idx}",
246
+ choices=["groq", "nvidia", "openai", "openrouter", "local", "custom"],
247
+ default=default_choice,
248
+ )
249
+
250
+ name = prov
251
+ ptype = "openai_compatible"
252
+ base_url = ""
253
+ api_key_env: str | None = None
254
+ api_key: str | None = None
255
+
256
+ if prov == "groq":
257
+ name = "groq" if not any(p.name == "groq" for p in configured_providers) else f"groq_{idx}"
258
+ ptype = "groq"
259
+ base_url = "https://api.groq.com/openai/v1"
260
+ api_key_env = "FORGEX_GROQ_API_KEY"
261
+ api_key = _strip_quotes(Prompt.ask(f"Enter Groq API key for {name} (gsk_...)", password=True, default=""))
262
+ elif prov == "nvidia":
263
+ name = "nvidia" if not any(p.name == "nvidia" for p in configured_providers) else f"nvidia_{idx}"
264
+ ptype = "openai_compatible"
265
+ base_url = "https://integrate.api.nvidia.com/v1"
266
+ api_key_env = "FORGEX_NVIDIA_API_KEY"
267
+ api_key = _strip_quotes(Prompt.ask(f"Enter NVIDIA API key for {name} (nvapi-...)", password=True, default=""))
268
+ elif prov == "openai":
269
+ name = "openai_compatible" if not any(p.name == "openai_compatible" for p in configured_providers) else f"openai_{idx}"
270
+ ptype = "openai_compatible"
271
+ base_url = "https://api.openai.com/v1"
272
+ api_key_env = "FORGEX_OPENAI_API_KEY"
273
+ api_key = _strip_quotes(Prompt.ask(f"Enter OpenAI API key for {name} (sk-...)", password=True, default=""))
274
+ elif prov == "openrouter":
275
+ name = "openrouter" if not any(p.name == "openrouter" for p in configured_providers) else f"openrouter_{idx}"
276
+ ptype = "openai_compatible"
277
+ base_url = "https://openrouter.ai/api/v1"
278
+ api_key_env = "FORGEX_OPENROUTER_API_KEY"
279
+ api_key = _strip_quotes(Prompt.ask(f"Enter OpenRouter API key for {name} (sk-or-...)", password=True, default=""))
280
+ elif prov == "local":
281
+ name = "local" if not any(p.name == "local" for p in configured_providers) else f"local_{idx}"
282
+ ptype = "local"
283
+ base_url = _strip_quotes(Prompt.ask("Enter local OpenAI-compatible endpoint", default="http://localhost:11434/v1"))
284
+ api_key_env = None
285
+ api_key = None
286
+ else: # custom
287
+ name = _strip_quotes(Prompt.ask("Enter custom provider name", default=f"custom_{idx}"))
288
+ ptype = "openai_compatible"
289
+ base_url = _strip_quotes(Prompt.ask("Enter base URL", default="http://localhost:8080/v1"))
290
+ api_key_env = f"FORGEX_{name.upper()}_API_KEY"
291
+ api_key = _strip_quotes(Prompt.ask("Enter API key (if required)", password=True, default=""))
292
+
293
+ # Test connectivity
294
+ ui.text(f"Testing connectivity to {name}...")
295
+ ok, detail = _test_provider_connectivity(name, base_url, api_key)
296
+ if ok:
297
+ ui.ok(f"✓ {name} connection verified: {detail}")
298
+ else:
299
+ ui.warn(f"⚠ {name} test: {detail} (proceeding)")
300
+
301
+ # Persist secret immediately
302
+ if api_key and api_key_env:
303
+ store_secret(api_key_env, api_key, global_dir=g_dir)
304
+ os.environ[api_key_env] = api_key
305
+
306
+ configured_providers.append(
307
+ ProviderSetupInfo(
308
+ name=name,
309
+ type=ptype,
310
+ base_url=base_url,
311
+ api_key_env=api_key_env,
312
+ api_key=api_key,
313
+ timeout_seconds=120.0,
314
+ )
315
+ )
316
+
317
+ # Step 2: Fetch models from all configured providers
318
+ ui.text("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
319
+ ui.text("[bold]Step 2: Model Discovery[/bold] — Fetching models from all providers...")
320
+ ui.text("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
321
+
322
+ all_models: list[tuple[str, str]] = [] # (provider_name, model_id)
323
+
324
+ for cp in configured_providers:
325
+ eff_url = cp.base_url or ("https://api.groq.com/openai/v1" if cp.type == "groq" else "")
326
+ if eff_url:
327
+ ui.text(f"Querying models from {cp.name} ({eff_url})...")
328
+ models = _fetch_models(eff_url, cp.api_key)
329
+ if models:
330
+ ui.ok(f"✓ Found {len(models)} models from {cp.name}")
331
+ for m in models:
332
+ all_models.append((cp.name, m))
333
+ else:
334
+ ui.warn(f"Could not automatically fetch models from {cp.name}")
335
+
336
+ if all_models:
337
+ table = Table(title="Available Models Catalog (All Providers)", show_header=True, header_style="bold cyan", border_style="dim")
338
+ table.add_column("#", style="dim", width=5)
339
+ table.add_column("Provider", style="magenta", width=14)
340
+ table.add_column("Model ID", style="green")
341
+ for i, (p_name, m_id) in enumerate(all_models, 1):
342
+ table.add_row(str(i), p_name, m_id)
343
+ console.print(table)
344
+ else:
345
+ ui.warn("No online models could be discovered automatically. You can specify model names manually.")
346
+
347
+ # Step 3: Sequential agent model selection
348
+ ui.text("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
349
+ ui.text("[bold]Step 3: Agent Model Selection[/bold] — Select model for each agent role in sequence")
350
+ ui.text("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
351
+
352
+ def _resolve_selection(role_label: str, default_val: tuple[str, str] | None) -> tuple[str, str]:
353
+ fallback_default = "llama-3.3-70b-versatile" if (configured_providers and configured_providers[0].type == "groq") else "gpt-4o-mini"
354
+ def_prompt = "1" if all_models else fallback_default
355
+ if default_val:
356
+ if default_val in all_models:
357
+ def_prompt = str(all_models.index(default_val) + 1)
358
+ else:
359
+ def_prompt = default_val[1]
360
+
361
+ prompt_str = f"Select model for {role_label}"
362
+ if all_models:
363
+ prompt_str += f" (enter # 1-{len(all_models)} or type name)"
364
+ else:
365
+ prompt_str += " (type model name)"
366
+
367
+ user_input = Prompt.ask(prompt_str, default=def_prompt).strip()
368
+ user_input = _strip_quotes(user_input)
369
+
370
+ if all_models:
371
+ try:
372
+ num = int(user_input)
373
+ if 1 <= num <= len(all_models):
374
+ chosen = all_models[num - 1]
375
+ ui.ok(f"✓ {role_label}: [{chosen[0]}] {chosen[1]}")
376
+ return chosen
377
+ except ValueError:
378
+ pass
379
+
380
+ # Check if typed model matches any known model
381
+ for p, m in all_models:
382
+ if m.lower() == user_input.lower():
383
+ ui.ok(f"✓ {role_label}: [{p}] {m}")
384
+ return (p, m)
385
+
386
+ # Fallback to first provider
387
+ fallback_prov = configured_providers[0].name
388
+ ui.ok(f"✓ {role_label}: [{fallback_prov}] {user_input}")
389
+ return (fallback_prov, user_input)
390
+
391
+ # 1. Architect
392
+ architect_choice = _resolve_selection("Architect (planning & architecture)", all_models[0] if all_models else None)
393
+
394
+ # 2. Builder
395
+ builder_choice = _resolve_selection("Builder (code generation & tools)", architect_choice)
396
+
397
+ # 3. Supervisor
398
+ supervisor_choice = _resolve_selection("Supervisor (verification & review)", architect_choice)
399
+
400
+ # 4. Expert Coder
401
+ expert_choice = _resolve_selection("Expert Coder (deep debugging & algorithms)", builder_choice)
402
+
403
+ role_bindings = {
404
+ "architect": architect_choice,
405
+ "builder": builder_choice,
406
+ "supervisor": supervisor_choice,
407
+ "expert_coder": expert_choice,
408
+ }
409
+
410
+ # Step 4: Sandbox & Approval Mode
411
+ ui.text("\n[bold]Step 4: Permissions & Sandbox[/bold]")
412
+ use_sandbox = Confirm.ask("Enable Docker sandbox for command execution?", default=False)
413
+ mode = Prompt.ask("Approval mode for commands and network", choices=["auto", "ask", "safe"], default="ask")
414
+
415
+ answers = SetupAnswers(
416
+ providers=configured_providers,
417
+ role_bindings=role_bindings,
418
+ sandbox_enabled=use_sandbox,
419
+ approval_mode=mode,
420
+ )
421
+
422
+ apply_setup(answers, global_dir=g_dir)
423
+ ui.ok(f"\n✓ Setup complete! Configuration successfully written to {g_dir}")
424
+
@@ -41,26 +41,43 @@ def plan_command(
41
41
  config = load_config(root)
42
42
  step = {"n": 0}
43
43
 
44
+ status_ctx = None
45
+ if ui.mode is Output.RICH:
46
+ status_ctx = ui.console.status("[bold cyan][1/3] Analyzing request & repository...[/bold cyan]")
47
+ status_ctx.start()
48
+
44
49
  def progress(stage: str) -> None:
45
50
  step["n"] += 1
46
- ui.text(f"[{step['n']}/3] {STAGE_LABELS.get(stage, stage)}...")
51
+ label = STAGE_LABELS.get(stage, stage)
52
+ ui.text(f"[{step['n']}/3] {label}...")
53
+ if status_ctx:
54
+ status_ctx.update(f"[bold cyan][{step['n']}/3] {label} (waiting for model response)...[/bold cyan]")
47
55
 
48
56
  with open_stores(root) as stores:
49
57
  planner = build_planner(config=config, workspace=root, stores=stores)
50
- try:
51
- result = asyncio.run(
52
- plan_project(
58
+
59
+ async def _run_plan():
60
+ try:
61
+ return await plan_project(
53
62
  planner,
54
63
  stores=stores,
55
64
  request=request,
56
65
  dry_run=dry_run,
57
66
  progress=progress,
58
67
  )
59
- )
68
+ finally:
69
+ aclose = getattr(planner, "aclose", None)
70
+ if aclose is not None:
71
+ try:
72
+ await aclose()
73
+ except Exception:
74
+ pass
75
+
76
+ try:
77
+ result = asyncio.run(_run_plan())
60
78
  finally:
61
- aclose = getattr(planner, "aclose", None)
62
- if aclose is not None:
63
- asyncio.run(aclose())
79
+ if status_ctx:
80
+ status_ctx.stop()
64
81
 
65
82
  tasks = specs_to_tasks(result.plan.tasks)
66
83
  ui.data({
@@ -45,7 +45,7 @@ class IntentResult:
45
45
  _INTENT_PATTERNS = [
46
46
  # Plan and build patterns (e.g. "build me a REST API", "plan and build a service")
47
47
  (
48
- r"^(?:please\s+)?(?:build\s+me\s+|plan\s+and\s+build\s+)(.+)$",
48
+ r"^(?:please\s+)?(?:build\s+(?:me\s+)?|make\s+(?:me\s+)?|plan\s+and\s+build\s+)(.+)$",
49
49
  IntentAction.PLAN_AND_BUILD,
50
50
  lambda m: [["plan", m.group(1).strip()], ["build"]],
51
51
  "Plan architecture and execute build pipeline",
@@ -246,6 +246,10 @@ class ForgexREPL:
246
246
  if text.startswith("/"):
247
247
  text = text[1:].strip()
248
248
 
249
+ # Strip optional leading 'forgex' if user typed 'forgex <cmd>' in REPL
250
+ if text.lower().startswith("forgex "):
251
+ text = text[7:].strip()
252
+
249
253
  tokens = shlex.split(text, posix=sys.platform != "win32")
250
254
  if not tokens:
251
255
  return
@@ -53,6 +53,7 @@ def load_config(
53
53
  overrides: dict[str, Any] | None = None,
54
54
  ) -> ForgexConfig:
55
55
  """Load merged configuration: defaults < global < project < overrides."""
56
+ load_env_files(project_root)
56
57
  data: dict[str, Any] = ForgexConfig().model_dump()
57
58
  data = _deep_merge(data, _read_toml(global_config_path()))
58
59
  if project_root is not None:
@@ -168,7 +169,8 @@ def load_env_files(project_root: Path | None = None) -> list[Path]:
168
169
  except OSError:
169
170
  continue
170
171
  for key, value in values.items():
171
- os.environ.setdefault(key, value)
172
+ if not os.environ.get(key):
173
+ os.environ[key] = value
172
174
  loaded.append(path)
173
175
  return loaded
174
176
 
@@ -21,7 +21,24 @@ def resolve_api_key(config: ProviderConfig) -> str | None:
21
21
  env_name = GROQ_KEY_ENV
22
22
  if not env_name:
23
23
  return None
24
- return os.environ.get(env_name) or None
24
+ val = os.environ.get(env_name)
25
+ if val and val.strip():
26
+ return val.strip()
27
+
28
+ # Direct fallback: check ~/.forgex/.env
29
+ from forgex.config.loader import ENV_FILE_NAME, global_config_path, parse_env_file
30
+
31
+ env_path = global_config_path().parent / ENV_FILE_NAME
32
+ if env_path.is_file():
33
+ try:
34
+ secrets = parse_env_file(env_path)
35
+ if env_name in secrets and secrets[env_name].strip():
36
+ secret_val = secrets[env_name].strip()
37
+ os.environ[env_name] = secret_val
38
+ return secret_val
39
+ except Exception:
40
+ pass
41
+ return None
25
42
 
26
43
 
27
44
  def build_provider(
@@ -58,7 +58,7 @@ class OpenAICompatibleProvider(ModelProvider):
58
58
  *,
59
59
  base_url: str,
60
60
  api_key: str | None = None,
61
- timeout_seconds: float = 60.0,
61
+ timeout_seconds: float = 120.0,
62
62
  max_retries: int = 2,
63
63
  http_client: httpx.AsyncClient | None = None,
64
64
  ) -> None:
@@ -153,6 +153,8 @@ class OpenAICompatibleProvider(ModelProvider):
153
153
  message_data = first_choice.get("message") or {}
154
154
 
155
155
  content = message_data.get("content")
156
+ if not content and message_data.get("reasoning_content"):
157
+ content = message_data.get("reasoning_content")
156
158
 
157
159
  # Parse tool calls
158
160
  tool_calls: list[ToolCall] = []
@@ -6,7 +6,8 @@ from forgex.prompts import PROMPT_VERSIONS, load_prompt
6
6
 
7
7
 
8
8
  def test_version_is_defined():
9
- assert forgex.__version__ == "0.1.0"
9
+ assert isinstance(forgex.__version__, str)
10
+ assert len(forgex.__version__.split(".")) >= 3
10
11
 
11
12
 
12
13
  def test_console_entry_point_is_callable():