gemcode 0.4.23__tar.gz → 0.4.25__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 (210) hide show
  1. {gemcode-0.4.23/src/gemcode.egg-info → gemcode-0.4.25}/PKG-INFO +1 -1
  2. {gemcode-0.4.23 → gemcode-0.4.25}/pyproject.toml +1 -1
  3. gemcode-0.4.25/src/gemcode/web/preview_api.py +213 -0
  4. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/server.py +29 -0
  5. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/sse_adapter.py +57 -12
  6. {gemcode-0.4.23 → gemcode-0.4.25/src/gemcode.egg-info}/PKG-INFO +1 -1
  7. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode.egg-info/SOURCES.txt +2 -0
  8. gemcode-0.4.25/tests/test_preview_proxy.py +21 -0
  9. gemcode-0.4.25/tests/test_web_permissions_hitl.py +71 -0
  10. gemcode-0.4.23/src/gemcode/web/preview_api.py +0 -82
  11. {gemcode-0.4.23 → gemcode-0.4.25}/LICENSE +0 -0
  12. {gemcode-0.4.23 → gemcode-0.4.25}/MANIFEST.in +0 -0
  13. {gemcode-0.4.23 → gemcode-0.4.25}/README.md +0 -0
  14. {gemcode-0.4.23 → gemcode-0.4.25}/setup.cfg +0 -0
  15. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/__init__.py +0 -0
  16. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/__main__.py +0 -0
  17. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/a2a_bridge.py +0 -0
  18. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/agent.py +0 -0
  19. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/agent_habits.py +0 -0
  20. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/agent_intelligence.py +0 -0
  21. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/agent_mesh.py +0 -0
  22. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/agent_triggers.py +0 -0
  23. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/audit.py +0 -0
  24. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/autocompact.py +0 -0
  25. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/automations.py +0 -0
  26. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/autotune.py +0 -0
  27. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/callbacks.py +0 -0
  28. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/capability_routing.py +0 -0
  29. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/checkpoints.py +0 -0
  30. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/cli.py +0 -0
  31. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/codebase_awareness.py +0 -0
  32. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/compaction.py +0 -0
  33. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/computer_use/__init__.py +0 -0
  34. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/computer_use/browser_computer.py +0 -0
  35. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/config.py +0 -0
  36. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/context_budget.py +0 -0
  37. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/context_warning.py +0 -0
  38. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/credentials.py +0 -0
  39. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/curated_memory.py +0 -0
  40. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/delegation_learning.py +0 -0
  41. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/dynamic_policy.py +0 -0
  42. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/evals/harness.py +0 -0
  43. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/event_bus.py +0 -0
  44. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/fleet_reports.py +0 -0
  45. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/habit_chains.py +0 -0
  46. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/habit_runs.py +0 -0
  47. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/hitl_session.py +0 -0
  48. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/hooks.py +0 -0
  49. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/ide_protocol.py +0 -0
  50. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/ide_stdio.py +0 -0
  51. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/intent_classifier.py +0 -0
  52. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/interactions.py +0 -0
  53. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/invoke.py +0 -0
  54. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/kaira_client.py +0 -0
  55. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/kaira_daemon.py +0 -0
  56. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/kaira_ipc.py +0 -0
  57. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/kaira_job_store.py +0 -0
  58. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/learning.py +0 -0
  59. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/limits.py +0 -0
  60. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/live_audio_engine.py +0 -0
  61. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/logging_config.py +0 -0
  62. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/mcp_loader.py +0 -0
  63. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/memory/__init__.py +0 -0
  64. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/memory/embedding_memory_service.py +0 -0
  65. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/memory/file_memory_service.py +0 -0
  66. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/modality_tools.py +0 -0
  67. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/model_errors.py +0 -0
  68. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/model_routing.py +0 -0
  69. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/multimodal_input.py +0 -0
  70. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/openapi_loader.py +0 -0
  71. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/org.py +0 -0
  72. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/output_styles.py +0 -0
  73. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/paths.py +0 -0
  74. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/permissions.py +0 -0
  75. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/plugins/__init__.py +0 -0
  76. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  77. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  78. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/policy_profile.py +0 -0
  79. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/pricing.py +0 -0
  80. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/prompt_suggestions.py +0 -0
  81. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/__init__.py +0 -0
  82. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/config.py +0 -0
  83. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/deps.py +0 -0
  84. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/engine.py +0 -0
  85. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/stop_hooks.py +0 -0
  86. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/token_budget.py +0 -0
  87. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/transitions.py +0 -0
  88. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query_sanitizer.py +0 -0
  89. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/refine.py +0 -0
  90. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/repl_commands.py +0 -0
  91. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/repl_slash.py +0 -0
  92. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/review_agent.py +0 -0
  93. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/rules.py +0 -0
  94. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/self_healing.py +0 -0
  95. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/session_runtime.py +0 -0
  96. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/session_store.py +0 -0
  97. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/session_summariser.py +0 -0
  98. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/skills.py +0 -0
  99. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/slash_commands.py +0 -0
  100. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/thinking.py +0 -0
  101. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tool_prompt_manifest.py +0 -0
  102. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tool_registry.py +0 -0
  103. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tool_result_store.py +0 -0
  104. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tool_synthesis.py +0 -0
  105. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/__init__.py +0 -0
  106. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/automations_tools.py +0 -0
  107. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/bash.py +0 -0
  108. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/browser.py +0 -0
  109. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/compress_memory.py +0 -0
  110. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/curated_memory.py +0 -0
  111. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/edit.py +0 -0
  112. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/filesystem.py +0 -0
  113. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/notebook.py +0 -0
  114. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/notes.py +0 -0
  115. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/org_tools.py +0 -0
  116. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/repo_map.py +0 -0
  117. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/search.py +0 -0
  118. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/shell.py +0 -0
  119. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/shell_gate.py +0 -0
  120. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/skills.py +0 -0
  121. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/subtask.py +0 -0
  122. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/tasks.py +0 -0
  123. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/think.py +0 -0
  124. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/todo.py +0 -0
  125. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/user_choice.py +0 -0
  126. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/veomem_tools.py +0 -0
  127. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/web.py +0 -0
  128. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/web_search.py +0 -0
  129. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools_inspector.py +0 -0
  130. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/trust.py +0 -0
  131. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tui/input_handler.py +0 -0
  132. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tui/scrollback.py +0 -0
  133. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tui/spinner.py +0 -0
  134. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tui/welcome_banner.py +0 -0
  135. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tui/welcome_rich.py +0 -0
  136. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/veomem_bridge.py +0 -0
  137. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/version.py +0 -0
  138. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/vertex.py +0 -0
  139. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/wal.py +0 -0
  140. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/__init__.py +0 -0
  141. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/chat_skills.py +0 -0
  142. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/customize_api.py +0 -0
  143. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/files_api.py +0 -0
  144. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/fleet_api.py +0 -0
  145. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/hitl_bridge.py +0 -0
  146. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/project_root.py +0 -0
  147. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/runtime_api.py +0 -0
  148. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/serve_bind.py +0 -0
  149. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/serve_state.py +0 -0
  150. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/sessions_api.py +0 -0
  151. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/terminal_api.py +0 -0
  152. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/terminal_repl.py +0 -0
  153. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/ui_chat_store.py +0 -0
  154. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/web_config_api.py +0 -0
  155. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/web_sse_compat.py +0 -0
  156. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/workspace_panel_api.py +0 -0
  157. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/workspace_hints.py +0 -0
  158. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode.egg-info/dependency_links.txt +0 -0
  159. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode.egg-info/entry_points.txt +0 -0
  160. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode.egg-info/requires.txt +0 -0
  161. {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode.egg-info/top_level.txt +0 -0
  162. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_add_dir.py +0 -0
  163. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_agent_habits.py +0 -0
  164. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_agent_instruction.py +0 -0
  165. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_agent_mesh.py +0 -0
  166. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_autocompact.py +0 -0
  167. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_automations.py +0 -0
  168. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_capability_routing.py +0 -0
  169. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_checkpoint_diff_command.py +0 -0
  170. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_cli_init.py +0 -0
  171. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_compress_memory_tool.py +0 -0
  172. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_computer_use_permissions.py +0 -0
  173. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_context_budget.py +0 -0
  174. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_context_warning.py +0 -0
  175. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_credentials.py +0 -0
  176. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_eval_harness_layout.py +0 -0
  177. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_event_bus.py +0 -0
  178. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_files_api.py +0 -0
  179. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_fleet_reports.py +0 -0
  180. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_habit_chains.py +0 -0
  181. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_habit_runs.py +0 -0
  182. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_hosted_tenant.py +0 -0
  183. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_ide_stdio_attachments.py +0 -0
  184. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_interactive_permission_ask.py +0 -0
  185. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_kaira_ipc_paths.py +0 -0
  186. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_kaira_scheduler.py +0 -0
  187. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_modality_tools.py +0 -0
  188. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_model_error_retry.py +0 -0
  189. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_model_errors.py +0 -0
  190. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_model_routing.py +0 -0
  191. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_multimodal_input.py +0 -0
  192. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_output_styles_and_rules.py +0 -0
  193. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_paths.py +0 -0
  194. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_permissions.py +0 -0
  195. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_prompt_suggestions.py +0 -0
  196. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_repl_commands.py +0 -0
  197. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_repl_slash.py +0 -0
  198. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_session_runtime_cache.py +0 -0
  199. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_skills.py +0 -0
  200. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_slash_commands.py +0 -0
  201. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_slash_completion_registry.py +0 -0
  202. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_sse_confirmation_batch.py +0 -0
  203. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_thinking_config.py +0 -0
  204. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_token_budget.py +0 -0
  205. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_tool_context_circulation.py +0 -0
  206. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_tools.py +0 -0
  207. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_tools_inspector.py +0 -0
  208. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_web_server.py +0 -0
  209. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_web_sse_adapter.py +0 -0
  210. {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.23
3
+ Version: 0.4.25
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.4.23"
7
+ version = "0.4.25"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,213 @@
1
+ """Detect local (or tenant-pod) dev servers and reverse-proxy them for the web UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import socket
6
+ from http.client import HTTPConnection
7
+ from typing import Any
8
+ from urllib.error import URLError
9
+ from urllib.parse import unquote
10
+ from urllib.request import Request, urlopen
11
+
12
+ # Common dev-server ports (GemCode web UI uses 3002; API 3001)
13
+ COMMON_DEV_PORTS = (
14
+ 3000,
15
+ 3003,
16
+ 3004,
17
+ 5173,
18
+ 5174,
19
+ 8080,
20
+ 8000,
21
+ 4200,
22
+ 5000,
23
+ 4321,
24
+ 8888,
25
+ )
26
+
27
+ _STRIP_RESPONSE_HEADERS = frozenset(
28
+ {
29
+ "transfer-encoding",
30
+ "connection",
31
+ "content-length",
32
+ "content-encoding",
33
+ "x-frame-options",
34
+ "content-security-policy",
35
+ "content-security-policy-report-only",
36
+ }
37
+ )
38
+
39
+
40
+ def _port_open(host: str, port: int, *, timeout: float = 0.2) -> bool:
41
+ try:
42
+ with socket.create_connection((host, port), timeout=timeout):
43
+ return True
44
+ except OSError:
45
+ return False
46
+
47
+
48
+ def _probe_http(url: str, *, timeout: float = 1.5) -> dict[str, Any] | None:
49
+ try:
50
+ req = Request(url, headers={"User-Agent": "GemCode-Web-Preview/1.0"})
51
+ with urlopen(req, timeout=timeout) as resp:
52
+ ctype = (resp.headers.get("Content-Type") or "").lower()
53
+ return {
54
+ "ok": True,
55
+ "status": resp.status,
56
+ "content_type": ctype,
57
+ }
58
+ except (URLError, OSError, TimeoutError, ValueError):
59
+ return None
60
+
61
+
62
+ def scan_local_preview_ports(
63
+ *,
64
+ host: str = "127.0.0.1",
65
+ extra_ports: list[int] | None = None,
66
+ ) -> list[dict[str, Any]]:
67
+ """Return listening HTTP-ish ports on localhost (tenant pod when hosted)."""
68
+ ports: list[int] = list(COMMON_DEV_PORTS)
69
+ if extra_ports:
70
+ for p in extra_ports:
71
+ if isinstance(p, int) and 1 < p < 65536 and p not in ports:
72
+ ports.append(p)
73
+
74
+ found: list[dict[str, Any]] = []
75
+ for port in ports:
76
+ if not _port_open(host, port):
77
+ continue
78
+ url = f"http://{host}:{port}"
79
+ probe = _probe_http(url)
80
+ found.append(
81
+ {
82
+ "port": port,
83
+ "url": url,
84
+ "reachable": probe is not None,
85
+ "content_type": (probe or {}).get("content_type"),
86
+ }
87
+ )
88
+ return found
89
+
90
+
91
+ def handle_preview_get(kind: str) -> tuple[int, dict[str, Any]]:
92
+ k = (kind or "ports").strip().lower()
93
+ if k == "ports":
94
+ ports = scan_local_preview_ports()
95
+ return 200, {"ok": True, "ports": ports, "host": "127.0.0.1"}
96
+ return 400, {"ok": False, "error": f"Unknown preview kind: {kind}"}
97
+
98
+
99
+ def _inject_base_href(html: str, base_path: str) -> str:
100
+ base_tag = f'<base href="{base_path}">'
101
+ if "<base " in html.lower():
102
+ return html
103
+ lower = html.lower()
104
+ idx = lower.find("<head")
105
+ if idx >= 0:
106
+ gt = html.find(">", idx)
107
+ if gt >= 0:
108
+ return html[: gt + 1] + base_tag + html[gt + 1 :]
109
+ return base_tag + html
110
+
111
+
112
+ def handle_preview_proxy(
113
+ port: int,
114
+ sub_path: str = "/",
115
+ *,
116
+ query: str = "",
117
+ ) -> tuple[int, bytes, dict[str, str]]:
118
+ """
119
+ Reverse-proxy HTTP from 127.0.0.1:{port} (on this process / tenant pod).
120
+
121
+ Returns (status, body, response_headers).
122
+ """
123
+ if not isinstance(port, int) or port < 1 or port > 65535:
124
+ return 400, b'{"error":"Invalid port"}', {"Content-Type": "application/json; charset=utf-8"}
125
+
126
+ # Block GemCode's own API/UI ports from being framed as "app preview"
127
+ if port in (3001, 3002):
128
+ return (
129
+ 400,
130
+ b'{"error":"Port reserved for GemCode API/UI"}',
131
+ {"Content-Type": "application/json; charset=utf-8"},
132
+ )
133
+
134
+ raw = sub_path or "/"
135
+ if not raw.startswith("/"):
136
+ raw = "/" + raw
137
+ # Collapse .. segments
138
+ parts: list[str] = []
139
+ for seg in raw.split("/"):
140
+ if seg in ("", "."):
141
+ continue
142
+ if seg == "..":
143
+ if parts:
144
+ parts.pop()
145
+ continue
146
+ parts.append(seg)
147
+ path = "/" + "/".join(parts)
148
+ if raw.endswith("/") and path != "/":
149
+ path += "/"
150
+
151
+ qs = (query or "").lstrip("?")
152
+ request_path = path if not qs else f"{path}?{qs}"
153
+
154
+ try:
155
+ conn = HTTPConnection("127.0.0.1", port, timeout=20.0)
156
+ conn.request(
157
+ "GET",
158
+ request_path,
159
+ headers={"User-Agent": "GemCode-Web-Preview/1.0", "Accept": "*/*"},
160
+ )
161
+ resp = conn.getresponse()
162
+ body = resp.read()
163
+ ctype = (resp.getheader("Content-Type") or "").lower()
164
+ headers: dict[str, str] = {}
165
+ for key, value in resp.getheaders():
166
+ if key.lower() in _STRIP_RESPONSE_HEADERS:
167
+ continue
168
+ headers[key] = value
169
+ status = int(resp.status)
170
+ conn.close()
171
+ except OSError as exc:
172
+ msg = f'{{"error":"Preview upstream unreachable: {type(exc).__name__}: {exc}"}}'
173
+ return 502, msg.encode(), {"Content-Type": "application/json; charset=utf-8"}
174
+
175
+ proxy_base = f"/api/preview/proxy/{port}/"
176
+ if "text/html" in ctype:
177
+ try:
178
+ text = body.decode("utf-8", errors="replace")
179
+ text = _inject_base_href(text, proxy_base)
180
+ body = text.encode("utf-8")
181
+ headers["Content-Type"] = "text/html; charset=utf-8"
182
+ except Exception:
183
+ pass
184
+
185
+ headers["Cache-Control"] = "no-store"
186
+ headers["X-Frame-Options"] = "SAMEORIGIN"
187
+ headers["Content-Security-Policy"] = (
188
+ "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; "
189
+ "script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline'; "
190
+ "frame-ancestors 'self';"
191
+ )
192
+ return status, body, headers
193
+
194
+
195
+ def parse_preview_proxy_path(raw_path: str) -> tuple[int, str] | None:
196
+ """Parse `/api/preview/proxy/{port}` or `/api/preview/proxy/{port}/...` → (port, sub_path)."""
197
+ prefix = "/api/preview/proxy/"
198
+ if not raw_path.startswith(prefix):
199
+ return None
200
+ rest = raw_path[len(prefix) :]
201
+ if not rest:
202
+ return None
203
+ parts = rest.split("/", 1)
204
+ try:
205
+ port = int(parts[0])
206
+ except ValueError:
207
+ return None
208
+ sub = "/"
209
+ if len(parts) > 1 and parts[1]:
210
+ sub = "/" + unquote(parts[1])
211
+ elif rest.endswith("/") or raw_path.endswith("/"):
212
+ sub = "/"
213
+ return port, sub
@@ -304,6 +304,35 @@ def _build_handler(project_root: str) -> type[BaseHTTPRequestHandler]:
304
304
  self.wfile.write(body)
305
305
  return
306
306
 
307
+ if parsed.path.startswith("/api/preview/proxy/"):
308
+ try:
309
+ from gemcode.web.preview_api import handle_preview_proxy, parse_preview_proxy_path
310
+
311
+ parsed_proxy = parse_preview_proxy_path(parsed.path)
312
+ if not parsed_proxy:
313
+ status, body, hdrs = (
314
+ 400,
315
+ b'{"error":"Invalid preview proxy path"}',
316
+ {"Content-Type": "application/json; charset=utf-8"},
317
+ )
318
+ else:
319
+ port, sub_path = parsed_proxy
320
+ status, body, hdrs = handle_preview_proxy(
321
+ port, sub_path, query=parsed.query or ""
322
+ )
323
+ except Exception as exc:
324
+ status = 500
325
+ body = json.dumps({"error": f"{type(exc).__name__}: {exc}"}).encode()
326
+ hdrs = {"Content-Type": "application/json; charset=utf-8"}
327
+ self.send_response(status)
328
+ for hk, hv in hdrs.items():
329
+ self.send_header(hk, hv)
330
+ self.send_header("Content-Length", str(len(body)))
331
+ self._cors()
332
+ self.end_headers()
333
+ self.wfile.write(body)
334
+ return
335
+
307
336
  fleet_get = {
308
337
  "/api/org": "org",
309
338
  "/api/habits": "habits",
@@ -489,15 +489,33 @@ def _inject_web_code_context(cfg: GemCodeConfig, prompt: str, req: dict[str, Any
489
489
  "or type @path — unless they mean the whole project (see below)."
490
490
  )
491
491
 
492
- lines.extend(
493
- [
494
- 'When the user says "this file", "the current one", or similar, use the active / @-referenced file above.',
495
- 'When they ask to analyze the whole codebase, project, repo, or "all files", start with `repo_map` and '
496
- '`list_directory` on `.` — do not ask which file first.',
497
- "**Web UI permissions** — shell and mutating tools show an **Approve / Deny** dialog in the browser. "
498
- "Never tell the user to approve in a terminal; wait for the in-app prompt and continue after approval.",
499
- ]
492
+ # Permission mode for THIS turn (must match actual HITL / auto-approve behavior).
493
+ lines.append(
494
+ 'When the user says "this file", "the current one", or similar, use the active / @-referenced file above.'
500
495
  )
496
+ lines.append(
497
+ 'When they ask to analyze the whole codebase, project, repo, or "all files", start with `repo_map` and '
498
+ '`list_directory` on `.` — do not ask which file first.'
499
+ )
500
+ auto_on = bool(
501
+ getattr(cfg, "yes_to_all", False)
502
+ or getattr(cfg, "super_mode", False)
503
+ or not getattr(cfg, "_web_interactive_hitl", True)
504
+ )
505
+ if auto_on:
506
+ lines.append(
507
+ "**Web UI permissions — AUTO-APPROVE is ON for this turn.** "
508
+ "Shell and mutating tools run immediately with **no** Yes/No card. "
509
+ "Never mention Approve, Deny, approval prompts, dialogs, or waiting for the user — "
510
+ "just run tools and report what you did."
511
+ )
512
+ else:
513
+ lines.append(
514
+ "**Web UI permissions — interactive.** Shell and mutating tools pause for an "
515
+ "**inline Yes/No card in the chat** (also pinned above the composer). "
516
+ "Do **not** invent Approve/Deny dialog text or tell the user to open a popup — "
517
+ "wait silently; after they tap Yes or No the turn continues."
518
+ )
501
519
 
502
520
  if mode == "agents":
503
521
  lines.extend(
@@ -920,6 +938,7 @@ def _apply_web_permissions(cfg: GemCodeConfig, req: dict[str, Any]) -> None:
920
938
  }
921
939
  object.__setattr__(cfg, "_web_auto_approve", normalized)
922
940
 
941
+ # UI is source of truth for web chat — do not leave env GEMCODE_SUPER_MODE stuck on.
923
942
  super_mode = bool(perms.get("super_mode"))
924
943
  if super_mode:
925
944
  cfg.super_mode = True
@@ -929,6 +948,7 @@ def _apply_web_permissions(cfg: GemCodeConfig, req: dict[str, Any]) -> None:
929
948
  object.__setattr__(cfg, "_web_interactive_hitl", False)
930
949
  return
931
950
 
951
+ cfg.super_mode = False
932
952
  if all(normalized.values()):
933
953
  cfg.yes_to_all = True
934
954
  cfg.interactive_permission_ask = False
@@ -946,13 +966,33 @@ def _apply_web_permissions(cfg: GemCodeConfig, req: dict[str, Any]) -> None:
946
966
 
947
967
 
948
968
  def _configure_web_permissions(cfg: GemCodeConfig, req: dict[str, Any]) -> None:
949
- """Web chat defaults to interactive Approve/Deny unless everything is auto-approved."""
969
+ """Web chat HITL follows the UI request, not process-level GEMCODE_SUPER_MODE.
970
+
971
+ Hosted tenant images historically set ``GEMCODE_SUPER_MODE=1`` so mesh/jobs are
972
+ unattended. That must not silently skip Yes/No cards when the user has
973
+ Auto-approve off in the web UI. Mesh workers still use
974
+ ``GEMCODE_MESH_WORKER_UNATTENDED`` independently.
975
+ """
950
976
  object.__setattr__(cfg, "_gemcode_web_sse", True)
951
- _apply_web_permissions(cfg, req)
952
- if not isinstance(req.get("permissions"), dict):
977
+
978
+ perms = req.get("permissions")
979
+ has_perms = isinstance(perms, dict)
980
+
981
+ # Clear env-applied super mode before applying the UI payload.
982
+ if not (has_perms and bool(perms.get("super_mode"))):
983
+ cfg.super_mode = False
953
984
  cfg.yes_to_all = False
954
985
  cfg.interactive_permission_ask = True
955
986
  object.__setattr__(cfg, "_web_interactive_hitl", True)
987
+
988
+ if has_perms:
989
+ _apply_web_permissions(cfg, req)
990
+ else:
991
+ cfg.super_mode = False
992
+ cfg.yes_to_all = False
993
+ cfg.interactive_permission_ask = True
994
+ object.__setattr__(cfg, "_web_interactive_hitl", True)
995
+
956
996
  _ensure_web_hitl(cfg)
957
997
  if os.environ.get("GEMCODE_WEB_YES_TO_ALL", "").lower() in ("1", "true", "yes", "on"):
958
998
  cfg.yes_to_all = True
@@ -961,8 +1001,9 @@ def _configure_web_permissions(cfg: GemCodeConfig, req: dict[str, Any]) -> None:
961
1001
 
962
1002
 
963
1003
  def _ensure_web_hitl(cfg: GemCodeConfig) -> None:
964
- """Web chat always uses the UI approval bridge when tools are not fully auto-approved."""
1004
+ """Web chat uses the UI approval bridge when tools are not fully auto-approved."""
965
1005
  if getattr(cfg, "yes_to_all", False) or getattr(cfg, "super_mode", False):
1006
+ object.__setattr__(cfg, "_web_interactive_hitl", False)
966
1007
  return
967
1008
  cfg.interactive_permission_ask = True
968
1009
  object.__setattr__(cfg, "_web_interactive_hitl", True)
@@ -1246,6 +1287,10 @@ async def run_adapter(req: dict[str, Any]) -> None:
1246
1287
  "model": model,
1247
1288
  "project_root": str(cfg.project_root),
1248
1289
  "workspace_mode": workspace_mode,
1290
+ "hitl_interactive": bool(getattr(cfg, "_web_interactive_hitl", False)),
1291
+ "auto_approve": bool(
1292
+ getattr(cfg, "yes_to_all", False) or getattr(cfg, "super_mode", False)
1293
+ ),
1249
1294
  **({"skill": resolved.skill_name} if resolved.skill_name else {}),
1250
1295
  }
1251
1296
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.23
3
+ Version: 0.4.25
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -187,6 +187,7 @@ tests/test_multimodal_input.py
187
187
  tests/test_output_styles_and_rules.py
188
188
  tests/test_paths.py
189
189
  tests/test_permissions.py
190
+ tests/test_preview_proxy.py
190
191
  tests/test_prompt_suggestions.py
191
192
  tests/test_repl_commands.py
192
193
  tests/test_repl_slash.py
@@ -200,6 +201,7 @@ tests/test_token_budget.py
200
201
  tests/test_tool_context_circulation.py
201
202
  tests/test_tools.py
202
203
  tests/test_tools_inspector.py
204
+ tests/test_web_permissions_hitl.py
203
205
  tests/test_web_server.py
204
206
  tests/test_web_sse_adapter.py
205
207
  tests/test_workspace_hints.py
@@ -0,0 +1,21 @@
1
+ """Tests for preview proxy path parsing."""
2
+
3
+ from gemcode.web.preview_api import parse_preview_proxy_path
4
+
5
+
6
+ def test_parse_preview_proxy_root() -> None:
7
+ assert parse_preview_proxy_path("/api/preview/proxy/8000") == (8000, "/")
8
+ assert parse_preview_proxy_path("/api/preview/proxy/8000/") == (8000, "/")
9
+
10
+
11
+ def test_parse_preview_proxy_subpath() -> None:
12
+ assert parse_preview_proxy_path("/api/preview/proxy/5173/assets/app.js") == (
13
+ 5173,
14
+ "/assets/app.js",
15
+ )
16
+
17
+
18
+ def test_parse_preview_proxy_invalid() -> None:
19
+ assert parse_preview_proxy_path("/api/preview") is None
20
+ assert parse_preview_proxy_path("/api/preview/proxy/") is None
21
+ assert parse_preview_proxy_path("/api/preview/proxy/abc") is None
@@ -0,0 +1,71 @@
1
+ """Web UI permissions must override process-level GEMCODE_SUPER_MODE."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from gemcode.config import GemCodeConfig, apply_super_mode
8
+ from gemcode.web import sse_adapter
9
+
10
+
11
+ def test_web_permissions_clear_env_super_mode(tmp_path: Path, monkeypatch) -> None:
12
+ monkeypatch.setenv("GEMCODE_SUPER_MODE", "1")
13
+ cfg = GemCodeConfig(project_root=tmp_path)
14
+ assert cfg.super_mode is True
15
+ apply_super_mode(cfg)
16
+ assert cfg.yes_to_all is True
17
+
18
+ sse_adapter._configure_web_permissions(
19
+ cfg,
20
+ {
21
+ "permissions": {
22
+ "super_mode": False,
23
+ "auto_approve": {
24
+ "bash": False,
25
+ "file_read": False,
26
+ "file_write": False,
27
+ "web_search": False,
28
+ },
29
+ }
30
+ },
31
+ )
32
+ assert cfg.super_mode is False
33
+ assert cfg.yes_to_all is False
34
+ assert getattr(cfg, "_web_interactive_hitl") is True
35
+
36
+
37
+ def test_web_permissions_ui_super_mode_on(tmp_path: Path, monkeypatch) -> None:
38
+ monkeypatch.delenv("GEMCODE_SUPER_MODE", raising=False)
39
+ cfg = GemCodeConfig(project_root=tmp_path)
40
+ cfg.super_mode = False
41
+ cfg.yes_to_all = False
42
+
43
+ sse_adapter._configure_web_permissions(
44
+ cfg,
45
+ {"permissions": {"super_mode": True, "auto_approve": {}}},
46
+ )
47
+ assert cfg.super_mode is True
48
+ assert cfg.yes_to_all is True
49
+ assert getattr(cfg, "_web_interactive_hitl") is False
50
+
51
+
52
+ def test_inject_web_context_auto_approve_copy(tmp_path: Path) -> None:
53
+ cfg = GemCodeConfig(project_root=tmp_path)
54
+ cfg.yes_to_all = True
55
+ object.__setattr__(cfg, "_web_workspace_mode", "code")
56
+ object.__setattr__(cfg, "_web_interactive_hitl", False)
57
+ out = sse_adapter._inject_web_code_context(cfg, "hello", {})
58
+ assert "AUTO-APPROVE is ON" in out
59
+ assert "Never mention Approve" in out
60
+ assert "inline Yes/No card" not in out
61
+
62
+
63
+ def test_inject_web_context_interactive_copy(tmp_path: Path) -> None:
64
+ cfg = GemCodeConfig(project_root=tmp_path)
65
+ cfg.super_mode = False
66
+ cfg.yes_to_all = False
67
+ object.__setattr__(cfg, "_web_workspace_mode", "code")
68
+ object.__setattr__(cfg, "_web_interactive_hitl", True)
69
+ out = sse_adapter._inject_web_code_context(cfg, "hello", {})
70
+ assert "inline Yes/No card" in out
71
+ assert "AUTO-APPROVE is ON" not in out
@@ -1,82 +0,0 @@
1
- """Detect local dev servers for in-UI localhost preview."""
2
-
3
- from __future__ import annotations
4
-
5
- import socket
6
- from typing import Any
7
- from urllib.error import URLError
8
- from urllib.request import Request, urlopen
9
-
10
- # Common dev-server ports (GemCode web UI uses 3002; API 3001)
11
- COMMON_DEV_PORTS = (
12
- 3000,
13
- 3003,
14
- 3004,
15
- 5173,
16
- 5174,
17
- 8080,
18
- 8000,
19
- 4200,
20
- 5000,
21
- 4321,
22
- 8888,
23
- )
24
-
25
-
26
- def _port_open(host: str, port: int, *, timeout: float = 0.2) -> bool:
27
- try:
28
- with socket.create_connection((host, port), timeout=timeout):
29
- return True
30
- except OSError:
31
- return False
32
-
33
-
34
- def _probe_http(url: str, *, timeout: float = 1.5) -> dict[str, Any] | None:
35
- try:
36
- req = Request(url, headers={"User-Agent": "GemCode-Web-Preview/1.0"})
37
- with urlopen(req, timeout=timeout) as resp:
38
- ctype = (resp.headers.get("Content-Type") or "").lower()
39
- return {
40
- "ok": True,
41
- "status": resp.status,
42
- "content_type": ctype,
43
- }
44
- except (URLError, OSError, TimeoutError, ValueError):
45
- return None
46
-
47
-
48
- def scan_local_preview_ports(
49
- *,
50
- host: str = "127.0.0.1",
51
- extra_ports: list[int] | None = None,
52
- ) -> list[dict[str, Any]]:
53
- """Return listening HTTP-ish ports on localhost."""
54
- ports: list[int] = list(COMMON_DEV_PORTS)
55
- if extra_ports:
56
- for p in extra_ports:
57
- if isinstance(p, int) and 1 < p < 65536 and p not in ports:
58
- ports.append(p)
59
-
60
- found: list[dict[str, Any]] = []
61
- for port in ports:
62
- if not _port_open(host, port):
63
- continue
64
- url = f"http://{host}:{port}"
65
- probe = _probe_http(url)
66
- found.append(
67
- {
68
- "port": port,
69
- "url": url,
70
- "reachable": probe is not None,
71
- "content_type": (probe or {}).get("content_type"),
72
- }
73
- )
74
- return found
75
-
76
-
77
- def handle_preview_get(kind: str) -> tuple[int, dict[str, Any]]:
78
- k = (kind or "ports").strip().lower()
79
- if k == "ports":
80
- ports = scan_local_preview_ports()
81
- return 200, {"ok": True, "ports": ports, "host": "127.0.0.1"}
82
- return 400, {"ok": False, "error": f"Unknown preview kind: {kind}"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes