gemcode 0.3.78__tar.gz → 0.3.80__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 (150) hide show
  1. {gemcode-0.3.78/src/gemcode.egg-info → gemcode-0.3.80}/PKG-INFO +15 -1
  2. {gemcode-0.3.78 → gemcode-0.3.80}/README.md +14 -0
  3. {gemcode-0.3.78 → gemcode-0.3.80}/pyproject.toml +1 -1
  4. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/cli.py +31 -2
  5. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/config.py +3 -0
  6. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/ide_protocol.py +8 -0
  7. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/ide_stdio.py +143 -10
  8. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/invoke.py +21 -5
  9. gemcode-0.3.80/src/gemcode/multimodal_input.py +144 -0
  10. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/repl_commands.py +6 -0
  11. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/repl_slash.py +50 -0
  12. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tui/scrollback.py +14 -1
  13. {gemcode-0.3.78 → gemcode-0.3.80/src/gemcode.egg-info}/PKG-INFO +15 -1
  14. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode.egg-info/SOURCES.txt +3 -0
  15. gemcode-0.3.80/tests/test_ide_stdio_attachments.py +42 -0
  16. gemcode-0.3.80/tests/test_multimodal_input.py +64 -0
  17. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_slash_completion_registry.py +1 -0
  18. {gemcode-0.3.78 → gemcode-0.3.80}/LICENSE +0 -0
  19. {gemcode-0.3.78 → gemcode-0.3.80}/MANIFEST.in +0 -0
  20. {gemcode-0.3.78 → gemcode-0.3.80}/setup.cfg +0 -0
  21. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/__init__.py +0 -0
  22. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/__main__.py +0 -0
  23. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/agent.py +0 -0
  24. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/audit.py +0 -0
  25. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/autocompact.py +0 -0
  26. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/autotune.py +0 -0
  27. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/callbacks.py +0 -0
  28. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/capability_routing.py +0 -0
  29. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/checkpoints.py +0 -0
  30. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/compaction.py +0 -0
  31. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/computer_use/__init__.py +0 -0
  32. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/computer_use/browser_computer.py +0 -0
  33. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/context_budget.py +0 -0
  34. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/context_warning.py +0 -0
  35. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/credentials.py +0 -0
  36. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/curated_memory.py +0 -0
  37. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/dynamic_policy.py +0 -0
  38. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/evals/harness.py +0 -0
  39. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/hitl_session.py +0 -0
  40. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/hooks.py +0 -0
  41. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/intent_classifier.py +0 -0
  42. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/interactions.py +0 -0
  43. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/kaira_daemon.py +0 -0
  44. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/learning.py +0 -0
  45. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/limits.py +0 -0
  46. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/live_audio_engine.py +0 -0
  47. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/logging_config.py +0 -0
  48. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/mcp_loader.py +0 -0
  49. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/memory/__init__.py +0 -0
  50. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/memory/embedding_memory_service.py +0 -0
  51. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/memory/file_memory_service.py +0 -0
  52. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/modality_tools.py +0 -0
  53. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/model_errors.py +0 -0
  54. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/model_routing.py +0 -0
  55. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/openapi_loader.py +0 -0
  56. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/output_styles.py +0 -0
  57. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/paths.py +0 -0
  58. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/permissions.py +0 -0
  59. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/plugins/__init__.py +0 -0
  60. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  61. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  62. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/policy_profile.py +0 -0
  63. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/pricing.py +0 -0
  64. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/prompt_suggestions.py +0 -0
  65. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/query/__init__.py +0 -0
  66. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/query/config.py +0 -0
  67. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/query/deps.py +0 -0
  68. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/query/engine.py +0 -0
  69. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/query/stop_hooks.py +0 -0
  70. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/query/token_budget.py +0 -0
  71. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/query/transitions.py +0 -0
  72. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/refine.py +0 -0
  73. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/review_agent.py +0 -0
  74. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/rules.py +0 -0
  75. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/session_runtime.py +0 -0
  76. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/session_store.py +0 -0
  77. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/skills.py +0 -0
  78. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/slash_commands.py +0 -0
  79. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/thinking.py +0 -0
  80. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tool_prompt_manifest.py +0 -0
  81. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tool_registry.py +0 -0
  82. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tool_result_store.py +0 -0
  83. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/__init__.py +0 -0
  84. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/bash.py +0 -0
  85. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/browser.py +0 -0
  86. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/curated_memory.py +0 -0
  87. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/edit.py +0 -0
  88. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/filesystem.py +0 -0
  89. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/notebook.py +0 -0
  90. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/notes.py +0 -0
  91. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/repo_map.py +0 -0
  92. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/search.py +0 -0
  93. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/shell.py +0 -0
  94. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/shell_gate.py +0 -0
  95. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/skills.py +0 -0
  96. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/subtask.py +0 -0
  97. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/tasks.py +0 -0
  98. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/think.py +0 -0
  99. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/todo.py +0 -0
  100. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/web.py +0 -0
  101. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools/web_search.py +0 -0
  102. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tools_inspector.py +0 -0
  103. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/trust.py +0 -0
  104. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tui/input_handler.py +0 -0
  105. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tui/spinner.py +0 -0
  106. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tui/welcome_banner.py +0 -0
  107. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/tui/welcome_rich.py +0 -0
  108. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/version.py +0 -0
  109. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/vertex.py +0 -0
  110. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/web/__init__.py +0 -0
  111. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/web/sse_adapter.py +0 -0
  112. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/web/terminal_repl.py +0 -0
  113. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/web/web_sse_compat.py +0 -0
  114. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode/workspace_hints.py +0 -0
  115. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode.egg-info/dependency_links.txt +0 -0
  116. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode.egg-info/entry_points.txt +0 -0
  117. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode.egg-info/requires.txt +0 -0
  118. {gemcode-0.3.78 → gemcode-0.3.80}/src/gemcode.egg-info/top_level.txt +0 -0
  119. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_add_dir.py +0 -0
  120. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_agent_instruction.py +0 -0
  121. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_autocompact.py +0 -0
  122. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_capability_routing.py +0 -0
  123. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_checkpoint_diff_command.py +0 -0
  124. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_cli_init.py +0 -0
  125. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_computer_use_permissions.py +0 -0
  126. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_context_budget.py +0 -0
  127. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_context_warning.py +0 -0
  128. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_credentials.py +0 -0
  129. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_eval_harness_layout.py +0 -0
  130. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_interactive_permission_ask.py +0 -0
  131. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_kaira_scheduler.py +0 -0
  132. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_modality_tools.py +0 -0
  133. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_model_error_retry.py +0 -0
  134. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_model_errors.py +0 -0
  135. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_model_routing.py +0 -0
  136. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_output_styles_and_rules.py +0 -0
  137. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_paths.py +0 -0
  138. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_permissions.py +0 -0
  139. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_prompt_suggestions.py +0 -0
  140. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_repl_commands.py +0 -0
  141. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_repl_slash.py +0 -0
  142. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_skills.py +0 -0
  143. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_slash_commands.py +0 -0
  144. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_thinking_config.py +0 -0
  145. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_token_budget.py +0 -0
  146. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_tool_context_circulation.py +0 -0
  147. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_tools.py +0 -0
  148. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_tools_inspector.py +0 -0
  149. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_web_sse_adapter.py +0 -0
  150. {gemcode-0.3.78 → gemcode-0.3.80}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.78
