forgex-cli 0.1.2__tar.gz → 0.1.4__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.
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/PKG-INFO +1 -1
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/__init__.py +1 -1
- forgex_cli-0.1.4/src/forgex/cli/commands/config.py +424 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/plan.py +11 -1
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/config/loader.py +3 -1
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/models/factory.py +18 -1
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/models/openai_compatible.py +3 -1
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_packaging.py +2 -1
- forgex_cli-0.1.2/src/forgex/cli/commands/config.py +0 -225
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/.github/workflows/ci.yml +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/.github/workflows/release.yml +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/.gitignore +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/CHANGELOG.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/LICENSE +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/README.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/SECURITY.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/docs/agents.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/docs/architecture.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/docs/configuration.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/docs/decisions.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/docs/providers.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/docs/quickstart.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/docs/sandbox.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/docs/security.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/examples/demo-project/README.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/examples/demo-project/pyproject.toml +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/examples/demo-project/src/demo/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/examples/demo-project/src/demo/calc.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/examples/demo-project/tests/test_calc.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/pyproject.toml +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/__main__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/agents/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/agents/architect.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/agents/base.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/agents/builder.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/agents/ensemble.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/agents/expert_coder.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/agents/supervisor.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/app.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/build_views.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/build.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/checkpoint.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/context.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/diff.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/do_cmd.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/doctor.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/fix.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/init.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/logs.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/models.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/review.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/rollback.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/run.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/status.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/supervise.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/task.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/tools.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/commands/verify.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/context_memory.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/intent.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/progress.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/repl.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/runtime.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/session.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/cli/ui.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/config/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/config/schema.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/context/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/context/chunker.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/context/dependency_graph.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/context/indexer.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/context/manager.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/context/retriever.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/models/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/models/base.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/models/capabilities.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/models/groq.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/models/local.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/models/registry.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/models/router.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/orchestration/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/orchestration/checkpoints.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/orchestration/engine.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/orchestration/events.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/orchestration/lockfile.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/orchestration/retry.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/orchestration/state.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/orchestration/stores.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/orchestration/workflow.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/project.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/prompts/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/prompts/architect.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/prompts/builder.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/prompts/expert_coder.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/prompts/reviewer.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/prompts/supervisor.md +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/research/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/research/citations.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/research/engine.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/research/sources.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/sandbox/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/sandbox/executor.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/sandbox/permissions.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/sandbox/policies.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/schemas/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/schemas/agent.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/schemas/checkpoint.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/schemas/error.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/schemas/event.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/schemas/plan.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/schemas/project.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/schemas/research.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/schemas/result.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/schemas/task.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/storage/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/storage/database.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/storage/migrations.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/storage/repositories.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/tools/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/tools/base.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/tools/dependencies.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/tools/filesystem.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/tools/git.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/tools/project.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/tools/search.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/tools/terminal.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/tools/testing.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/tools/verification.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/utils/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/utils/logging.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/utils/paths.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/src/forgex/utils/redact.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/conftest.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/__init__.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_agents_architect.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_builder_agent.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_checkpoints.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_cli.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_cli_build.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_cli_checkpoint.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_cli_do.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_cli_models.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_cli_plan.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_cli_review.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_cli_surface.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_cli_task.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_cli_tools.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_cli_verify.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_config.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_config_env.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_config_setup.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_context_agents.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_context_manager.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_context_memory.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_engine.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_ensemble.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_events.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_executor.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_expert_coder.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_fix.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_indexer.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_init.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_intent.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_large_repo.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_offline.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_paths.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_permissions.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_plan_schemas.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_policies.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_project.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_providers.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_recovery.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_redact.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_registry.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_repl.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_research.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_retriever.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_retry.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_router.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_run_cli.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_runtime.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_security.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_session.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_state.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_storage.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_stores.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_supervisor.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_tasks.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_tools_dependencies.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_tools_filesystem.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_tools_git.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_tools_search.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_tools_terminal.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/tests/unit/test_verification.py +0 -0
- {forgex_cli-0.1.2 → forgex_cli-0.1.4}/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
|
+
Version: 0.1.4
|
|
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
|
|
@@ -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,9 +41,17 @@ 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
|
-
|
|
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)
|
|
@@ -58,6 +66,8 @@ def plan_command(
|
|
|
58
66
|
)
|
|
59
67
|
)
|
|
60
68
|
finally:
|
|
69
|
+
if status_ctx:
|
|
70
|
+
status_ctx.stop()
|
|
61
71
|
aclose = getattr(planner, "aclose", None)
|
|
62
72
|
if aclose is not None:
|
|
63
73
|
asyncio.run(aclose())
|
|
@@ -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.
|
|
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
|
-
|
|
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 =
|
|
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__
|
|
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():
|