gemcode 0.3.78__tar.gz → 0.3.81__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.3.78/src/gemcode.egg-info → gemcode-0.3.81}/PKG-INFO +16 -2
- {gemcode-0.3.78 → gemcode-0.3.81}/README.md +15 -1
- {gemcode-0.3.78 → gemcode-0.3.81}/pyproject.toml +1 -1
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/agent.py +6 -5
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/cli.py +31 -2
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/config.py +3 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/ide_protocol.py +8 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/ide_stdio.py +143 -10
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/invoke.py +21 -5
- gemcode-0.3.81/src/gemcode/multimodal_input.py +144 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/repl_commands.py +6 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/repl_slash.py +52 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/edit.py +10 -5
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tui/scrollback.py +14 -1
- {gemcode-0.3.78 → gemcode-0.3.81/src/gemcode.egg-info}/PKG-INFO +16 -2
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode.egg-info/SOURCES.txt +3 -0
- gemcode-0.3.81/tests/test_ide_stdio_attachments.py +42 -0
- gemcode-0.3.81/tests/test_multimodal_input.py +64 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_slash_completion_registry.py +1 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_tools.py +11 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/LICENSE +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/MANIFEST.in +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/setup.cfg +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/autotune.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/learning.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/rules.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/session_store.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/skills.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/version.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/web/sse_adapter.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_add_dir.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_credentials.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_paths.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_permissions.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_skills.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.3.78 → gemcode-0.3.81}/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.
|
|
3
|
+
Version: 0.3.81
|
|
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
|
|
|
@@ -460,7 +473,7 @@ Tools are registered in `gemcode/tools/` and exposed to the model as ADK functio
|
|
|
460
473
|
- **Computer use:** ADK `ComputerUseToolset` + Playwright (separate install and flags).
|
|
461
474
|
- **MCP:** Tools loaded from configured servers.
|
|
462
475
|
|
|
463
|
-
**Vendor file policy:** Writes to certain
|
|
476
|
+
**Vendor file policy:** Writes to certain third-party instruction filenames (`CLAUDE.md`, `AGENTS.md`, `*.local` variants, `.cursorrules`, …) are blocked; use `GEMINI.md` and `.gemcode/notes.md` instead. The agent instruction always states this; `write_file` / `search_replace` enforce it.
|
|
464
477
|
|
|
465
478
|
---
|
|
466
479
|
|
|
@@ -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
|
|
|
@@ -271,7 +284,7 @@ Tools are registered in `gemcode/tools/` and exposed to the model as ADK functio
|
|
|
271
284
|
- **Computer use:** ADK `ComputerUseToolset` + Playwright (separate install and flags).
|
|
272
285
|
- **MCP:** Tools loaded from configured servers.
|
|
273
286
|
|
|
274
|
-
**Vendor file policy:** Writes to certain
|
|
287
|
+
**Vendor file policy:** Writes to certain third-party instruction filenames (`CLAUDE.md`, `AGENTS.md`, `*.local` variants, `.cursorrules`, …) are blocked; use `GEMINI.md` and `.gemcode/notes.md` instead. The agent instruction always states this; `write_file` / `search_replace` enforce it.
|
|
275
288
|
|
|
276
289
|
---
|
|
277
290
|
|
|
@@ -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. |
|
|
@@ -77,7 +77,8 @@ def build_global_instruction() -> str:
|
|
|
77
77
|
"Think deeply about what the person actually wants before you do anything. "
|
|
78
78
|
"Use exactly as many tools as the task genuinely requires — no more. "
|
|
79
79
|
"Act fully and autonomously when action is needed. "
|
|
80
|
-
"Always use read-only tools before shell or write tools."
|
|
80
|
+
"Always use read-only tools before shell or write tools. "
|
|
81
|
+
"Never create CLAUDE.md or AGENTS.md; use GEMINI.md for project instructions."
|
|
81
82
|
)
|
|
82
83
|
|
|
83
84
|
|
|
@@ -580,6 +581,10 @@ You have native deep thinking capability — use it actively:
|
|
|
580
581
|
Keep tool usage minimal. Prefer short, targeted calls and keep tool outputs small.
|
|
581
582
|
If you need more tool usage examples, set `GEMCODE_VERBOSE_INSTRUCTIONS=1`.
|
|
582
583
|
|
|
584
|
+
## Instruction files (GemCode — always follow)
|
|
585
|
+
- **Do not** create or modify `CLAUDE.md`, `AGENTS.md`, `claude.local.md`, `agents.local.md`, or `.cursorrules` unless the user **explicitly** asks for that exact filename. Those are for other assistants; GemCode reads **`GEMINI.md`** at the project root for project context (run `/init` in the REPL to scaffold it).
|
|
586
|
+
- If you need to capture project conventions, edit **`GEMINI.md`** or append to **`.gemcode/notes.md`** via the notes tools — not vendor-specific instruction filenames.
|
|
587
|
+
|
|
583
588
|
"""
|
|
584
589
|
|
|
585
590
|
if not verbose_tools_guide:
|
|
@@ -895,10 +900,6 @@ You have two tools to persist project insights across sessions (auto-memory styl
|
|
|
895
900
|
Notes are loaded at session start so future sessions inherit this knowledge.
|
|
896
901
|
|
|
897
902
|
- **`read_project_notes()`** — read current notes **only when starting a real engineering task** (editing, debugging, building). Do NOT call this for greetings or general questions. If notes exist and you're about to work on a task, read them once to avoid re-discovering known information.
|
|
898
|
-
|
|
899
|
-
## Do not create vendor-specific instruction files
|
|
900
|
-
- Do NOT create or modify `CLAUDE.md` or `AGENTS.md`. GemCode does not use these.
|
|
901
|
-
- If project instructions are needed and the user asked for it, use `GEMINI.md` (repo root).
|
|
902
903
|
"""
|
|
903
904
|
|
|
904
905
|
# Inject capability-specific strategy sections only when those caps are on.
|
|
@@ -147,7 +147,12 @@ def _initialize_gemcode_project(cfg: GemCodeConfig) -> None:
|
|
|
147
147
|
|
|
148
148
|
|
|
149
149
|
async def _run_prompt(
|
|
150
|
-
cfg: GemCodeConfig,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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
|
-
#
|
|
171
|
-
|
|
172
|
-
|
|
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",
|