gemcode 0.4.21__tar.gz → 0.4.22__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 (201) hide show
  1. {gemcode-0.4.21/src/gemcode.egg-info → gemcode-0.4.22}/PKG-INFO +1 -1
  2. {gemcode-0.4.21 → gemcode-0.4.22}/pyproject.toml +1 -1
  3. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/repl_commands.py +1 -0
  4. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/repl_slash.py +28 -0
  5. gemcode-0.4.22/src/gemcode/web/files_api.py +280 -0
  6. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/server.py +36 -0
  7. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/sse_adapter.py +1 -1
  8. {gemcode-0.4.21 → gemcode-0.4.22/src/gemcode.egg-info}/PKG-INFO +1 -1
  9. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode.egg-info/SOURCES.txt +2 -0
  10. gemcode-0.4.22/tests/test_files_api.py +54 -0
  11. {gemcode-0.4.21 → gemcode-0.4.22}/LICENSE +0 -0
  12. {gemcode-0.4.21 → gemcode-0.4.22}/MANIFEST.in +0 -0
  13. {gemcode-0.4.21 → gemcode-0.4.22}/README.md +0 -0
  14. {gemcode-0.4.21 → gemcode-0.4.22}/setup.cfg +0 -0
  15. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/__init__.py +0 -0
  16. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/__main__.py +0 -0
  17. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/a2a_bridge.py +0 -0
  18. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/agent.py +0 -0
  19. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/agent_habits.py +0 -0
  20. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/agent_intelligence.py +0 -0
  21. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/agent_mesh.py +0 -0
  22. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/agent_triggers.py +0 -0
  23. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/audit.py +0 -0
  24. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/autocompact.py +0 -0
  25. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/automations.py +0 -0
  26. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/autotune.py +0 -0
  27. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/callbacks.py +0 -0
  28. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/capability_routing.py +0 -0
  29. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/checkpoints.py +0 -0
  30. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/cli.py +0 -0
  31. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/codebase_awareness.py +0 -0
  32. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/compaction.py +0 -0
  33. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/computer_use/__init__.py +0 -0
  34. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/computer_use/browser_computer.py +0 -0
  35. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/config.py +0 -0
  36. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/context_budget.py +0 -0
  37. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/context_warning.py +0 -0
  38. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/credentials.py +0 -0
  39. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/curated_memory.py +0 -0
  40. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/delegation_learning.py +0 -0
  41. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/dynamic_policy.py +0 -0
  42. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/evals/harness.py +0 -0
  43. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/event_bus.py +0 -0
  44. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/fleet_reports.py +0 -0
  45. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/hitl_session.py +0 -0
  46. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/hooks.py +0 -0
  47. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/ide_protocol.py +0 -0
  48. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/ide_stdio.py +0 -0
  49. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/intent_classifier.py +0 -0
  50. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/interactions.py +0 -0
  51. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/invoke.py +0 -0
  52. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/kaira_client.py +0 -0
  53. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/kaira_daemon.py +0 -0
  54. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/kaira_ipc.py +0 -0
  55. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/kaira_job_store.py +0 -0
  56. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/learning.py +0 -0
  57. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/limits.py +0 -0
  58. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/live_audio_engine.py +0 -0
  59. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/logging_config.py +0 -0
  60. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/mcp_loader.py +0 -0
  61. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/memory/__init__.py +0 -0
  62. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/memory/embedding_memory_service.py +0 -0
  63. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/memory/file_memory_service.py +0 -0
  64. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/modality_tools.py +0 -0
  65. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/model_errors.py +0 -0
  66. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/model_routing.py +0 -0
  67. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/multimodal_input.py +0 -0
  68. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/openapi_loader.py +0 -0
  69. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/org.py +0 -0
  70. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/output_styles.py +0 -0
  71. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/paths.py +0 -0
  72. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/permissions.py +0 -0
  73. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/plugins/__init__.py +0 -0
  74. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  75. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  76. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/policy_profile.py +0 -0
  77. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/pricing.py +0 -0
  78. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/prompt_suggestions.py +0 -0
  79. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/__init__.py +0 -0
  80. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/config.py +0 -0
  81. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/deps.py +0 -0
  82. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/engine.py +0 -0
  83. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/stop_hooks.py +0 -0
  84. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/token_budget.py +0 -0
  85. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/transitions.py +0 -0
  86. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query_sanitizer.py +0 -0
  87. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/refine.py +0 -0
  88. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/review_agent.py +0 -0
  89. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/rules.py +0 -0
  90. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/self_healing.py +0 -0
  91. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/session_runtime.py +0 -0
  92. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/session_store.py +0 -0
  93. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/session_summariser.py +0 -0
  94. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/skills.py +0 -0
  95. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/slash_commands.py +0 -0
  96. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/thinking.py +0 -0
  97. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tool_prompt_manifest.py +0 -0
  98. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tool_registry.py +0 -0
  99. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tool_result_store.py +0 -0
  100. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tool_synthesis.py +0 -0
  101. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/__init__.py +0 -0
  102. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/automations_tools.py +0 -0
  103. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/bash.py +0 -0
  104. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/browser.py +0 -0
  105. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/compress_memory.py +0 -0
  106. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/curated_memory.py +0 -0
  107. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/edit.py +0 -0
  108. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/filesystem.py +0 -0
  109. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/notebook.py +0 -0
  110. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/notes.py +0 -0
  111. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/org_tools.py +0 -0
  112. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/repo_map.py +0 -0
  113. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/search.py +0 -0
  114. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/shell.py +0 -0
  115. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/shell_gate.py +0 -0
  116. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/skills.py +0 -0
  117. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/subtask.py +0 -0
  118. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/tasks.py +0 -0
  119. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/think.py +0 -0
  120. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/todo.py +0 -0
  121. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/user_choice.py +0 -0
  122. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/veomem_tools.py +0 -0
  123. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/web.py +0 -0
  124. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/web_search.py +0 -0
  125. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools_inspector.py +0 -0
  126. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/trust.py +0 -0
  127. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tui/input_handler.py +0 -0
  128. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tui/scrollback.py +0 -0
  129. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tui/spinner.py +0 -0
  130. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tui/welcome_banner.py +0 -0
  131. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tui/welcome_rich.py +0 -0
  132. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/veomem_bridge.py +0 -0
  133. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/version.py +0 -0
  134. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/vertex.py +0 -0
  135. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/wal.py +0 -0
  136. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/__init__.py +0 -0
  137. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/chat_skills.py +0 -0
  138. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/customize_api.py +0 -0
  139. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/fleet_api.py +0 -0
  140. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/hitl_bridge.py +0 -0
  141. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/preview_api.py +0 -0
  142. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/project_root.py +0 -0
  143. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/runtime_api.py +0 -0
  144. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/serve_bind.py +0 -0
  145. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/serve_state.py +0 -0
  146. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/sessions_api.py +0 -0
  147. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/terminal_api.py +0 -0
  148. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/terminal_repl.py +0 -0
  149. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/web_config_api.py +0 -0
  150. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/web_sse_compat.py +0 -0
  151. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/workspace_panel_api.py +0 -0
  152. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/workspace_hints.py +0 -0
  153. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode.egg-info/dependency_links.txt +0 -0
  154. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode.egg-info/entry_points.txt +0 -0
  155. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode.egg-info/requires.txt +0 -0
  156. {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode.egg-info/top_level.txt +0 -0
  157. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_add_dir.py +0 -0
  158. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_agent_habits.py +0 -0
  159. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_agent_instruction.py +0 -0
  160. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_agent_mesh.py +0 -0
  161. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_autocompact.py +0 -0
  162. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_automations.py +0 -0
  163. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_capability_routing.py +0 -0
  164. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_checkpoint_diff_command.py +0 -0
  165. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_cli_init.py +0 -0
  166. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_compress_memory_tool.py +0 -0
  167. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_computer_use_permissions.py +0 -0
  168. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_context_budget.py +0 -0
  169. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_context_warning.py +0 -0
  170. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_credentials.py +0 -0
  171. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_eval_harness_layout.py +0 -0
  172. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_event_bus.py +0 -0
  173. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_fleet_reports.py +0 -0
  174. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_hosted_tenant.py +0 -0
  175. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_ide_stdio_attachments.py +0 -0
  176. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_interactive_permission_ask.py +0 -0
  177. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_kaira_ipc_paths.py +0 -0
  178. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_kaira_scheduler.py +0 -0
  179. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_modality_tools.py +0 -0
  180. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_model_error_retry.py +0 -0
  181. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_model_errors.py +0 -0
  182. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_model_routing.py +0 -0
  183. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_multimodal_input.py +0 -0
  184. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_output_styles_and_rules.py +0 -0
  185. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_paths.py +0 -0
  186. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_permissions.py +0 -0
  187. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_prompt_suggestions.py +0 -0
  188. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_repl_commands.py +0 -0
  189. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_repl_slash.py +0 -0
  190. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_session_runtime_cache.py +0 -0
  191. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_skills.py +0 -0
  192. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_slash_commands.py +0 -0
  193. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_slash_completion_registry.py +0 -0
  194. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_thinking_config.py +0 -0
  195. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_token_budget.py +0 -0
  196. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_tool_context_circulation.py +0 -0
  197. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_tools.py +0 -0
  198. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_tools_inspector.py +0 -0
  199. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_web_server.py +0 -0
  200. {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_web_sse_adapter.py +0 -0
  201. {gemcode-0.4.21 → gemcode-0.4.22}/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.21
3
+ Version: 0.4.22
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.21"
7
+ version = "0.4.22"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -252,6 +252,7 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
252
252
  ("runtime", "Fleet socket status · gemcode runtime · attach/connect"),
253
253
  ("serve", "HTTP API for web/custom UIs — gemcode serve on :3001"),
254
254
  ("hosted", "Hosted multi-tenant lock — GEMCODE_HOSTED_TENANT_ROOT status"),
255
+ ("workspace", "Workspace root + hosted file API routes"),
255
256
  ("bus", "Runtime bus — send/publish lightweight messages over IPC"),
256
257
  ("inbox", "Bus inbox filters for this UI (to/topics)"),
257
258
  ("fleet", "Fleet inbox — /fleet show | digest (habits / mesh reports)"),
@@ -2683,6 +2683,34 @@ async def process_repl_slash(
2683
2683
  out()
2684
2684
  return ReplSlashResult(skip_model_turn=True)
2685
2685
 
2686
+ # ── /workspace (hosted tenant + file API) ───────────────────────────────
2687
+ if name == "workspace":
2688
+ from gemcode.web.project_root import hosted_tenant_root
2689
+
2690
+ args_w = (sc.args or "").strip().lower()
2691
+ if args_w in ("help", "?"):
2692
+ out("Workspace / hosted tenant:")
2693
+ out(" /workspace Show active workspace root and hosted lock")
2694
+ out(" /workspace files File API routes on gemcode serve")
2695
+ out("")
2696
+ out("Hosted tenants use /api/files, /api/files/read, /api/files/write on gemcode serve.")
2697
+ out("See docs/hosted.md")
2698
+ out()
2699
+ return ReplSlashResult(skip_model_turn=True)
2700
+
2701
+ locked = hosted_tenant_root()
2702
+ out(f" project_root: {cfg.project_root.resolve()}")
2703
+ if locked is not None:
2704
+ out(f" hosted lock: {locked}")
2705
+ else:
2706
+ out(" hosted lock: (not set — local mode)")
2707
+ if args_w == "files":
2708
+ out(" GET /api/files — file tree")
2709
+ out(" GET /api/files/read — read file")
2710
+ out(" POST /api/files/write — write file")
2711
+ out()
2712
+ return ReplSlashResult(skip_model_turn=True)
2713
+
2686
2714
  # ── /runtime ────────────────────────────────────────────────────────────
2687
2715
  if name == "runtime":
2688
2716
  from gemcode.kaira_ipc import fleet_manager_ipc_path_for_workspace
@@ -0,0 +1,280 @@
1
+ """Workspace file browser API for web UIs (local and hosted tenants)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import mimetypes
8
+ import os
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from gemcode.web.project_root import HostedTenantPathError, resolve_web_project_root
14
+
15
+ ALWAYS_HIDDEN = {
16
+ ".git",
17
+ "node_modules",
18
+ ".next",
19
+ ".turbo",
20
+ ".cache",
21
+ "__pycache__",
22
+ ".DS_Store",
23
+ "Thumbs.db",
24
+ }
25
+
26
+ MAX_READ_BYTES = 10 * 1024 * 1024
27
+ MAX_WRITE_BYTES = 5 * 1024 * 1024
28
+ MAX_TREE_DEPTH = 8
29
+
30
+
31
+ def _resolve_root(raw_path: str | None) -> Path:
32
+ return resolve_web_project_root(raw_path)
33
+
34
+
35
+ def _resolve_under_root(root: Path, relative: str) -> Path:
36
+ rel = (relative or "").strip().lstrip("/")
37
+ candidate = (root / rel).resolve()
38
+ try:
39
+ candidate.relative_to(root.resolve())
40
+ except ValueError as exc:
41
+ raise HostedTenantPathError(f"path is outside workspace: {candidate}") from exc
42
+ return candidate
43
+
44
+
45
+ def _git_status_map(root: Path) -> dict[str, str]:
46
+ try:
47
+ proc = subprocess.run(
48
+ ["git", "status", "--porcelain", "-u"],
49
+ cwd=str(root),
50
+ capture_output=True,
51
+ text=True,
52
+ timeout=5,
53
+ check=False,
54
+ )
55
+ except (OSError, subprocess.TimeoutExpired):
56
+ return {}
57
+ out: dict[str, str] = {}
58
+ for line in (proc.stdout or "").splitlines():
59
+ if not line:
60
+ continue
61
+ xy = line[:2].strip()
62
+ file_path = line[3:].strip().strip('"')
63
+ if not xy or not file_path:
64
+ continue
65
+ status = xy[0] if xy[0] not in (" ", "?") else xy[1] if xy[1] != " " else "?"
66
+ out[file_path] = "?" if status == "?" else status
67
+ return out
68
+
69
+
70
+ def _ignore_patterns(root: Path) -> list[str]:
71
+ p = root / ".gitignore"
72
+ if not p.is_file():
73
+ return []
74
+ try:
75
+ lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
76
+ except OSError:
77
+ return []
78
+ return [ln.strip() for ln in lines if ln.strip() and not ln.strip().startswith("#")]
79
+
80
+
81
+ def _matches_ignore(name: str, patterns: list[str]) -> bool:
82
+ for pattern in patterns:
83
+ p = pattern[1:] if pattern.startswith("/") else pattern
84
+ if p == name:
85
+ return True
86
+ if p.endswith("/") and p[:-1] == name:
87
+ return True
88
+ if "*" in p:
89
+ import re
90
+
91
+ rx = "^" + re.escape(p).replace(r"\*", "[^/]*") + "$"
92
+ if re.match(rx, name):
93
+ return True
94
+ return False
95
+
96
+
97
+ def _build_tree(
98
+ dir_path: Path,
99
+ root: Path,
100
+ *,
101
+ git_map: dict[str, str],
102
+ ignore_patterns: list[str],
103
+ show_ignored: bool,
104
+ depth: int = 0,
105
+ ) -> list[dict[str, Any]]:
106
+ if depth > MAX_TREE_DEPTH:
107
+ return []
108
+ try:
109
+ entries = sorted(dir_path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
110
+ except OSError:
111
+ return []
112
+ nodes: list[dict[str, Any]] = []
113
+ for entry in entries:
114
+ name = entry.name
115
+ if name in ALWAYS_HIDDEN:
116
+ continue
117
+ if not show_ignored and _matches_ignore(name, ignore_patterns):
118
+ continue
119
+ if not show_ignored and name.startswith(".") and name != ".env.example":
120
+ continue
121
+ rel = entry.relative_to(root).as_posix()
122
+ git_status = git_map.get(rel)
123
+ if entry.is_dir():
124
+ children = _build_tree(
125
+ entry,
126
+ root,
127
+ git_map=git_map,
128
+ ignore_patterns=ignore_patterns,
129
+ show_ignored=show_ignored,
130
+ depth=depth + 1,
131
+ )
132
+ nodes.append(
133
+ {
134
+ "name": name,
135
+ "path": rel,
136
+ "type": "directory",
137
+ "children": children,
138
+ "gitStatus": git_status,
139
+ }
140
+ )
141
+ elif entry.is_file():
142
+ nodes.append(
143
+ {
144
+ "name": name,
145
+ "path": rel,
146
+ "type": "file",
147
+ "gitStatus": git_status,
148
+ }
149
+ )
150
+ return nodes
151
+
152
+
153
+ def handle_files_tree_get(
154
+ raw_path: str | None,
155
+ *,
156
+ cwd: str = "",
157
+ show_ignored: bool = False,
158
+ ) -> tuple[int, dict[str, Any]]:
159
+ try:
160
+ root = _resolve_root(raw_path)
161
+ except HostedTenantPathError as exc:
162
+ return 403, {"error": str(exc)}
163
+ if not root.is_dir():
164
+ return 400, {"error": "workspace is not a directory"}
165
+ try:
166
+ resolved_cwd = _resolve_under_root(root, cwd)
167
+ except HostedTenantPathError as exc:
168
+ return 403, {"error": str(exc)}
169
+ if not resolved_cwd.is_dir():
170
+ return 400, {"error": "Invalid cwd"}
171
+
172
+ git_map = _git_status_map(root)
173
+ ignore_patterns = _ignore_patterns(root)
174
+ tree = _build_tree(
175
+ resolved_cwd,
176
+ root,
177
+ git_map=git_map,
178
+ ignore_patterns=ignore_patterns,
179
+ show_ignored=show_ignored,
180
+ )
181
+ rel = resolved_cwd.relative_to(root)
182
+ segments = [] if str(rel) == "." else str(rel).split("/")
183
+ breadcrumbs = [root.name or str(root), *segments]
184
+ breadcrumb_rels = [""] + [
185
+ "/".join(segments[: i + 1]) for i in range(len(segments))
186
+ ]
187
+ return 200, {
188
+ "tree": tree,
189
+ "root": str(root),
190
+ "cwd": cwd,
191
+ "breadcrumbs": breadcrumbs,
192
+ "breadcrumbRels": breadcrumb_rels,
193
+ }
194
+
195
+
196
+ def handle_files_read_get(
197
+ raw_path: str | None,
198
+ file_path: str,
199
+ ) -> tuple[int, dict[str, Any]]:
200
+ if not file_path.strip():
201
+ return 400, {"error": "path parameter required"}
202
+ try:
203
+ root = _resolve_root(raw_path)
204
+ resolved = _resolve_under_root(root, file_path)
205
+ except HostedTenantPathError as exc:
206
+ return 403, {"error": str(exc)}
207
+ if not resolved.is_file():
208
+ return 404 if not resolved.exists() else 400, {"error": "path is not a file"}
209
+ try:
210
+ size = resolved.stat().st_size
211
+ except OSError as exc:
212
+ return 404, {"error": str(exc)}
213
+ if size > MAX_READ_BYTES:
214
+ return 413, {"error": f"File exceeds maximum readable size ({MAX_READ_BYTES // 1024 // 1024} MB)"}
215
+
216
+ ext = resolved.suffix.lower().lstrip(".")
217
+ mime, _ = mimetypes.guess_type(str(resolved))
218
+
219
+ if ext in ("docx", "doc", "pdf"):
220
+ data = base64.b64encode(resolved.read_bytes()).decode("ascii")
221
+ kind = "pdf" if ext == "pdf" else "docx"
222
+ return 200, {
223
+ "content": data,
224
+ "isImage": False,
225
+ "kind": kind,
226
+ "mimeType": mime or "application/octet-stream",
227
+ "size": size,
228
+ "modified": resolved.stat().st_mtime,
229
+ }
230
+
231
+ if ext in ("png", "jpg", "jpeg", "gif", "webp", "bmp", "ico"):
232
+ mime = mime or f"image/{ext}"
233
+ data = base64.b64encode(resolved.read_bytes()).decode("ascii")
234
+ return 200, {
235
+ "content": f"data:{mime};base64,{data}",
236
+ "isImage": True,
237
+ "size": size,
238
+ "modified": resolved.stat().st_mtime,
239
+ }
240
+
241
+ try:
242
+ probe = resolved.read_bytes()[:512]
243
+ if b"\x00" in probe:
244
+ return 415, {"error": "Binary file cannot be read as text"}
245
+ content = resolved.read_text(encoding="utf-8")
246
+ except OSError as exc:
247
+ return 404, {"error": str(exc)}
248
+ except UnicodeDecodeError:
249
+ return 415, {"error": "Binary file cannot be read as text"}
250
+
251
+ return 200, {
252
+ "content": content,
253
+ "isImage": ext == "svg",
254
+ "size": size,
255
+ "modified": resolved.stat().st_mtime,
256
+ }
257
+
258
+
259
+ def handle_files_write_post(data: dict[str, Any], raw_path: str | None) -> tuple[int, dict[str, Any]]:
260
+ file_path = str(data.get("path") or "").strip()
261
+ if not file_path:
262
+ return 400, {"ok": False, "error": "path is required"}
263
+ content = data.get("content")
264
+ if content is None:
265
+ content = ""
266
+ if not isinstance(content, str):
267
+ return 400, {"ok": False, "error": "content must be a string"}
268
+ if len(content.encode("utf-8")) > MAX_WRITE_BYTES:
269
+ return 413, {"ok": False, "error": "content too large"}
270
+ try:
271
+ root = _resolve_root(raw_path)
272
+ resolved = _resolve_under_root(root, file_path)
273
+ except HostedTenantPathError as exc:
274
+ return 403, {"ok": False, "error": str(exc)}
275
+ try:
276
+ resolved.parent.mkdir(parents=True, exist_ok=True)
277
+ resolved.write_text(content, encoding="utf-8")
278
+ except OSError as exc:
279
+ return 500, {"ok": False, "error": str(exc)}
280
+ return 200, {"ok": True, "path": file_path}
@@ -250,6 +250,38 @@ def _build_handler(project_root: str) -> type[BaseHTTPRequestHandler]:
250
250
  return
251
251
 
252
252
  parsed = urlparse(self.path)
253
+ if parsed.path in ("/api/files", "/api/files/read"):
254
+ params = parse_qs(parsed.query)
255
+ raw_path = (params.get("path") or [root])[0]
256
+ try:
257
+ from gemcode.web.files_api import handle_files_read_get, handle_files_tree_get
258
+
259
+ if parsed.path == "/api/files/read":
260
+ file_path = (params.get("path") or [""])[0]
261
+ ws = (params.get("root") or [root])[0]
262
+ status, payload = handle_files_read_get(ws, file_path)
263
+ else:
264
+ cwd = (params.get("cwd") or [""])[0]
265
+ show_ignored = (params.get("showIgnored") or ["false"])[0].lower() == "true"
266
+ ws = (params.get("root") or [root])[0]
267
+ status, payload = handle_files_tree_get(
268
+ ws,
269
+ cwd=cwd,
270
+ show_ignored=show_ignored,
271
+ )
272
+ except HostedTenantPathError as exc:
273
+ status, payload = 403, {"error": str(exc)}
274
+ except Exception as exc:
275
+ status, payload = 500, {"error": f"{type(exc).__name__}: {exc}"}
276
+ body = json.dumps(payload, default=str).encode()
277
+ self.send_response(status)
278
+ self.send_header("Content-Type", "application/json; charset=utf-8")
279
+ self.send_header("Content-Length", str(len(body)))
280
+ self._cors()
281
+ self.end_headers()
282
+ self.wfile.write(body)
283
+ return
284
+
253
285
  fleet_get = {
254
286
  "/api/org": "org",
255
287
  "/api/habits": "habits",
@@ -353,6 +385,7 @@ def _build_handler(project_root: str) -> type[BaseHTTPRequestHandler]:
353
385
  "/api/settings/credentials": ("gemcode.web.customize_api", "handle_credentials_post"),
354
386
  "/api/runtime": ("gemcode.web.runtime_api", "handle_runtime_post"),
355
387
  "/api/terminal": ("gemcode.web.terminal_api", "handle_terminal_post"),
388
+ "/api/files/write": ("gemcode.web.files_api", "handle_files_write_post"),
356
389
  }
357
390
 
358
391
  if self.path in post_routes:
@@ -374,6 +407,9 @@ def _build_handler(project_root: str) -> type[BaseHTTPRequestHandler]:
374
407
  elif self.path == "/api/sessions":
375
408
  raw_path = str(data.get("path") or root).strip()
376
409
  status, payload = handler(raw_path, data)
410
+ elif self.path == "/api/files/write":
411
+ raw_path = str(data.get("root") or data.get("path") or root).strip()
412
+ status, payload = handler(data, raw_path)
377
413
  else:
378
414
  raw_path = str(data.get("path") or root).strip()
379
415
  status, payload = handler(data, raw_path)
@@ -1048,7 +1048,7 @@ def _apply_web_request_options(cfg: GemCodeConfig, req: dict[str, Any], workspac
1048
1048
  async def run_adapter(req: dict[str, Any]) -> None:
1049
1049
  messages = req.get("messages")
1050
1050
  requested_model = req.get("model")
1051
- model = requested_model or os.environ.get("GEMCODE_MODEL") or "gemini-2.5-flash"
1051
+ model = requested_model or os.environ.get("GEMCODE_MODEL") or "gemini-3.1-pro-preview"
1052
1052
 
1053
1053
  if not isinstance(messages, list):
1054
1054
  raise ValueError("messages must be a list")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.21
3
+ Version: 0.4.22
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -135,6 +135,7 @@ src/gemcode/tui/welcome_rich.py
135
135
  src/gemcode/web/__init__.py
136
136
  src/gemcode/web/chat_skills.py
137
137
  src/gemcode/web/customize_api.py
138
+ src/gemcode/web/files_api.py
138
139
  src/gemcode/web/fleet_api.py
139
140
  src/gemcode/web/hitl_bridge.py
140
141
  src/gemcode/web/preview_api.py
@@ -166,6 +167,7 @@ tests/test_context_warning.py
166
167
  tests/test_credentials.py
167
168
  tests/test_eval_harness_layout.py
168
169
  tests/test_event_bus.py
170
+ tests/test_files_api.py
169
171
  tests/test_fleet_reports.py
170
172
  tests/test_hosted_tenant.py
171
173
  tests/test_ide_stdio_attachments.py
@@ -0,0 +1,54 @@
1
+ """Tests for workspace files API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from gemcode.web.files_api import (
12
+ handle_files_read_get,
13
+ handle_files_tree_get,
14
+ handle_files_write_post,
15
+ )
16
+
17
+
18
+ @pytest.fixture
19
+ def workspace(tmp_path: Path) -> Path:
20
+ (tmp_path / "hello.txt").write_text("hi\n", encoding="utf-8")
21
+ (tmp_path / "src").mkdir()
22
+ (tmp_path / "src" / "main.py").write_text("print('ok')\n", encoding="utf-8")
23
+ return tmp_path
24
+
25
+
26
+ def test_files_tree_lists_workspace(workspace: Path) -> None:
27
+ status, payload = handle_files_tree_get(str(workspace))
28
+ assert status == 200
29
+ names = {n["name"] for n in payload["tree"]}
30
+ assert "hello.txt" in names
31
+ assert "src" in names
32
+
33
+
34
+ def test_files_read_and_write(workspace: Path) -> None:
35
+ status, payload = handle_files_read_get(str(workspace), "hello.txt")
36
+ assert status == 200
37
+ assert payload["content"] == "hi\n"
38
+
39
+ status, payload = handle_files_write_post(
40
+ {"path": "new.md", "content": "# test"},
41
+ str(workspace),
42
+ )
43
+ assert status == 200
44
+ assert payload["ok"] is True
45
+ assert (workspace / "new.md").read_text(encoding="utf-8") == "# test"
46
+
47
+
48
+ def test_files_reject_path_outside_hosted_root(
49
+ workspace: Path, monkeypatch: pytest.MonkeyPatch
50
+ ) -> None:
51
+ outside = tempfile.mkdtemp()
52
+ monkeypatch.setenv("GEMCODE_HOSTED_TENANT_ROOT", str(workspace))
53
+ status, payload = handle_files_read_get(str(workspace), f"../{Path(outside).name}/x")
54
+ assert status == 403
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