gdmcode 0.1.3__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.
- {gdmcode-0.1.3 → gdmcode-0.1.4}/PKG-INFO +2 -2
- {gdmcode-0.1.3 → gdmcode-0.1.4}/README.md +1 -1
- {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/cli-reference.md +3 -3
- {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/quick-start.md +7 -1
- {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/security-hardening.md +2 -0
- gdmcode-0.1.4/gdmcode/__init__.py +1 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/auth.py +7 -4
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/cli.py +2 -2
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/commands.py +93 -1
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/mcp_server.py +1 -1
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/repl.py +66 -7
- {gdmcode-0.1.3 → gdmcode-0.1.4}/pyproject.toml +1 -1
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_commands.py +43 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_repl_smoke.py +55 -1
- gdmcode-0.1.3/gdmcode/__init__.py +0 -1
- {gdmcode-0.1.3 → gdmcode-0.1.4}/.gitignore +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/CONTRIBUTING.md +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/config.toml +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/agentic-runtime-audit.md +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/architecture.md +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/configuration.md +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/deployment.md +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/plugin-guide.md +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/_internal/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/_internal/constants.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/_internal/domain_skills.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/commit_classifier.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/context_budget.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/daemon.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/dag_validator.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/debug_loop.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/impact_analyzer.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/impact_graph.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/loop.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/orchestrator.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/regression_guard.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/review_gate.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/risk_scorer.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/self_healing.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/smart_test_selector.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/system_prompt.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/task_tracker.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/test_validator.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/tool_orchestrator.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/transcript.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/verification_loop.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/work_director.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/worktree_manager.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/artifacts/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/artifacts/artifact_store.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/artifacts/verification_graph.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/config.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/cost_tracker.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/db/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/db/migrations.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/audit_log.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/identity.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/rbac.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/team_config.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/usage_analytics.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/exceptions.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/git_workflow.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/github_actions.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/sentry_integration.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/sentry_server.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/webhook_security.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/main.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/code_index.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/compressor.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/context_memory.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/continuous_memory.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/conventions.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/db.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/document_index.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/file_cache.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/project_scanner.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/session_store.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/models/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/models/client.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/models/definitions.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/models/router.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/models/schemas.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/permissions.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/command_filter.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/models.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/permission_handler.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/phone_ui.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/protocol.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/qr.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/server.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/token_manager.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/tunnel.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/runtime/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/runtime/branch_farm.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/runtime/replay.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sandbox/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sandbox/hermetic.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sandbox/policy.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sdk/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sdk/plugin_base.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sdk/plugin_host.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sdk/plugin_loader.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/security.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/server/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/server/bridge.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/server/bridge_cli.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/server/bridge_client.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/server/protocol_version.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/session/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/session/event_fanout.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/session/input_broker.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/session/permission_bridge.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/_atomic.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/agent_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/ask_user_tool.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/bash_tool.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/browser_tool.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/browser_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/dep_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/document_reader.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/document_tool.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/document_writer.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/impact_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/playwright_tool.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/quality_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/read_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/result_cache.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/search_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/shell_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/write_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/audio_capture.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/audio_playback.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/errors.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/models.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/providers.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/vad.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/voice_loop.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/proxy/Dockerfile +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/proxy/main.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/proxy/requirements.txt +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/remote/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/remote/test_remote_server.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_agent_loop.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_agent_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_api_fallback.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_artifact_store.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_audit_log.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_auth.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_auto_quality.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_autonomy_levels.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_bash_tool.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_batch_api.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_branch_farm.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_bridge.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_bridge_smoke.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_browser_tool_smoke.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_browser_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_btw_queue.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_budget_tracker.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_chrome_extension.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_ci_runner.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_cli_smoke.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_code_index.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_compression.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_confidence.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_config.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_continuous_memory.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_convention_drift.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_cost_tracker.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_daemon.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_daemon_stability.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_daemon_watchdog.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_db.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_debate.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_debug_loop.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_debug_loop_smoke.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_dep_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_doctor.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_document_index.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_document_reader.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_document_tool.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_document_writer.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_domain_skills.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_eval_harness.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_event_log.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_failure_taxonomy.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_file_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_git_workflow.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_github_actions.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_health.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_hermetic_sandbox.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_identity_rbac.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_impact_analysis.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_impact_analyzer.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_impact_graph.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_impact_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_injection_gate.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_leaderboard.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_local_models.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_loop.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_loop_p3.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_mcp_server.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_memory.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_migrations.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_mock_provider.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_model_config.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_orchestrator.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_package.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_permissions.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_phase2_modules.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_phone_ui.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_playwright_tool.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_plugin_sdk.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_protocol_version.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_provenance.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_proxy_server.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_quality_integration.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_reasoning_toggle.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_redaction.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_regression_collector.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_regression_guard_integration.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_regression_runner.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_replay.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_resilience.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_result_cache.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_review_gate_expanded.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_risk_scorer.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_rollback.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_router.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_router_compressor_conventions.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_router_escalation.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_sandbox.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_scoring.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_search_tools.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_self_healing.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_semantic_edit.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_sentry_integration.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_session_checkpoint.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_session_controller.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_session_restore.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_signal_handling.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_swebench_adapter.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_swebench_runner.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_system_prompt.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_team_config.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_tool_cache.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_tool_orchestrator.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_tool_timeout.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_tools_registry.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_tunnel_qr.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_usage_analytics.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_verification_graph.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_verification_loop.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_voice_loop.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_voice_providers.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_whole_codebase.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_work_director.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/voice/__init__.py +0 -0
- {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/voice/test_audio_foundation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gdmcode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: gdm: AI coding agent for professional developers
|
|
5
5
|
Project-URL: Homepage, https://github.com/guidegdm/gdmcode
|
|
6
6
|
Project-URL: Repository, https://github.com/guidegdm/gdmcode
|
|
@@ -204,7 +204,7 @@ gdm login grok # or: gemini | codex | all
|
|
|
204
204
|
|
|
205
205
|
Secrets are stored in the OS keychain — never in TOML files.
|
|
206
206
|
|
|
207
|
-
Interactive setup is also available inside `gdm` with `/login
|
|
207
|
+
Interactive setup is also available inside `gdm` with `/login`, `/logout`, and `/proxy`. First launch without credentials shows onboarding instead of starting model agents; type a normal message like `hey` to test the connected LLM.
|
|
208
208
|
|
|
209
209
|
Full configuration reference: [docs/configuration.md](docs/configuration.md)
|
|
210
210
|
|
|
@@ -107,7 +107,7 @@ gdm login grok # or: gemini | codex | all
|
|
|
107
107
|
|
|
108
108
|
Secrets are stored in the OS keychain — never in TOML files.
|
|
109
109
|
|
|
110
|
-
Interactive setup is also available inside `gdm` with `/login
|
|
110
|
+
Interactive setup is also available inside `gdm` with `/login`, `/logout`, and `/proxy`. First launch without credentials shows onboarding instead of starting model agents; type a normal message like `hey` to test the connected LLM.
|
|
111
111
|
|
|
112
112
|
Full configuration reference: [docs/configuration.md](docs/configuration.md)
|
|
113
113
|
|
|
@@ -71,11 +71,11 @@ Authenticate with an AI provider and store credentials in the OS keychain.
|
|
|
71
71
|
gdm logout [PROVIDER]
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
Remove stored credentials for a provider.
|
|
74
|
+
Remove stored credentials for a provider, proxy, or all routes.
|
|
75
75
|
|
|
76
76
|
| Argument | Values | Default |
|
|
77
77
|
|----------|--------|---------|
|
|
78
|
-
| `PROVIDER` | `grok`, `gemini`, `codex`, `all` | `all` |
|
|
78
|
+
| `PROVIDER` | `grok`, `gemini`, `codex`, `proxy`, `all` | `all` |
|
|
79
79
|
|
|
80
80
|
---
|
|
81
81
|
|
|
@@ -186,4 +186,4 @@ Manage the gdm background daemon (code indexing, session compression, security s
|
|
|
186
186
|
|
|
187
187
|
| Argument | Values | Default |
|
|
188
188
|
|----------|--------|---------|
|
|
189
|
-
| `ACTION` | `start`, `stop`, `status` | `status` |
|
|
189
|
+
| `ACTION` | `start`, `stop`, `status` | `status` |
|
|
@@ -48,6 +48,10 @@ requests and should not store provider keys server-side.
|
|
|
48
48
|
You can run `gdm health` after login/proxy setup to check API connectivity, the database, tool
|
|
49
49
|
availability, and budget state.
|
|
50
50
|
|
|
51
|
+
To disconnect later, run `gdm logout grok` or use `/logout grok` inside the interactive shell.
|
|
52
|
+
`/logout all` forgets stored provider and proxy credentials; environment variables or TOML keys
|
|
53
|
+
must be removed manually if they are still present.
|
|
54
|
+
|
|
51
55
|
## 4. Navigate to your project
|
|
52
56
|
|
|
53
57
|
```bash
|
|
@@ -65,7 +69,9 @@ gdm "fix the login bug"
|
|
|
65
69
|
|
|
66
70
|
If you start `gdm` before connecting a model, it shows setup guidance and keeps slash commands
|
|
67
71
|
available instead of loading the agent loop. Once connected, the agent will read relevant files,
|
|
68
|
-
plan a solution, and propose edits.
|
|
72
|
+
plan a solution, and propose edits. Type `hey` as a quick connection test; if the provider rejects
|
|
73
|
+
the request, gdm explains whether it looks like no LLM, invalid key, rate limit, billing/credit, or
|
|
74
|
+
network/proxy trouble. At autonomy level 2
|
|
69
75
|
(default) it will ask before writing files or running shell commands. Lower the level for more
|
|
70
76
|
prompts; raise it for more independence.
|
|
71
77
|
|
|
@@ -144,6 +144,8 @@ actor attribution in audit logs.
|
|
|
144
144
|
**Rules:**
|
|
145
145
|
- API keys, tokens, and passwords must be stored in the OS keychain via `gdm login`.
|
|
146
146
|
- Relay tokens should be entered with `/proxy token` so they are hidden at the terminal.
|
|
147
|
+
- Use `gdm logout <provider>` or `/logout <provider>` to remove keychain/fallback credentials.
|
|
148
|
+
Environment variables and TOML entries override logout and must be removed manually.
|
|
147
149
|
- They must **never** appear in `config.toml`, `team.toml`, or any environment variable committed
|
|
148
150
|
to version control.
|
|
149
151
|
- `team.toml` is committed to git — treat it as fully public.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.4"
|
|
@@ -217,7 +217,7 @@ def login_interactive(provider: str, store: CredentialStore | None = None) -> No
|
|
|
217
217
|
- "codex" → paste API key
|
|
218
218
|
|
|
219
219
|
Args:
|
|
220
|
-
provider: one of "grok", "gemini", "codex", "all"
|
|
220
|
+
provider: one of "grok", "gemini", "codex", "proxy", "all"
|
|
221
221
|
store: CredentialStore instance (creates a new one if None)
|
|
222
222
|
"""
|
|
223
223
|
from rich.console import Console
|
|
@@ -254,15 +254,18 @@ def logout(provider: str, store: CredentialStore | None = None) -> None:
|
|
|
254
254
|
console = Console()
|
|
255
255
|
s = store or CredentialStore()
|
|
256
256
|
|
|
257
|
+
provider = provider.lower().strip()
|
|
257
258
|
if provider == "all":
|
|
258
|
-
for prov in ("grok", "gemini", "gemini_refresh", "codex"):
|
|
259
|
+
for prov in ("grok", "gemini", "gemini_refresh", "codex", "proxy"):
|
|
259
260
|
s.delete(prov)
|
|
260
|
-
console.print("[green]✓[/green] Logged out from all providers.")
|
|
261
|
-
|
|
261
|
+
console.print("[green]✓[/green] Logged out from all providers and proxy.")
|
|
262
|
+
elif provider in {"grok", "gemini", "codex", "proxy"}:
|
|
262
263
|
s.delete(provider)
|
|
263
264
|
if provider == "gemini":
|
|
264
265
|
s.delete("gemini_refresh")
|
|
265
266
|
console.print(f"[green]✓[/green] Logged out from [bold]{provider}[/bold].")
|
|
267
|
+
else:
|
|
268
|
+
console.print("[red]Unknown provider.[/red] Valid providers: grok, gemini, codex, proxy, all")
|
|
266
269
|
|
|
267
270
|
|
|
268
271
|
# ---------------------------------------------------------------------------
|
|
@@ -92,10 +92,10 @@ def cmd_login(
|
|
|
92
92
|
def cmd_logout(
|
|
93
93
|
provider: Annotated[
|
|
94
94
|
str,
|
|
95
|
-
typer.Argument(help="Provider to log out from: grok | gemini | codex | all"),
|
|
95
|
+
typer.Argument(help="Provider to log out from: grok | gemini | codex | proxy | all"),
|
|
96
96
|
] = "all",
|
|
97
97
|
) -> None:
|
|
98
|
-
"""Remove stored credentials for a provider."""
|
|
98
|
+
"""Remove stored credentials for a provider, proxy, or all routes."""
|
|
99
99
|
from gdmcode.auth import logout
|
|
100
100
|
logout(provider)
|
|
101
101
|
|
|
@@ -7,7 +7,9 @@ Supported commands: /help /model /cost /tasks /btw /compact /clear /status
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
|
+
import os
|
|
10
11
|
import subprocess
|
|
12
|
+
import tomllib
|
|
11
13
|
from dataclasses import dataclass
|
|
12
14
|
from datetime import datetime, timezone
|
|
13
15
|
from pathlib import Path
|
|
@@ -78,10 +80,25 @@ _COMMANDS: dict[str, str] = {
|
|
|
78
80
|
"/artifacts [view|diff|search|export] ...": "List, view, diff, search, or export saved artifacts",
|
|
79
81
|
"/save [name]": "Save the last assistant response as a named artifact",
|
|
80
82
|
"/login [provider]": "Authenticate inside the REPL: grok | gemini | codex | all",
|
|
83
|
+
"/logout [provider]": "Forget credentials: grok | gemini | codex | proxy | all",
|
|
81
84
|
"/proxy [on|off|url <url>|token <tok>|status]": "Route LLM calls via proxy (for geo-blocked regions)",
|
|
82
85
|
"/exit, /quit": "Exit the REPL",
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
_LOGOUT_PROVIDERS: frozenset[str] = frozenset({"grok", "gemini", "codex", "proxy", "all"})
|
|
89
|
+
_PROVIDER_ENV_KEYS: dict[str, tuple[str, ...]] = {
|
|
90
|
+
"grok": ("XAI_API_KEY",),
|
|
91
|
+
"gemini": ("GEMINI_API_KEY",),
|
|
92
|
+
"codex": ("OPENAI_API_KEY",),
|
|
93
|
+
"proxy": ("GDM_PROXY_TOKEN", "GDM_PROXY_ENABLED"),
|
|
94
|
+
}
|
|
95
|
+
_PROVIDER_TOML_KEYS: dict[str, tuple[tuple[str, str], ...]] = {
|
|
96
|
+
"grok": (("api", "xai_api_key"),),
|
|
97
|
+
"gemini": (("api", "gemini_api_key"),),
|
|
98
|
+
"codex": (("api", "openai_api_key"),),
|
|
99
|
+
"proxy": (("proxy", "enabled"),),
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
|
|
86
103
|
@dataclass
|
|
87
104
|
class CommandResult:
|
|
@@ -102,6 +119,7 @@ class CommandResult:
|
|
|
102
119
|
proxy_url: str | None = None
|
|
103
120
|
proxy_token: str | None = None
|
|
104
121
|
prompt_secret: str | None = None # if set, REPL prompts for hidden input then calls apply_proxy_token
|
|
122
|
+
reset_agent: bool = False # credentials/model route changed; caller should rebuild loop
|
|
105
123
|
|
|
106
124
|
|
|
107
125
|
class CommandDispatcher:
|
|
@@ -221,6 +239,8 @@ class CommandDispatcher:
|
|
|
221
239
|
return self._cmd_save(parts[1] if len(parts) > 1 else "")
|
|
222
240
|
case "/login":
|
|
223
241
|
return self._cmd_login(parts[1] if len(parts) > 1 else "all")
|
|
242
|
+
case "/logout":
|
|
243
|
+
return self._cmd_logout(parts[1] if len(parts) > 1 else "all")
|
|
224
244
|
case "/proxy":
|
|
225
245
|
return self._cmd_proxy(parts[1:])
|
|
226
246
|
case "/exit" | "/quit":
|
|
@@ -237,7 +257,12 @@ class CommandDispatcher:
|
|
|
237
257
|
|
|
238
258
|
def _cmd_help(self) -> CommandResult:
|
|
239
259
|
"""Display a table of all available slash commands."""
|
|
240
|
-
tbl = Table(
|
|
260
|
+
tbl = Table(
|
|
261
|
+
title="gdm interactive commands",
|
|
262
|
+
caption="Type a normal message like 'hey' to talk to the connected LLM.",
|
|
263
|
+
header_style="bold cyan",
|
|
264
|
+
show_lines=False,
|
|
265
|
+
)
|
|
241
266
|
tbl.add_column("Command", style="cyan", no_wrap=True)
|
|
242
267
|
tbl.add_column("Description")
|
|
243
268
|
for cmd, desc in _COMMANDS.items():
|
|
@@ -1383,9 +1408,76 @@ class CommandDispatcher:
|
|
|
1383
1408
|
return CommandResult(
|
|
1384
1409
|
handled=True,
|
|
1385
1410
|
refresh_config=True,
|
|
1411
|
+
reset_agent=True,
|
|
1386
1412
|
output="[green]Login updated.[/green] The next agent turn will use the refreshed credentials.",
|
|
1387
1413
|
)
|
|
1388
1414
|
|
|
1415
|
+
def _credential_sources_after_logout(self, providers: list[str]) -> list[str]:
|
|
1416
|
+
"""Return non-keychain sources that can still reconnect after /logout."""
|
|
1417
|
+
targets = ["grok", "gemini", "codex", "proxy"] if "all" in providers else providers
|
|
1418
|
+
sources: list[str] = []
|
|
1419
|
+
for provider in targets:
|
|
1420
|
+
for env_key in _PROVIDER_ENV_KEYS.get(provider, ()):
|
|
1421
|
+
if os.environ.get(env_key):
|
|
1422
|
+
sources.append(f"environment variable {env_key}")
|
|
1423
|
+
|
|
1424
|
+
cfg_file = Path.home() / ".config" / "gdm" / "config.toml"
|
|
1425
|
+
if cfg_file.exists():
|
|
1426
|
+
try:
|
|
1427
|
+
cfg = tomllib.loads(cfg_file.read_text(encoding="utf-8"))
|
|
1428
|
+
except Exception as exc: # noqa: BLE001
|
|
1429
|
+
sources.append(f"{cfg_file} could not be checked ({exc})")
|
|
1430
|
+
else:
|
|
1431
|
+
for provider in targets:
|
|
1432
|
+
for section, key in _PROVIDER_TOML_KEYS.get(provider, ()):
|
|
1433
|
+
if cfg.get(section, {}).get(key):
|
|
1434
|
+
sources.append(f"{cfg_file} [{section}].{key}")
|
|
1435
|
+
return sources
|
|
1436
|
+
|
|
1437
|
+
def _cmd_logout(self, provider: str) -> CommandResult:
|
|
1438
|
+
"""Forget stored credentials for one provider, proxy, or all routes."""
|
|
1439
|
+
provider = provider.lower().strip()
|
|
1440
|
+
if provider not in _LOGOUT_PROVIDERS:
|
|
1441
|
+
return CommandResult(
|
|
1442
|
+
handled=True,
|
|
1443
|
+
output="[red]Usage:[/red] /logout [grok|gemini|codex|proxy|all]",
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
providers = ["grok", "gemini", "codex", "proxy"] if provider == "all" else [provider]
|
|
1447
|
+
try:
|
|
1448
|
+
from gdmcode.auth import CredentialStore
|
|
1449
|
+
|
|
1450
|
+
store = CredentialStore()
|
|
1451
|
+
for prov in providers:
|
|
1452
|
+
store.delete(prov)
|
|
1453
|
+
if prov == "gemini":
|
|
1454
|
+
store.delete("gemini_refresh")
|
|
1455
|
+
except Exception as exc: # noqa: BLE001
|
|
1456
|
+
log.exception("logout failed")
|
|
1457
|
+
return CommandResult(handled=True, output=f"[red]Logout failed:[/red] {exc}")
|
|
1458
|
+
|
|
1459
|
+
if "proxy" in providers:
|
|
1460
|
+
self._proxy_enabled = False
|
|
1461
|
+
self._proxy_token = ""
|
|
1462
|
+
|
|
1463
|
+
label = "all providers and proxy" if provider == "all" else provider
|
|
1464
|
+
lines = [f"[green]✓ Logged out from {label}.[/green]"]
|
|
1465
|
+
sources = self._credential_sources_after_logout(providers)
|
|
1466
|
+
if sources:
|
|
1467
|
+
lines.append("[yellow]Still connected through source(s) /logout cannot remove:[/yellow]")
|
|
1468
|
+
lines.extend(f" [dim]- {source}[/dim]" for source in sources)
|
|
1469
|
+
lines.append("[dim]Remove those values manually, then restart or run /logout again.[/dim]")
|
|
1470
|
+
else:
|
|
1471
|
+
lines.append("[dim]Type /login or /proxy token when you want to reconnect.[/dim]")
|
|
1472
|
+
|
|
1473
|
+
return CommandResult(
|
|
1474
|
+
handled=True,
|
|
1475
|
+
output="\n".join(lines),
|
|
1476
|
+
refresh_config=True,
|
|
1477
|
+
reset_agent=True,
|
|
1478
|
+
proxy_action="disable" if "proxy" in providers else None,
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1389
1481
|
def apply_proxy_token(self, token: str) -> CommandResult:
|
|
1390
1482
|
"""Store proxy token received via hidden prompt (never written to history)."""
|
|
1391
1483
|
token = token.strip()
|
|
@@ -51,7 +51,7 @@ _INTERNAL_ERROR = -32603
|
|
|
51
51
|
class MCPServer:
|
|
52
52
|
"""Minimal MCP server with stdio transport."""
|
|
53
53
|
|
|
54
|
-
def __init__(self, name: str = "gdmcode", version: str = "0.1.
|
|
54
|
+
def __init__(self, name: str = "gdmcode", version: str = "0.1.4") -> None:
|
|
55
55
|
self._name = name
|
|
56
56
|
self._version = version
|
|
57
57
|
self._tools: dict[str, MCPTool] = {}
|
|
@@ -15,6 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from typing import TYPE_CHECKING
|
|
16
16
|
|
|
17
17
|
from rich.console import Console
|
|
18
|
+
from rich.markup import escape
|
|
18
19
|
from rich.panel import Panel
|
|
19
20
|
from rich.status import Status
|
|
20
21
|
|
|
@@ -126,7 +127,42 @@ def _fmt_args(args: dict) -> str: # type: ignore[type-arg]
|
|
|
126
127
|
return ", ".join(parts)
|
|
127
128
|
|
|
128
129
|
|
|
129
|
-
def
|
|
130
|
+
def _format_llm_error(error: object) -> str:
|
|
131
|
+
"""Return an actionable, safe-to-render message for model/provider failures."""
|
|
132
|
+
status_code = getattr(error, "status_code", None) or getattr(error, "status", None)
|
|
133
|
+
raw = str(error or "").strip() or "Unknown model error"
|
|
134
|
+
text = raw.lower()
|
|
135
|
+
if status_code is None:
|
|
136
|
+
import re
|
|
137
|
+
|
|
138
|
+
match = re.search(r"\b(?:http\s*)?(401|402|403|429|500|503)\b", text)
|
|
139
|
+
if match:
|
|
140
|
+
status_code = int(match.group(1))
|
|
141
|
+
|
|
142
|
+
if "no api key" in text or "missing credential" in text or "no llm" in text:
|
|
143
|
+
title = "No LLM connected"
|
|
144
|
+
hint = "Run /login grok, /login gemini, /login codex, or /proxy token then /proxy on."
|
|
145
|
+
elif status_code in (401, 403) or any(term in text for term in ("invalid api key", "incorrect api key", "unauthorized", "authentication", "permission denied", "forbidden")):
|
|
146
|
+
title = "LLM authentication failed"
|
|
147
|
+
hint = "Run /logout for that provider, then /login again with a valid key."
|
|
148
|
+
elif status_code == 402 or any(term in text for term in ("insufficient_quota", "insufficient quota", "no credit", "no credits", "billing", "quota exceeded", "balance")):
|
|
149
|
+
title = "LLM account has no available credit"
|
|
150
|
+
hint = "Add credits or billing with the provider, or switch provider with /login or /proxy."
|
|
151
|
+
elif status_code == 429 or any(term in text for term in ("rate limit", "rate_limit", "too many requests", "429")):
|
|
152
|
+
title = "LLM rate limit hit"
|
|
153
|
+
hint = "Wait and retry, lower concurrency, switch provider, or enable /proxy if direct access is blocked."
|
|
154
|
+
elif any(term in text for term in ("connection", "timeout", "timed out", "network", "proxy", "tls", "ssl", "dns")):
|
|
155
|
+
title = "LLM network/proxy error"
|
|
156
|
+
hint = "Check connectivity, provider availability, proxy URL/token, or try /proxy off/on."
|
|
157
|
+
else:
|
|
158
|
+
title = "LLM provider error"
|
|
159
|
+
hint = "Check /status, run /doctor, or switch/re-login with /login."
|
|
160
|
+
|
|
161
|
+
code = f" HTTP {status_code}" if status_code else ""
|
|
162
|
+
return f"[red]{title}{code}.[/red]\n[dim]{escape(raw[:800])}[/dim]\n[yellow]{hint}[/yellow]"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _render_event(event: object, status: Status, console: Console) -> bool:
|
|
130
166
|
"""Render one AgentEvent to the terminal while the spinner is live."""
|
|
131
167
|
from gdmcode.agent.loop import EventType # lazy
|
|
132
168
|
|
|
@@ -146,12 +182,16 @@ def _render_event(event: object, status: Status, console: Console) -> None:
|
|
|
146
182
|
console.print(str(ev.content or "")) # type: ignore[union-attr]
|
|
147
183
|
case EventType.ERROR:
|
|
148
184
|
status.stop()
|
|
149
|
-
console.print(
|
|
185
|
+
console.print(_format_llm_error(ev.content)) # type: ignore[union-attr]
|
|
186
|
+
return True
|
|
187
|
+
case EventType.WARNING:
|
|
188
|
+
console.print(f"[yellow]{escape(str(ev.content or 'Warning'))}[/yellow]") # type: ignore[union-attr]
|
|
150
189
|
case EventType.COST_UPDATE:
|
|
151
190
|
console.print(f"[dim] [${ev.cost_usd:.5f} | turn {ev.turn}][/dim]") # type: ignore[union-attr]
|
|
152
191
|
case EventType.DONE:
|
|
153
192
|
status.stop()
|
|
154
193
|
console.print("[green]Done[/green]")
|
|
194
|
+
return False
|
|
155
195
|
|
|
156
196
|
|
|
157
197
|
# ---------------------------------------------------------------------------
|
|
@@ -175,12 +215,18 @@ def _run_agent_turn(
|
|
|
175
215
|
verb = _pick_spinner_verb(db, session_id)
|
|
176
216
|
status = Status(f"[cyan]{verb}...[/cyan]", console=console, spinner="dots")
|
|
177
217
|
status.start()
|
|
218
|
+
error_seen = False
|
|
178
219
|
try:
|
|
179
220
|
for event in loop.run(user_message): # type: ignore[union-attr]
|
|
180
|
-
|
|
221
|
+
from gdmcode.agent.loop import EventType
|
|
222
|
+
if error_seen and getattr(event, "type", None) == EventType.DONE:
|
|
223
|
+
status.stop()
|
|
224
|
+
console.print("[yellow]Stopped after model error.[/yellow]")
|
|
225
|
+
continue
|
|
226
|
+
error_seen = _render_event(event, status, console) or error_seen
|
|
181
227
|
except Exception as exc: # noqa: BLE001
|
|
182
228
|
status.stop()
|
|
183
|
-
console.print(
|
|
229
|
+
console.print(_format_llm_error(exc))
|
|
184
230
|
log.exception("Agent turn failed")
|
|
185
231
|
finally:
|
|
186
232
|
status.stop()
|
|
@@ -278,6 +324,17 @@ def start_repl(cfg: "GdmConfig", db: "GdmDatabase", *, yes: bool = False, model_
|
|
|
278
324
|
loop: object = None
|
|
279
325
|
permissions: object = None
|
|
280
326
|
|
|
327
|
+
def _drop_agent_loop() -> None:
|
|
328
|
+
"""Flush and discard the current loop after credentials/model route changes."""
|
|
329
|
+
nonlocal loop, permissions
|
|
330
|
+
if loop is not None:
|
|
331
|
+
try:
|
|
332
|
+
loop._flush_checkpoint_sync() # type: ignore[union-attr]
|
|
333
|
+
except Exception: # noqa: BLE001
|
|
334
|
+
pass
|
|
335
|
+
loop = None
|
|
336
|
+
permissions = None
|
|
337
|
+
|
|
281
338
|
def _ensure_loop() -> bool:
|
|
282
339
|
nonlocal cfg, loop, permissions
|
|
283
340
|
if loop is not None:
|
|
@@ -467,6 +524,8 @@ def start_repl(cfg: "GdmConfig", db: "GdmDatabase", *, yes: bool = False, model_
|
|
|
467
524
|
dispatcher._provider = getattr(cfg, "provider", dispatcher._provider) # type: ignore[attr-defined]
|
|
468
525
|
except Exception as exc: # noqa: BLE001
|
|
469
526
|
console.print(f"[yellow]Config reload failed:[/yellow] {exc}")
|
|
527
|
+
if isinstance(result.reset_agent, bool) and result.reset_agent:
|
|
528
|
+
_drop_agent_loop()
|
|
470
529
|
if result.should_exit:
|
|
471
530
|
console.print("[dim]Bye.[/dim]")
|
|
472
531
|
break
|
|
@@ -507,7 +566,7 @@ def start_repl(cfg: "GdmConfig", db: "GdmDatabase", *, yes: bool = False, model_
|
|
|
507
566
|
elif result.proxy_action == "disable":
|
|
508
567
|
from dataclasses import replace as _dc_replace
|
|
509
568
|
|
|
510
|
-
cfg = _dc_replace(cfg, proxy_enabled=False)
|
|
569
|
+
cfg = _dc_replace(cfg, proxy_enabled=False, proxy_token=None)
|
|
511
570
|
dispatcher._cfg = cfg # type: ignore[attr-defined]
|
|
512
571
|
if loop is None:
|
|
513
572
|
console.print("[dim]Proxy disabled for future agent turns.[/dim]")
|
|
@@ -543,7 +602,7 @@ def _print_onboarding(console: Console, *, error: str | None = None) -> None:
|
|
|
543
602
|
"""Show the first-run path without starting model or agent machinery."""
|
|
544
603
|
title = "Connect an LLM to start coding"
|
|
545
604
|
if error:
|
|
546
|
-
title = "
|
|
605
|
+
title = "No LLM connected"
|
|
547
606
|
body = [
|
|
548
607
|
"[bold]Choose one setup path:[/bold]",
|
|
549
608
|
"",
|
|
@@ -554,7 +613,7 @@ def _print_onboarding(console: Console, *, error: str | None = None) -> None:
|
|
|
554
613
|
" [cyan]/proxy token[/cyan] Enter your provider key hidden, stored locally",
|
|
555
614
|
" [cyan]/proxy on[/cyan] Route calls through the configured proxy",
|
|
556
615
|
"",
|
|
557
|
-
"[dim]
|
|
616
|
+
"[dim]Type a normal prompt like 'hey' after connecting. No agent/model starts before then.[/dim]",
|
|
558
617
|
]
|
|
559
618
|
if error:
|
|
560
619
|
body.insert(0, f"[yellow]{error}[/yellow]")
|
|
@@ -93,6 +93,49 @@ class TestHelpCommand:
|
|
|
93
93
|
assert result.handled is True
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# /logout
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
class TestLogoutCommand:
|
|
101
|
+
def test_logout_provider_deletes_stored_credential(self, dispatcher):
|
|
102
|
+
store = MagicMock()
|
|
103
|
+
with patch("gdmcode.auth.CredentialStore", return_value=store):
|
|
104
|
+
result = dispatcher.handle("/logout grok")
|
|
105
|
+
|
|
106
|
+
store.delete.assert_called_once_with("grok")
|
|
107
|
+
assert result.handled is True
|
|
108
|
+
assert result.refresh_config is True
|
|
109
|
+
assert result.reset_agent is True
|
|
110
|
+
assert "Logged out" in result.output
|
|
111
|
+
|
|
112
|
+
def test_logout_all_deletes_providers_proxy_and_gemini_refresh(self, dispatcher):
|
|
113
|
+
store = MagicMock()
|
|
114
|
+
with patch("gdmcode.auth.CredentialStore", return_value=store):
|
|
115
|
+
result = dispatcher.handle("/logout all")
|
|
116
|
+
|
|
117
|
+
deleted = [call.args[0] for call in store.delete.call_args_list]
|
|
118
|
+
assert deleted == ["grok", "gemini", "gemini_refresh", "codex", "proxy"]
|
|
119
|
+
assert result.proxy_action == "disable"
|
|
120
|
+
assert dispatcher._proxy_enabled is False
|
|
121
|
+
assert dispatcher._proxy_token == ""
|
|
122
|
+
|
|
123
|
+
def test_logout_rejects_unknown_provider(self, dispatcher):
|
|
124
|
+
result = dispatcher.handle("/logout nope")
|
|
125
|
+
assert result.handled is True
|
|
126
|
+
assert "Usage" in result.output
|
|
127
|
+
assert result.refresh_config is False
|
|
128
|
+
|
|
129
|
+
def test_logout_warns_when_env_still_provides_key(self, dispatcher, monkeypatch):
|
|
130
|
+
monkeypatch.setenv("XAI_API_KEY", "still-set")
|
|
131
|
+
store = MagicMock()
|
|
132
|
+
with patch("gdmcode.auth.CredentialStore", return_value=store):
|
|
133
|
+
result = dispatcher.handle("/logout grok")
|
|
134
|
+
|
|
135
|
+
assert "XAI_API_KEY" in result.output
|
|
136
|
+
assert "cannot remove" in result.output
|
|
137
|
+
|
|
138
|
+
|
|
96
139
|
# ---------------------------------------------------------------------------
|
|
97
140
|
# /model
|
|
98
141
|
# ---------------------------------------------------------------------------
|
|
@@ -5,6 +5,7 @@ CommandDispatcher handles slash commands without requiring a live model.
|
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
from types import SimpleNamespace
|
|
8
9
|
from unittest.mock import MagicMock, patch
|
|
9
10
|
|
|
10
11
|
import pytest
|
|
@@ -135,10 +136,63 @@ class TestReplStartRepl:
|
|
|
135
136
|
start_repl(cfg, db)
|
|
136
137
|
|
|
137
138
|
output = capsys.readouterr().out
|
|
138
|
-
assert "
|
|
139
|
+
assert "No LLM connected" in output
|
|
139
140
|
assert "missing credentials" in output
|
|
140
141
|
assert "/proxy on" in output
|
|
141
142
|
|
|
143
|
+
def test_plain_hey_without_credentials_reports_no_llm(self, tmp_path, capsys) -> None:
|
|
144
|
+
from gdmcode.repl import start_repl
|
|
145
|
+
|
|
146
|
+
cfg, db = self._make_mocks(tmp_path)
|
|
147
|
+
|
|
148
|
+
with patch("gdmcode.repl._build_input_fn") as mock_build:
|
|
149
|
+
mock_build.return_value = MagicMock(side_effect=["hey", EOFError])
|
|
150
|
+
with patch("gdmcode.config.load_config", side_effect=RuntimeError("No API key found")):
|
|
151
|
+
with patch("gdmcode.cost_tracker.CostTracker"):
|
|
152
|
+
with patch("gdmcode.repl._ensure_session", return_value="session-123"):
|
|
153
|
+
start_repl(cfg, db)
|
|
154
|
+
|
|
155
|
+
output = capsys.readouterr().out
|
|
156
|
+
assert "No LLM connected" in output
|
|
157
|
+
assert "/login grok" in output
|
|
158
|
+
assert "/proxy token" in output
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class TestReplErrorFormatting:
|
|
162
|
+
def test_format_llm_error_rate_limit(self) -> None:
|
|
163
|
+
from gdmcode.exceptions import ApiRateLimitError
|
|
164
|
+
from gdmcode.repl import _format_llm_error
|
|
165
|
+
|
|
166
|
+
msg = _format_llm_error(ApiRateLimitError("rate limit exceeded", status_code=429))
|
|
167
|
+
assert "rate limit" in msg.lower()
|
|
168
|
+
assert "retry" in msg.lower()
|
|
169
|
+
|
|
170
|
+
def test_format_llm_error_infers_status_from_string(self) -> None:
|
|
171
|
+
from gdmcode.repl import _format_llm_error
|
|
172
|
+
|
|
173
|
+
msg = _format_llm_error("HTTP 429 too many requests")
|
|
174
|
+
assert "rate limit" in msg.lower()
|
|
175
|
+
|
|
176
|
+
def test_format_llm_error_invalid_key(self) -> None:
|
|
177
|
+
from gdmcode.exceptions import ApiError
|
|
178
|
+
from gdmcode.repl import _format_llm_error
|
|
179
|
+
|
|
180
|
+
msg = _format_llm_error(ApiError("invalid api key", status_code=401))
|
|
181
|
+
assert "authentication" in msg.lower()
|
|
182
|
+
assert "/login" in msg
|
|
183
|
+
|
|
184
|
+
def test_render_error_suppresses_misleading_done(self, capsys) -> None:
|
|
185
|
+
from gdmcode.agent.loop import EventType
|
|
186
|
+
from gdmcode.repl import _render_event
|
|
187
|
+
from rich.console import Console
|
|
188
|
+
|
|
189
|
+
status = MagicMock()
|
|
190
|
+
console = Console()
|
|
191
|
+
event = SimpleNamespace(type=EventType.ERROR, content="insufficient quota")
|
|
192
|
+
assert _render_event(event, status, console) is True
|
|
193
|
+
output = capsys.readouterr().out
|
|
194
|
+
assert "credit" in output.lower() or "quota" in output.lower()
|
|
195
|
+
|
|
142
196
|
|
|
143
197
|
class TestCommandDispatcher:
|
|
144
198
|
"""Smoke tests for CommandDispatcher (called by start_repl)."""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|