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.
Files changed (267) hide show
  1. {gdmcode-0.1.3 → gdmcode-0.1.4}/PKG-INFO +2 -2
  2. {gdmcode-0.1.3 → gdmcode-0.1.4}/README.md +1 -1
  3. {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/cli-reference.md +3 -3
  4. {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/quick-start.md +7 -1
  5. {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/security-hardening.md +2 -0
  6. gdmcode-0.1.4/gdmcode/__init__.py +1 -0
  7. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/auth.py +7 -4
  8. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/cli.py +2 -2
  9. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/commands.py +93 -1
  10. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/mcp_server.py +1 -1
  11. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/repl.py +66 -7
  12. {gdmcode-0.1.3 → gdmcode-0.1.4}/pyproject.toml +1 -1
  13. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_commands.py +43 -0
  14. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_repl_smoke.py +55 -1
  15. gdmcode-0.1.3/gdmcode/__init__.py +0 -1
  16. {gdmcode-0.1.3 → gdmcode-0.1.4}/.gitignore +0 -0
  17. {gdmcode-0.1.3 → gdmcode-0.1.4}/CONTRIBUTING.md +0 -0
  18. {gdmcode-0.1.3 → gdmcode-0.1.4}/config.toml +0 -0
  19. {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/agentic-runtime-audit.md +0 -0
  20. {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/architecture.md +0 -0
  21. {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/configuration.md +0 -0
  22. {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/deployment.md +0 -0
  23. {gdmcode-0.1.3 → gdmcode-0.1.4}/docs/plugin-guide.md +0 -0
  24. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/_internal/__init__.py +0 -0
  25. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/_internal/constants.py +0 -0
  26. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/_internal/domain_skills.py +0 -0
  27. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/__init__.py +0 -0
  28. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/commit_classifier.py +0 -0
  29. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/context_budget.py +0 -0
  30. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/daemon.py +0 -0
  31. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/dag_validator.py +0 -0
  32. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/debug_loop.py +0 -0
  33. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/impact_analyzer.py +0 -0
  34. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/impact_graph.py +0 -0
  35. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/loop.py +0 -0
  36. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/orchestrator.py +0 -0
  37. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/regression_guard.py +0 -0
  38. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/review_gate.py +0 -0
  39. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/risk_scorer.py +0 -0
  40. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/self_healing.py +0 -0
  41. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/smart_test_selector.py +0 -0
  42. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/system_prompt.py +0 -0
  43. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/task_tracker.py +0 -0
  44. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/test_validator.py +0 -0
  45. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/tool_orchestrator.py +0 -0
  46. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/transcript.py +0 -0
  47. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/verification_loop.py +0 -0
  48. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/work_director.py +0 -0
  49. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/agent/worktree_manager.py +0 -0
  50. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/artifacts/__init__.py +0 -0
  51. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/artifacts/artifact_store.py +0 -0
  52. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/artifacts/verification_graph.py +0 -0
  53. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/config.py +0 -0
  54. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/cost_tracker.py +0 -0
  55. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/db/__init__.py +0 -0
  56. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/db/migrations.py +0 -0
  57. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/__init__.py +0 -0
  58. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/audit_log.py +0 -0
  59. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/identity.py +0 -0
  60. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/rbac.py +0 -0
  61. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/team_config.py +0 -0
  62. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/enterprise/usage_analytics.py +0 -0
  63. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/exceptions.py +0 -0
  64. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/git_workflow.py +0 -0
  65. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/__init__.py +0 -0
  66. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/github_actions.py +0 -0
  67. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/sentry_integration.py +0 -0
  68. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/sentry_server.py +0 -0
  69. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/integrations/webhook_security.py +0 -0
  70. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/main.py +0 -0
  71. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/__init__.py +0 -0
  72. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/code_index.py +0 -0
  73. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/compressor.py +0 -0
  74. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/context_memory.py +0 -0
  75. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/continuous_memory.py +0 -0
  76. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/conventions.py +0 -0
  77. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/db.py +0 -0
  78. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/document_index.py +0 -0
  79. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/file_cache.py +0 -0
  80. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/project_scanner.py +0 -0
  81. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/memory/session_store.py +0 -0
  82. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/models/__init__.py +0 -0
  83. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/models/client.py +0 -0
  84. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/models/definitions.py +0 -0
  85. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/models/router.py +0 -0
  86. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/models/schemas.py +0 -0
  87. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/permissions.py +0 -0
  88. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/__init__.py +0 -0
  89. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/command_filter.py +0 -0
  90. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/models.py +0 -0
  91. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/permission_handler.py +0 -0
  92. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/phone_ui.py +0 -0
  93. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/protocol.py +0 -0
  94. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/qr.py +0 -0
  95. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/server.py +0 -0
  96. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/token_manager.py +0 -0
  97. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/remote/tunnel.py +0 -0
  98. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/runtime/__init__.py +0 -0
  99. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/runtime/branch_farm.py +0 -0
  100. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/runtime/replay.py +0 -0
  101. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sandbox/__init__.py +0 -0
  102. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sandbox/hermetic.py +0 -0
  103. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sandbox/policy.py +0 -0
  104. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sdk/__init__.py +0 -0
  105. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sdk/plugin_base.py +0 -0
  106. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sdk/plugin_host.py +0 -0
  107. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/sdk/plugin_loader.py +0 -0
  108. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/security.py +0 -0
  109. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/server/__init__.py +0 -0
  110. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/server/bridge.py +0 -0
  111. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/server/bridge_cli.py +0 -0
  112. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/server/bridge_client.py +0 -0
  113. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/server/protocol_version.py +0 -0
  114. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/session/__init__.py +0 -0
  115. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/session/event_fanout.py +0 -0
  116. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/session/input_broker.py +0 -0
  117. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/session/permission_bridge.py +0 -0
  118. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/__init__.py +0 -0
  119. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/_atomic.py +0 -0
  120. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/agent_tools.py +0 -0
  121. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/ask_user_tool.py +0 -0
  122. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/bash_tool.py +0 -0
  123. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/browser_tool.py +0 -0
  124. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/browser_tools.py +0 -0
  125. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/dep_tools.py +0 -0
  126. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/document_reader.py +0 -0
  127. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/document_tool.py +0 -0
  128. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/document_writer.py +0 -0
  129. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/impact_tools.py +0 -0
  130. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/playwright_tool.py +0 -0
  131. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/quality_tools.py +0 -0
  132. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/read_tools.py +0 -0
  133. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/result_cache.py +0 -0
  134. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/search_tools.py +0 -0
  135. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/shell_tools.py +0 -0
  136. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/tools/write_tools.py +0 -0
  137. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/__init__.py +0 -0
  138. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/audio_capture.py +0 -0
  139. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/audio_playback.py +0 -0
  140. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/errors.py +0 -0
  141. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/models.py +0 -0
  142. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/providers.py +0 -0
  143. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/vad.py +0 -0
  144. {gdmcode-0.1.3 → gdmcode-0.1.4}/gdmcode/voice/voice_loop.py +0 -0
  145. {gdmcode-0.1.3 → gdmcode-0.1.4}/proxy/Dockerfile +0 -0
  146. {gdmcode-0.1.3 → gdmcode-0.1.4}/proxy/main.py +0 -0
  147. {gdmcode-0.1.3 → gdmcode-0.1.4}/proxy/requirements.txt +0 -0
  148. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/__init__.py +0 -0
  149. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/remote/__init__.py +0 -0
  150. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/remote/test_remote_server.py +0 -0
  151. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_agent_loop.py +0 -0
  152. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_agent_tools.py +0 -0
  153. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_api_fallback.py +0 -0
  154. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_artifact_store.py +0 -0
  155. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_audit_log.py +0 -0
  156. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_auth.py +0 -0
  157. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_auto_quality.py +0 -0
  158. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_autonomy_levels.py +0 -0
  159. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_bash_tool.py +0 -0
  160. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_batch_api.py +0 -0
  161. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_branch_farm.py +0 -0
  162. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_bridge.py +0 -0
  163. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_bridge_smoke.py +0 -0
  164. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_browser_tool_smoke.py +0 -0
  165. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_browser_tools.py +0 -0
  166. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_btw_queue.py +0 -0
  167. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_budget_tracker.py +0 -0
  168. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_chrome_extension.py +0 -0
  169. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_ci_runner.py +0 -0
  170. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_cli_smoke.py +0 -0
  171. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_code_index.py +0 -0
  172. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_compression.py +0 -0
  173. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_confidence.py +0 -0
  174. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_config.py +0 -0
  175. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_continuous_memory.py +0 -0
  176. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_convention_drift.py +0 -0
  177. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_cost_tracker.py +0 -0
  178. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_daemon.py +0 -0
  179. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_daemon_stability.py +0 -0
  180. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_daemon_watchdog.py +0 -0
  181. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_db.py +0 -0
  182. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_debate.py +0 -0
  183. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_debug_loop.py +0 -0
  184. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_debug_loop_smoke.py +0 -0
  185. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_dep_tools.py +0 -0
  186. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_doctor.py +0 -0
  187. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_document_index.py +0 -0
  188. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_document_reader.py +0 -0
  189. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_document_tool.py +0 -0
  190. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_document_writer.py +0 -0
  191. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_domain_skills.py +0 -0
  192. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_eval_harness.py +0 -0
  193. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_event_log.py +0 -0
  194. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_failure_taxonomy.py +0 -0
  195. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_file_tools.py +0 -0
  196. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_git_workflow.py +0 -0
  197. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_github_actions.py +0 -0
  198. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_health.py +0 -0
  199. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_hermetic_sandbox.py +0 -0
  200. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_identity_rbac.py +0 -0
  201. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_impact_analysis.py +0 -0
  202. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_impact_analyzer.py +0 -0
  203. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_impact_graph.py +0 -0
  204. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_impact_tools.py +0 -0
  205. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_injection_gate.py +0 -0
  206. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_leaderboard.py +0 -0
  207. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_local_models.py +0 -0
  208. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_loop.py +0 -0
  209. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_loop_p3.py +0 -0
  210. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_mcp_server.py +0 -0
  211. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_memory.py +0 -0
  212. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_migrations.py +0 -0
  213. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_mock_provider.py +0 -0
  214. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_model_config.py +0 -0
  215. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_orchestrator.py +0 -0
  216. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_package.py +0 -0
  217. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_permissions.py +0 -0
  218. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_phase2_modules.py +0 -0
  219. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_phone_ui.py +0 -0
  220. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_playwright_tool.py +0 -0
  221. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_plugin_sdk.py +0 -0
  222. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_protocol_version.py +0 -0
  223. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_provenance.py +0 -0
  224. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_proxy_server.py +0 -0
  225. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_quality_integration.py +0 -0
  226. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_reasoning_toggle.py +0 -0
  227. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_redaction.py +0 -0
  228. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_regression_collector.py +0 -0
  229. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_regression_guard_integration.py +0 -0
  230. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_regression_runner.py +0 -0
  231. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_replay.py +0 -0
  232. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_resilience.py +0 -0
  233. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_result_cache.py +0 -0
  234. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_review_gate_expanded.py +0 -0
  235. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_risk_scorer.py +0 -0
  236. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_rollback.py +0 -0
  237. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_router.py +0 -0
  238. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_router_compressor_conventions.py +0 -0
  239. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_router_escalation.py +0 -0
  240. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_sandbox.py +0 -0
  241. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_scoring.py +0 -0
  242. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_search_tools.py +0 -0
  243. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_self_healing.py +0 -0
  244. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_semantic_edit.py +0 -0
  245. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_sentry_integration.py +0 -0
  246. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_session_checkpoint.py +0 -0
  247. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_session_controller.py +0 -0
  248. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_session_restore.py +0 -0
  249. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_signal_handling.py +0 -0
  250. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_swebench_adapter.py +0 -0
  251. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_swebench_runner.py +0 -0
  252. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_system_prompt.py +0 -0
  253. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_team_config.py +0 -0
  254. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_tool_cache.py +0 -0
  255. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_tool_orchestrator.py +0 -0
  256. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_tool_timeout.py +0 -0
  257. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_tools_registry.py +0 -0
  258. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_tunnel_qr.py +0 -0
  259. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_usage_analytics.py +0 -0
  260. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_verification_graph.py +0 -0
  261. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_verification_loop.py +0 -0
  262. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_voice_loop.py +0 -0
  263. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_voice_providers.py +0 -0
  264. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_whole_codebase.py +0 -0
  265. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/test_work_director.py +0 -0
  266. {gdmcode-0.1.3 → gdmcode-0.1.4}/tests/voice/__init__.py +0 -0
  267. {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
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` and `/proxy`. First launch without credentials shows onboarding instead of starting model agents.
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` and `/proxy`. First launch without credentials shows onboarding instead of starting model agents.
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. At autonomy level 2
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
- else:
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(title="gdm slash commands", header_style="bold cyan", show_lines=False)
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.3") -> None:
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 _render_event(event: object, status: Status, console: Console) -> None:
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(f"[red]Error: {ev.content}[/red]") # type: ignore[union-attr]
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
- _render_event(event, status, console)
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(f"[red]Agent error: {exc}[/red]")
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 = "Model connection required"
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]No agent or model client starts until your first non-command prompt.[/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]")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gdmcode"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "gdm: AI coding agent for professional developers"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -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 "Model connection required" in output
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