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.
- {gemcode-0.4.21/src/gemcode.egg-info → gemcode-0.4.22}/PKG-INFO +1 -1
- {gemcode-0.4.21 → gemcode-0.4.22}/pyproject.toml +1 -1
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/repl_commands.py +1 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/repl_slash.py +28 -0
- gemcode-0.4.22/src/gemcode/web/files_api.py +280 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/server.py +36 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/sse_adapter.py +1 -1
- {gemcode-0.4.21 → gemcode-0.4.22/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode.egg-info/SOURCES.txt +2 -0
- gemcode-0.4.22/tests/test_files_api.py +54 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/LICENSE +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/MANIFEST.in +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/README.md +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/setup.cfg +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/__init__.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/__main__.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/a2a_bridge.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/agent.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/agent_habits.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/agent_intelligence.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/agent_mesh.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/agent_triggers.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/audit.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/automations.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/autotune.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/cli.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/codebase_awareness.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/compaction.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/config.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/credentials.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/delegation_learning.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/event_bus.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/fleet_reports.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/hooks.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/interactions.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/invoke.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/kaira_client.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/kaira_ipc.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/learning.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/limits.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/org.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/paths.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/permissions.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/pricing.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/config.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/refine.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/rules.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/self_healing.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/session_store.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/skills.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/thinking.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tool_synthesis.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/automations_tools.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/org_tools.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/trust.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/version.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/vertex.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/wal.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/chat_skills.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/customize_api.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/fleet_api.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/hitl_bridge.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/preview_api.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/project_root.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/runtime_api.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/serve_bind.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/serve_state.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/sessions_api.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/terminal_api.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/web_config_api.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/web/workspace_panel_api.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_add_dir.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_agent_habits.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_agent_mesh.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_autocompact.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_automations.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_capability_routing.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_cli_init.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_context_budget.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_context_warning.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_credentials.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_event_bus.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_fleet_reports.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_hosted_tenant.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_kaira_ipc_paths.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_modality_tools.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_model_errors.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_model_routing.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_paths.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_permissions.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_repl_commands.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_repl_slash.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_session_runtime_cache.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_skills.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_slash_commands.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_thinking_config.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_token_budget.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_tools.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_web_server.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.4.21 → gemcode-0.4.22}/tests/test_workspace_hints.py +0 -0
|
@@ -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-
|
|
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")
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|