3
+ Version: 0.3.80
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -330,6 +330,18 @@ Non-interactive environments (CI, pipes) must set `GOOGLE_API_KEY` explicitly an
330
330
  | `gemcode kaira` | Stdin-line → queued jobs scheduler (see [Kaira](#kaira-scheduler)). |
331
331
  | `gemcode ide --stdio` | **JSONL IDE protocol** on stdin/stdout for editor extensions (hidden entry; used by VS Code). |
332
332
 
333
+ **Files with a prompt (multimodal):** attach one or more files Gemini can read (images, **PDF**, audio, video, plain text, etc.) for that message only:
334
+
335
+ ```bash
336
+ gemcode -C . --attach ./screenshot.png "Why does this UI look wrong?"
337
+ gemcode -C . --attach ./report.pdf "What are the main conclusions?"
338
+ gemcode -C . --image a.png --image b.png "Compare these two layouts"
339
+ ```
340
+
341
+ (`--image` is an alias of `--attach`.)
342
+
343
+ In the **REPL / TUI**, queue files for the **next** message: `/attach path/to.pdf` (repeat for multiple), then type your question. Use `/attach list`, `/attach clear`. Aliases: `/image`, `/img`, `/file`.
344
+
333
345
  ---
334
346
 
335
347
  ## Main CLI flags
@@ -349,6 +361,7 @@ Non-interactive environments (CI, pipes) must set `GOOGLE_API_KEY` explicitly an
349
361
  | `--tool-combination-mode` | Gemini 3 **tool context circulation**: `deep_research\|always\|never\|auto`. |
350
362
  | `--mcp` | Load MCP toolsets from `.gemcode/mcp.json` (requires `pip install -e ".[mcp]"`). |
351
363
  | `--max-llm-calls` | Cap model↔tool iterations (`RunConfig.max_llm_calls`). |
364
+ | `--attach PATH` | Attach file(s) for **this** CLI message only (repeat flag). Gemini-supported MIME (e.g. images, PDF, audio, video). Default max ~20 MiB each (`GEMCODE_MAX_ATTACHMENT_BYTES`). Alias: `--image`. |
352
365
 
353
366
  Kaira and `live-audio` accept overlapping options (project root, `--yes`, research/embeddings, etc.); run `gemcode kaira -h` / `gemcode live-audio -h` for full lists.
354
367
 
@@ -472,6 +485,7 @@ In interactive mode, lines starting with `/` are **slash commands** (see `repl_c
472
485
 
473
486
  | Command | Purpose |
474
487
  |---------|---------|
488
+ | `/attach <path>`, `/attach list`, `/attach clear` | Queue file(s) for the **next** message (Gemini multimodal). Aliases: **`/image`**, **`/img`**, **`/file`**. |
475
489
  | `/trust`, `/trust on`, `/trust off` | Show, grant, or revoke **workspace trust** for the project root (stored in `~/.gemcode/trust.json`; required for file/shell/git tools). |
476
490
  | `/init` \| `/init force` | Analyze the repo and generate or overwrite `GEMINI.md`. |
477
491
  | `/cost` | Token usage and estimated cost for the session. |
@@ -141,6 +141,18 @@ Non-interactive environments (CI, pipes) must set `GOOGLE_API_KEY` explicitly an
141
141
  | `gemcode kaira` | Stdin-line → queued jobs scheduler (see [Kaira](#kaira-scheduler)). |
142
142
  | `gemcode ide --stdio` | **JSONL IDE protocol** on stdin/stdout for editor extensions (hidden entry; used by VS Code). |
143
143
 
144
+ **Files with a prompt (multimodal):** attach one or more files Gemini can read (images, **PDF**, audio, video, plain text, etc.) for that message only:
145
+
146
+ ```bash
147
+ gemcode -C . --attach ./screenshot.png "Why does this UI look wrong?"
148
+ gemcode -C . --attach ./report.pdf "What are the main conclusions?"
149
+ gemcode -C . --image a.png --image b.png "Compare these two layouts"
150
+ ```
151
+
152
+ (`--image` is an alias of `--attach`.)
153
+
154
+ In the **REPL / TUI**, queue files for the **next** message: `/attach path/to.pdf` (repeat for multiple), then type your question. Use `/attach list`, `/attach clear`. Aliases: `/image`, `/img`, `/file`.
155
+
144
156
  ---
145
157
 
146
158
  ## Main CLI flags
@@ -160,6 +172,7 @@ Non-interactive environments (CI, pipes) must set `GOOGLE_API_KEY` explicitly an
160
172
  | `--tool-combination-mode` | Gemini 3 **tool context circulation**: `deep_research\|always\|never\|auto`. |
161
173
  | `--mcp` | Load MCP toolsets from `.gemcode/mcp.json` (requires `pip install -e ".[mcp]"`). |
162
174
  | `--max-llm-calls` | Cap model↔tool iterations (`RunConfig.max_llm_calls`). |
175
+ | `--attach PATH` | Attach file(s) for **this** CLI message only (repeat flag). Gemini-supported MIME (e.g. images, PDF, audio, video). Default max ~20 MiB each (`GEMCODE_MAX_ATTACHMENT_BYTES`). Alias: `--image`. |
163
176
 
164
177
  Kaira and `live-audio` accept overlapping options (project root, `--yes`, research/embeddings, etc.); run `gemcode kaira -h` / `gemcode live-audio -h` for full lists.
165
178
 
@@ -283,6 +296,7 @@ In interactive mode, lines starting with `/` are **slash commands** (see `repl_c
283
296
 
284
297
  | Command | Purpose |
285
298
  |---------|---------|
299
+ | `/attach <path>`, `/attach list`, `/attach clear` | Queue file(s) for the **next** message (Gemini multimodal). Aliases: **`/image`**, **`/img`**, **`/file`**. |
286
300
  | `/trust`, `/trust on`, `/trust off` | Show, grant, or revoke **workspace trust** for the project root (stored in `~/.gemcode/trust.json`; required for file/shell/git tools). |
287
301
  | `/init` \| `/init force` | Analyze the repo and generate or overwrite `GEMINI.md`. |
288
302
  | `/cost` | Token usage and estimated cost for the session. |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.78"
7
+ version = "0.3.80"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -147,7 +147,12 @@ def _initialize_gemcode_project(cfg: GemCodeConfig) -> None:
147
147
 
148
148
 
149
149
  async def _run_prompt(
150
- cfg: GemCodeConfig, prompt: str, session_id: str, *, use_mcp: bool
150
+ cfg: GemCodeConfig,
151
+ prompt: str,
152
+ session_id: str,
153
+ *,
154
+ use_mcp: bool,
155
+ attachment_paths: list[Path] | None = None,
151
156
  ) -> str:
152
157
  load_cli_environment()
153
158
  _maybe_prompt_trust(cfg)
@@ -164,6 +169,7 @@ async def _run_prompt(
164
169
  prompt=prompt,
165
170
  max_llm_calls=cfg.max_llm_calls,
166
171
  cfg=cfg,
172
+ attachment_paths=attachment_paths,
167
173
  )
168
174
  return _events_to_text(collected)
169
175
  finally:
@@ -296,6 +302,8 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
296
302
 
297
303
  apply_capability_routing(cfg, prompt_text, context="prompt")
298
304
  cfg.model = pick_effective_model(cfg, prompt_text)
305
+ _repl_attach = list(cfg.pending_attachment_paths)
306
+ cfg.pending_attachment_paths.clear()
299
307
  collected = await run_turn(
300
308
  runner,
301
309
  user_id="local",
@@ -303,6 +311,7 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
303
311
  prompt=prompt_text,
304
312
  max_llm_calls=cfg.max_llm_calls,
305
313
  cfg=cfg,
314
+ attachment_paths=_repl_attach if _repl_attach else None,
306
315
  )
307
316
  out = _events_to_text(collected)
308
317
  if out:
@@ -795,6 +804,17 @@ def main() -> None:
795
804
  metavar="N",
796
805
  help="Cap model↔tool iterations for this message (maps to ADK RunConfig.max_llm_calls)",
797
806
  )
807
+ parser.add_argument(
808
+ "--attach",
809
+ "--image",
810
+ dest="attachments",
811
+ action="append",
812
+ default=[],
813
+ metavar="PATH",
814
+ help="Attach file(s) for this message (repeatable): images, PDF, audio, video, text, etc. "
815
+ "(Gemini-supported MIME). Default max ~20 MiB each (GEMCODE_MAX_ATTACHMENT_BYTES). "
816
+ "REPL: /attach or /image <path>.",
817
+ )
798
818
  args = parser.parse_args()
799
819
 
800
820
  load_cli_environment()
@@ -846,7 +866,16 @@ def main() -> None:
846
866
  prompt_text = prompt.strip()
847
867
  apply_capability_routing(cfg, prompt_text, context="prompt")
848
868
  cfg.model = pick_effective_model(cfg, prompt_text)
849
- out = asyncio.run(_run_prompt(cfg, prompt_text, session_id, use_mcp=args.mcp))
869
+ _cli_attach = list(args.attachments) if getattr(args, "attachments", None) else []
870
+ out = asyncio.run(
871
+ _run_prompt(
872
+ cfg,
873
+ prompt_text,
874
+ session_id,
875
+ use_mcp=args.mcp,
876
+ attachment_paths=_cli_attach if _cli_attach else None,
877
+ )
878
+ )
850
879
  if out:
851
880
  print(out)
852
881
  print(f"\n[gemcode] session_id={session_id}", file=sys.stderr)
@@ -218,6 +218,9 @@ class GemCodeConfig:
218
218
  # Substitutes ${GEMCODE_SESSION_ID} when expanding loaded skills for prompts.
219
219
  session_skill_expand_session_id: str | None = None
220
220
 
221
+ # REPL/TUI: paths queued with /attach (or /image …), sent on the next message then cleared.
222
+ pending_attachment_paths: list[Path] = field(default_factory=list)
223
+
221
224
  # Modality toggles (tool injection + routing).
222
225
  enable_deep_research: bool = field(
223
226
  default_factory=lambda: _truthy_env("GEMCODE_ENABLE_DEEP_RESEARCH", default=False)
@@ -8,6 +8,14 @@ Design goals:
8
8
  - Human-readable JSONL (easy to debug)
9
9
  - Streaming (token deltas + progress)
10
10
  - Safe editing (engine proposes; IDE applies via WorkspaceEdit)
11
+
12
+ Attachments on ``action: turn`` may include:
13
+
14
+ - Textual: ``type: selection`` / ``file`` (appended into the prompt as fenced blocks).
15
+ - Binary / multimodal: ``type: inline`` | ``binary`` | ``blob`` with ``data`` or ``base64``
16
+ (standard base64), plus optional ``filename`` / ``name`` and ``mimeType`` / ``mime_type``.
17
+ The engine writes bytes to a temp file and passes them to Gemini as inline parts
18
+ (same limits as CLI ``--attach``).
11
19
  """
12
20
 
13
21
  from __future__ import annotations
@@ -16,8 +16,13 @@ GemCode is responsible for:
16
16
  from __future__ import annotations
17
17
 
18
18
  import asyncio
19
+ import base64
20
+ import binascii
21
+ import mimetypes
19
22
  import os
20
23
  import sys
24
+ import tempfile
25
+ from pathlib import Path
21
26
  from typing import Any
22
27
 
23
28
  from gemcode.config import GemCodeConfig, load_cli_environment
@@ -38,6 +43,107 @@ def _truthy(v: Any, default: bool = False) -> bool:
38
43
  return default
39
44
 
40
45
 
46
+ def _max_ide_inline_bytes() -> int:
47
+ raw = os.environ.get("GEMCODE_MAX_ATTACHMENT_BYTES")
48
+ if raw:
49
+ try:
50
+ v = int(raw, 10)
51
+ if v > 0:
52
+ return v
53
+ except ValueError:
54
+ pass
55
+ return 20 * 1024 * 1024
56
+
57
+
58
+ def _suffix_for_inline_attachment(name: str, mime: str) -> str:
59
+ p = Path(name or "")
60
+ if p.suffix and len(p.suffix) <= 12:
61
+ return p.suffix
62
+ m = (mime or "").strip().lower().split(";")[0].strip()
63
+ if m:
64
+ ext = mimetypes.guess_extension(m, strict=False)
65
+ if ext == ".jpe":
66
+ ext = ".jpeg"
67
+ if ext:
68
+ return ext
69
+ return ".bin"
70
+
71
+
72
+ def prepare_inline_attachment_paths(
73
+ attachments: list[Any] | None,
74
+ *,
75
+ max_bytes: int | None = None,
76
+ max_count: int = 16,
77
+ ) -> tuple[list[Path], list[str]]:
78
+ """
79
+ Materialize IDE ``inline`` / ``binary`` / ``blob`` attachment dicts as temp files.
80
+
81
+ Expected keys (camelCase accepted): ``data`` or ``base64``, optional ``name`` /
82
+ ``filename``, ``mime_type`` / ``mimeType``.
83
+
84
+ Returns ``(paths, errors)``. Caller must unlink paths when done.
85
+ """
86
+ cap = max_bytes if max_bytes is not None else _max_ide_inline_bytes()
87
+ max_b64 = int(cap * 4 / 3) + 32
88
+ paths: list[Path] = []
89
+ errors: list[str] = []
90
+ if not attachments:
91
+ return paths, errors
92
+
93
+ for a in attachments:
94
+ if len(paths) >= max_count:
95
+ errors.append(f"inline attachments: max {max_count} files")
96
+ break
97
+ if not isinstance(a, dict):
98
+ continue
99
+ at = str(a.get("type") or "").strip().lower()
100
+ if at not in ("inline", "binary", "blob"):
101
+ continue
102
+ b64 = a.get("data") if isinstance(a.get("data"), str) else None
103
+ if b64 is None:
104
+ b64 = a.get("base64") if isinstance(a.get("base64"), str) else None
105
+ if not b64:
106
+ errors.append("inline attachment missing base64 data")
107
+ continue
108
+ if len(b64) > max_b64:
109
+ errors.append("inline attachment too large (base64)")
110
+ continue
111
+ try:
112
+ raw = base64.b64decode(b64, validate=True)
113
+ except (binascii.Error, ValueError) as e:
114
+ errors.append(f"inline attachment: invalid base64 ({e})")
115
+ continue
116
+ if len(raw) > cap:
117
+ errors.append(f"inline attachment exceeds max {cap} bytes")
118
+ continue
119
+ name = str(a.get("name") or a.get("filename") or "attachment")
120
+ mime = str(a.get("mime_type") or a.get("mimeType") or "")
121
+ suffix = _suffix_for_inline_attachment(name, mime)
122
+ try:
123
+ fd, fspath = tempfile.mkstemp(prefix="gemcode_ide_", suffix=suffix)
124
+ with os.fdopen(fd, "wb") as f:
125
+ f.write(raw)
126
+ paths.append(Path(fspath))
127
+ except OSError as e:
128
+ errors.append(f"inline attachment temp file failed: {e}")
129
+
130
+ return paths, errors
131
+
132
+
133
+ def _textual_attachments_only(attachments: list[dict] | None) -> list[dict]:
134
+ if not attachments:
135
+ return []
136
+ out: list[dict] = []
137
+ for a in attachments:
138
+ if not isinstance(a, dict):
139
+ continue
140
+ at = str(a.get("type") or "").strip().lower()
141
+ if at in ("inline", "binary", "blob"):
142
+ continue
143
+ out.append(a)
144
+ return out
145
+
146
+
41
147
  def _build_prompt(prompt: str, attachments: list[dict] | None) -> str:
42
148
  # Keep it simple: attachments are appended as fenced blocks.
43
149
  if not attachments:
@@ -47,6 +153,8 @@ def _build_prompt(prompt: str, attachments: list[dict] | None) -> str:
47
153
  if not isinstance(a, dict):
48
154
  continue
49
155
  at = (a.get("type") or "").strip().lower()
156
+ if at in ("inline", "binary", "blob"):
157
+ continue
50
158
  if at == "selection":
51
159
  txt = a.get("text") or ""
52
160
  path = a.get("path") or ""
@@ -139,7 +247,6 @@ async def run_stdio_loop() -> int:
139
247
  if cfg is None:
140
248
  root = msg.get("project_root") or os.getcwd()
141
249
  model = msg.get("model") or os.environ.get("GEMCODE_MODEL") or ""
142
- from pathlib import Path
143
250
  cfg = GemCodeConfig(project_root=Path(str(root)), model=str(model))
144
251
  # Attach emitter + proposal mode flags (used by tool wrappers).
145
252
  object.__setattr__(cfg, "_ide_emitter", emitter)
@@ -151,7 +258,25 @@ async def run_stdio_loop() -> int:
151
258
 
152
259
  prompt = str(msg.get("prompt") or "")
153
260
  attachments = msg.get("attachments") if isinstance(msg.get("attachments"), list) else None
154
- full_prompt = _build_prompt(prompt, attachments)
261
+ att_dicts = [a for a in (attachments or []) if isinstance(a, dict)]
262
+ inline_paths, inline_err = prepare_inline_attachment_paths(attachments)
263
+ if inline_err:
264
+ for p in inline_paths:
265
+ try:
266
+ p.unlink()
267
+ except OSError:
268
+ pass
269
+ emitter.send(
270
+ make_response(
271
+ id=req_id,
272
+ ok=False,
273
+ error="; ".join(inline_err),
274
+ session=session_id,
275
+ )
276
+ )
277
+ continue
278
+
279
+ full_prompt = _build_prompt(prompt, _textual_attachments_only(att_dicts))
155
280
 
156
281
  # Per-turn allow flags (the engine still only proposes in IDE mode; the IDE applies).
157
282
  allow_write = _truthy(msg.get("allowWrite"), default=False)
@@ -161,14 +286,22 @@ async def run_stdio_loop() -> int:
161
286
 
162
287
  emitter.send(make_event(event="turn_start", id=req_id, session=session_id))
163
288
  try:
164
- events = await run_turn(
165
- runner,
166
- user_id="local",
167
- session_id=session_id,
168
- prompt=full_prompt,
169
- max_llm_calls=cfg.max_llm_calls,
170
- cfg=cfg,
171
- )
289
+ try:
290
+ events = await run_turn(
291
+ runner,
292
+ user_id="local",
293
+ session_id=session_id,
294
+ prompt=full_prompt,
295
+ max_llm_calls=cfg.max_llm_calls,
296
+ cfg=cfg,
297
+ attachment_paths=inline_paths if inline_paths else None,
298
+ )
299
+ finally:
300
+ for p in inline_paths:
301
+ try:
302
+ p.unlink()
303
+ except OSError:
304
+ pass
172
305
  except Exception as e:
173
306
  emitter.send(make_response(id=req_id, ok=False, error=f"{type(e).__name__}: {e}", session=session_id))
174
307
  continue
@@ -9,8 +9,9 @@ from __future__ import annotations
9
9
  import asyncio
10
10
  import os
11
11
  import sys
12
- from typing import Any
12
+ from pathlib import Path
13
13
  from threading import Lock
14
+ from typing import Any, Sequence
14
15
 
15
16
  from google.adk.agents.run_config import RunConfig
16
17
  from google.adk.runners import Runner
@@ -66,6 +67,7 @@ async def run_turn(
66
67
  prompt: str,
67
68
  max_llm_calls: int | None = None,
68
69
  cfg: "GemCodeConfig | None" = None,
70
+ attachment_paths: Sequence[Path | str] | None = None,
69
71
  ) -> list:
70
72
  """Execute one user message; collect all Events (caller aggregates text)."""
71
73
  # Dynamic risk score: updated each user message; later refined by tool outcomes.
@@ -86,6 +88,8 @@ async def run_turn(
86
88
  risk += 0.2
87
89
  if re.search(r"\\b(test|pytest|ci|build|deploy|release)\\b", p, re.I):
88
90
  risk += 0.1
91
+ if attachment_paths:
92
+ risk = min(1.0, risk + 0.12)
89
93
  # Multi-file hints
90
94
  if p.count("/") >= 6 or p.count(".py") + p.count(".ts") + p.count(".tsx") >= 3:
91
95
  risk += 0.1
@@ -167,10 +171,22 @@ async def run_turn(
167
171
 
168
172
  state_delta = token_budget_invocation_reset()
169
173
 
170
- # The first message is plain user text.
171
- current_message = types.Content(
172
- role="user", parts=[types.Part(text=prompt)]
173
- )
174
+ # First message: optional inline files + text (Gemini multimodal).
175
+ if attachment_paths:
176
+ from gemcode.multimodal_input import build_user_content
177
+
178
+ root = cfg.project_root if cfg is not None else Path.cwd()
179
+ current_message, attach_warn = build_user_content(
180
+ prompt,
181
+ attachment_paths,
182
+ project_root=root,
183
+ )
184
+ for w in attach_warn:
185
+ print(f"[gemcode] {w}", file=sys.stderr)
186
+ else:
187
+ current_message = types.Content(
188
+ role="user", parts=[types.Part(text=prompt)]
189
+ )
174
190
 
175
191
  async def _await_runner_events(
176
192
  *, next_message: types.Content, do_reset: bool
@@ -0,0 +1,144 @@
1
+ """
2
+ Build multimodal user Content (text + inline files) for Gemini.
3
+
4
+ Paths may be absolute or relative to the current working directory, then project root.
5
+ MIME types are inferred from the filename (``mimetypes``) and optionally from file
6
+ headers (PDF, common images, some audio/video) so PDFs and other Gemini-supported
7
+ types work—not only images.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import mimetypes
13
+ import os
14
+ from pathlib import Path
15
+ from typing import Sequence
16
+
17
+ from google.genai import types
18
+
19
+ _MAX_ATTACHMENTS = 16
20
+
21
+
22
+ def _max_attachment_bytes() -> int:
23
+ raw = os.environ.get("GEMCODE_MAX_ATTACHMENT_BYTES")
24
+ if raw:
25
+ try:
26
+ v = int(raw, 10)
27
+ if v > 0:
28
+ return v
29
+ except ValueError:
30
+ pass
31
+ return 20 * 1024 * 1024
32
+
33
+
34
+ def resolve_attachment_path(p: Path | str, *, project_root: Path) -> Path:
35
+ path = Path(p).expanduser()
36
+ if path.is_absolute():
37
+ return path.resolve()
38
+ cwd_try = (Path.cwd() / path).resolve()
39
+ if cwd_try.is_file():
40
+ return cwd_try
41
+ root_try = (project_root / path).resolve()
42
+ if root_try.is_file():
43
+ return root_try
44
+ return (Path.cwd() / path).resolve()
45
+
46
+
47
+ # Backward-compatible name from the images-only era.
48
+ resolve_image_path = resolve_attachment_path
49
+
50
+
51
+ def _sniff_mime(head: bytes) -> str | None:
52
+ if len(head) >= 5 and head[:5] == b"%PDF-":
53
+ return "application/pdf"
54
+ if len(head) >= 8 and head[:8] == b"\x89PNG\r\n\x1a\n":
55
+ return "image/png"
56
+ if len(head) >= 3 and head[:3] == b"\xff\xd8\xff":
57
+ return "image/jpeg"
58
+ if len(head) >= 6 and head[:6] in (b"GIF87a", b"GIF89a"):
59
+ return "image/gif"
60
+ if len(head) >= 12 and head[:4] == b"RIFF" and head[8:12] == b"WEBP":
61
+ return "image/webp"
62
+ if len(head) >= 2 and head[:2] == b"BM":
63
+ return "image/bmp"
64
+ if len(head) >= 12 and head[4:8] == b"ftyp":
65
+ return "video/mp4"
66
+ if len(head) >= 4 and head[:4] == b"\x1a\x45\xdf\xa3":
67
+ return "video/webm"
68
+ if len(head) >= 4 and head[:4] == b"OggS":
69
+ return "audio/ogg"
70
+ if len(head) >= 12 and head[:4] == b"RIFF" and head[8:12] == b"WAVE":
71
+ return "audio/wav"
72
+ if len(head) >= 4 and head[:4] == b"fLaC":
73
+ return "audio/flac"
74
+ if len(head) >= 3 and head[:3] in (b"ID3", b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"):
75
+ return "audio/mpeg"
76
+ return None
77
+
78
+
79
+ def _infer_mime(path: Path, data: bytes) -> tuple[str, list[str]]:
80
+ warnings: list[str] = []
81
+ head = data[:512]
82
+ guess, _ = mimetypes.guess_type(path.name, strict=False)
83
+ sniff = _sniff_mime(head)
84
+
85
+ if sniff and (not guess or guess == "application/octet-stream"):
86
+ return sniff, warnings
87
+ if guess and guess != "application/octet-stream":
88
+ return guess, warnings
89
+ if sniff:
90
+ return sniff, warnings
91
+ if guess:
92
+ return guess, warnings
93
+ warnings.append(
94
+ f"could not infer MIME type for {path}; using application/octet-stream "
95
+ "(Gemini may reject unsupported types)"
96
+ )
97
+ return "application/octet-stream", warnings
98
+
99
+
100
+ def build_user_content(
101
+ prompt: str,
102
+ attachment_paths: Sequence[Path | str] | None,
103
+ *,
104
+ project_root: Path,
105
+ ) -> tuple[types.Content, list[str]]:
106
+ """
107
+ Build ``Content`` with inline file parts first, then the text part.
108
+
109
+ Returns ``(content, warnings)`` — warnings are non-fatal skips or hints (stderr them).
110
+ """
111
+ warnings: list[str] = []
112
+ parts: list[types.Part] = []
113
+ max_b = _max_attachment_bytes()
114
+ if attachment_paths:
115
+ for raw in list(attachment_paths)[:_MAX_ATTACHMENTS]:
116
+ p = resolve_attachment_path(raw, project_root=project_root)
117
+ if not p.is_file():
118
+ warnings.append(f"attachment not found: {raw}")
119
+ continue
120
+ try:
121
+ size = p.stat().st_size
122
+ except OSError as e:
123
+ warnings.append(f"attachment stat failed {p}: {e}")
124
+ continue
125
+ if size > max_b:
126
+ warnings.append(
127
+ f"attachment too large ({size} bytes, max {max_b}): {p} "
128
+ "(set GEMCODE_MAX_ATTACHMENT_BYTES or use a smaller file)"
129
+ )
130
+ continue
131
+ try:
132
+ data = p.read_bytes()
133
+ except OSError as e:
134
+ warnings.append(f"attachment read failed {p}: {e}")
135
+ continue
136
+ mime, mw = _infer_mime(p, data)
137
+ warnings.extend(mw)
138
+ parts.append(types.Part(inline_data=types.Blob(data=data, mime_type=mime)))
139
+
140
+ text = (prompt or "").strip() or (
141
+ "(User attached file(s) only — describe or analyze them.)" if parts else ""
142
+ )
143
+ parts.append(types.Part(text=text))
144
+ return types.Content(role="user", parts=parts), warnings
@@ -227,7 +227,11 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
227
227
  ("exit", "Leave the REPL · /quit same"),
228
228
  ("help", "Short help · /? same"),
229
229
  ("hooks", "Post-turn hook configuration"),
230
+ ("attach", "Queue file(s) for next message (PDF, images, …) · /image /file /img · list · clear"),
230
231
  ("init", "Generate GEMINI.md project instructions"),
232
+ ("file", "Alias of /attach"),
233
+ ("image", "Alias of /attach (same queue)"),
234
+ ("img", "Alias of /attach"),
231
235
  ("kaira", "Background job scheduler — how to run gemcode kaira"),
232
236
  ("limits", "Execution limits (calls, context, …)"),
233
237
  ("live-audio", "How to run gemcode live-audio · /liveaudio same"),
@@ -320,6 +324,8 @@ def slash_help_lines() -> list[str]:
320
324
  " (CLI) gemcode login Save or change API key (~/.gemcode/credentials.json)",
321
325
  "",
322
326
  " Project setup:",
327
+ " /attach <path> Queue file(s) for the **next** message (PDF, images, …); /attach list|clear",
328
+ " Aliases: /image /img /file",
323
329
  " /trust Show workspace trust status (file/shell tools)",
324
330
  " /trust on|off Trust or revoke trust for this project root (~/.gemcode/trust.json)",
325
331
  " /init Analyze project structure and generate GEMINI.md",