gemcode 0.3.15__tar.gz → 0.3.16__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 (111) hide show
  1. {gemcode-0.3.15/src/gemcode.egg-info → gemcode-0.3.16}/PKG-INFO +1 -1
  2. {gemcode-0.3.15 → gemcode-0.3.16}/pyproject.toml +1 -1
  3. gemcode-0.3.16/src/gemcode/agent.py +286 -0
  4. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/config.py +79 -3
  5. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tool_registry.py +4 -2
  6. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/__init__.py +9 -2
  7. gemcode-0.3.16/src/gemcode/tools/bash.py +127 -0
  8. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/filesystem.py +67 -5
  9. gemcode-0.3.16/src/gemcode/tools/search.py +142 -0
  10. gemcode-0.3.16/src/gemcode/tools/web.py +133 -0
  11. {gemcode-0.3.15 → gemcode-0.3.16/src/gemcode.egg-info}/PKG-INFO +1 -1
  12. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode.egg-info/SOURCES.txt +2 -0
  13. gemcode-0.3.15/src/gemcode/agent.py +0 -199
  14. gemcode-0.3.15/src/gemcode/tools/search.py +0 -85
  15. {gemcode-0.3.15 → gemcode-0.3.16}/LICENSE +0 -0
  16. {gemcode-0.3.15 → gemcode-0.3.16}/MANIFEST.in +0 -0
  17. {gemcode-0.3.15 → gemcode-0.3.16}/README.md +0 -0
  18. {gemcode-0.3.15 → gemcode-0.3.16}/setup.cfg +0 -0
  19. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/__init__.py +0 -0
  20. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/__main__.py +0 -0
  21. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/audit.py +0 -0
  22. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/autocompact.py +0 -0
  23. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/callbacks.py +0 -0
  24. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/capability_routing.py +0 -0
  25. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/cli.py +0 -0
  26. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/compaction.py +0 -0
  27. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/computer_use/__init__.py +0 -0
  28. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/computer_use/browser_computer.py +0 -0
  29. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/context_budget.py +0 -0
  30. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/context_warning.py +0 -0
  31. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/credentials.py +0 -0
  32. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/hitl_session.py +0 -0
  33. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/interactions.py +0 -0
  34. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/invoke.py +0 -0
  35. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/kairos_daemon.py +0 -0
  36. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/limits.py +0 -0
  37. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/live_audio_engine.py +0 -0
  38. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/logging_config.py +0 -0
  39. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/mcp_loader.py +0 -0
  40. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/memory/__init__.py +0 -0
  41. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/memory/embedding_memory_service.py +0 -0
  42. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/memory/file_memory_service.py +0 -0
  43. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/modality_tools.py +0 -0
  44. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/model_errors.py +0 -0
  45. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/model_routing.py +0 -0
  46. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/paths.py +0 -0
  47. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/permissions.py +0 -0
  48. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/plugins/__init__.py +0 -0
  49. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  50. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  51. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/prompt_suggestions.py +0 -0
  52. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/__init__.py +0 -0
  53. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/config.py +0 -0
  54. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/deps.py +0 -0
  55. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/engine.py +0 -0
  56. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/stop_hooks.py +0 -0
  57. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/token_budget.py +0 -0
  58. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/transitions.py +0 -0
  59. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/repl_commands.py +0 -0
  60. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/repl_slash.py +0 -0
  61. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/session_runtime.py +0 -0
  62. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/slash_commands.py +0 -0
  63. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/thinking.py +0 -0
  64. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tool_prompt_manifest.py +0 -0
  65. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/edit.py +0 -0
  66. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/shell.py +0 -0
  67. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/shell_gate.py +0 -0
  68. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/todo.py +0 -0
  69. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools_inspector.py +0 -0
  70. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/trust.py +0 -0
  71. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tui/scrollback.py +0 -0
  72. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tui/spinner.py +0 -0
  73. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tui/welcome_banner.py +0 -0
  74. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tui/welcome_rich.py +0 -0
  75. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/version.py +0 -0
  76. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/vertex.py +0 -0
  77. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/web/__init__.py +0 -0
  78. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/web/claude_sse_adapter.py +0 -0
  79. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/web/terminal_repl.py +0 -0
  80. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/workspace_hints.py +0 -0
  81. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode.egg-info/dependency_links.txt +0 -0
  82. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode.egg-info/entry_points.txt +0 -0
  83. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode.egg-info/requires.txt +0 -0
  84. {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode.egg-info/top_level.txt +0 -0
  85. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_agent_instruction.py +0 -0
  86. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_autocompact.py +0 -0
  87. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_capability_routing.py +0 -0
  88. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_claude_web_adapter_sse.py +0 -0
  89. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_cli_init.py +0 -0
  90. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_computer_use_permissions.py +0 -0
  91. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_context_budget.py +0 -0
  92. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_context_warning.py +0 -0
  93. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_credentials.py +0 -0
  94. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_interactive_permission_ask.py +0 -0
  95. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_kairos_scheduler.py +0 -0
  96. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_modality_tools.py +0 -0
  97. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_model_error_retry.py +0 -0
  98. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_model_errors.py +0 -0
  99. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_model_routing.py +0 -0
  100. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_paths.py +0 -0
  101. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_permissions.py +0 -0
  102. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_prompt_suggestions.py +0 -0
  103. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_repl_commands.py +0 -0
  104. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_repl_slash.py +0 -0
  105. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_slash_commands.py +0 -0
  106. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_thinking_config.py +0 -0
  107. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_token_budget.py +0 -0
  108. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_tool_context_circulation.py +0 -0
  109. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_tools.py +0 -0
  110. {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_tools_inspector.py +0 -0
  111. {gemcode-0.3.15 → gemcode-0.3.16}/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.15
3
+ Version: 0.3.16
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.15"
7
+ version = "0.3.16"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,286 @@
1
+ """
2
+ Root LlmAgent definition (Claude Code: agent config + tool list, analogous to tools.ts + prompts).
3
+
4
+ See `session_runtime.py` for Runner/session wiring (outer layer).
5
+ See `tool_registry.py` for tool categories (read vs mutating vs shell).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import inspect
11
+ from pathlib import Path
12
+
13
+ from google.adk.agents.llm_agent import LlmAgent
14
+
15
+ from gemcode.autocompact import make_before_model_autocompact_callback
16
+ from gemcode.callbacks import (
17
+ make_after_model_callback,
18
+ make_after_tool_callback,
19
+ make_before_tool_callback,
20
+ make_on_model_error_callback,
21
+ make_on_tool_error_callback,
22
+ )
23
+ from gemcode.compaction import make_before_model_callback
24
+ from gemcode.config import GemCodeConfig
25
+ from gemcode.context_budget import make_before_model_context_shrink_callback
26
+ from gemcode.limits import make_before_model_limits_callback, make_before_model_token_budget_callback
27
+ from gemcode.thinking import build_thinking_config
28
+ from gemcode.tools import build_function_tools
29
+ from gemcode.tool_prompt_manifest import build_tool_manifest
30
+
31
+
32
+ def _chain_before_model_callbacks(*callbacks):
33
+ cbs = [c for c in callbacks if c is not None]
34
+ if not cbs:
35
+ return None
36
+ if len(cbs) == 1:
37
+ return cbs[0]
38
+
39
+ async def chained(callback_context, llm_request):
40
+ for cb in cbs:
41
+ out = cb(callback_context, llm_request)
42
+ if inspect.isawaitable(out):
43
+ out = await out
44
+ if out is not None:
45
+ return out
46
+ return None
47
+
48
+ return chained
49
+
50
+
51
+ def _load_gemini_md(project_root: Path) -> str:
52
+ for name in ("GEMINI.md", "gemini.md"):
53
+ p = project_root / name
54
+ if p.is_file():
55
+ return p.read_text(encoding="utf-8", errors="replace")[:50_000]
56
+ return ""
57
+
58
+
59
+ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
60
+ """
61
+ Injected every session so the model does not hallucinate deployment, permissions,
62
+ or "how to switch Pro" the way a product-agnostic base prompt would.
63
+ """
64
+ root = cfg.project_root.resolve()
65
+ model = (getattr(cfg, "model", None) or "").strip() or "(default)"
66
+ return f"""## Runtime facts (authoritative for this session)
67
+ - **Project root** — every filesystem tool path is relative to: `{root}`
68
+ - **Model id in use:** `{model}`. In this TUI/REPL you can override it for subsequent turns with `/model use <id>` (use `/model list` to browse IDs). For a full restart you can still use `--model <id>` or env `GEMCODE_MODEL`.
69
+ - **UI banner** phrases such as "GemCode Pro" are **terminal marketing**, not a separate API tier or model you enable from chat.
70
+ - **Env toggles** (`GEMCODE_ENABLE_COMPUTER_USE`, `GEMCODE_MODEL`, etc.) affect only the **OS process** that launched `gemcode`. Pasting `VAR=1` in chat does **not** reconfigure a running session—tell the user to export in their shell, use project `.env`, or restart the CLI.
71
+ - **Working in subfolders** — use tools: e.g. `list_directory("Desktop")`, `glob_files("**/query.ts")`, `read_file("testing/ai-edtech-app/src/app/page.tsx")`, or `run_command` with `cwd_subdir`. Never claim the sandbox cannot reach a subpath unless a tool returned an explicit error."""
72
+
73
+
74
+ def build_instruction(cfg: GemCodeConfig) -> str:
75
+ base = f"""You are GemCode, an expert software engineering agent powered by Google Gemini.
76
+ You run locally via the GemCode CLI. You are the same agent the user launched — not a hosted portal.
77
+
78
+ {_build_runtime_facts(cfg)}
79
+
80
+ ## Core identity and approach
81
+ You are a senior engineer who *acts*, not just advises. When given a task:
82
+ 1. **Orient** — understand the repo structure, find the relevant files.
83
+ 2. **Plan** — for complex tasks, call `todo_write` upfront to map out the work.
84
+ 3. **Execute** — make the changes, run the checks, iterate.
85
+ 4. **Verify** — confirm the result is correct before reporting done.
86
+
87
+ Never stop mid-task just because the first tool call succeeded. Keep going until the full task is complete or you hit a genuine blocker.
88
+
89
+ ## Thinking through hard problems
90
+ You have native deep thinking capability — use it actively:
91
+ - **Before** starting a complex refactor or architectural change, think: what are the dependencies? what could break? what is the minimal safe change?
92
+ - **When debugging**: trace the execution path mentally before acting. Form a hypothesis, then verify with tools.
93
+ - **When stuck after 2 attempts**: stop and reconsider your assumptions rather than repeating the same approach.
94
+ - **For trade-off decisions** (which library, which pattern, which approach): reason through the pros/cons given this specific codebase.
95
+
96
+ ## Interpreting requests
97
+ - Treat every message as a software engineering task unless clearly otherwise.
98
+ - If vague ("fix it", "the config", "rename that"), **infer from the repo**: search, read, then act. Do not give abstract advice when concrete files exist.
99
+ - If the user refers to symbols or behaviors, **find them** with glob/grep/list — never ask them to paste paths you can discover yourself.
100
+ - **Never propose edits to files you haven't read.** Read first, then edit.
101
+ - When something fails, diagnose (re-read the error, check assumptions) before switching strategy. Do not repeat the same failed call.
102
+
103
+ ## Tool selection guide
104
+
105
+ ### Shell execution (critical — use these for real work)
106
+ - **`bash`** — use for all shell workflows that need pipelines, redirects, or shell features:
107
+ - `bash("git log --oneline -20")` — git history
108
+ - `bash("git diff HEAD~1 -- src/api/")` — targeted diff
109
+ - `bash("git status && git diff --stat")` — repo state
110
+ - `bash("find . -name '*.py' | xargs grep -l 'SomeClass' | head -20")` — cross-file search
111
+ - `bash("npm run build 2>&1 | tail -50")` — build output (stderr + stdout combined)
112
+ - `bash("pytest tests/ -x -q --tb=short 2>&1 | head -150")` — test run
113
+ - `bash("cat package.json | python3 -m json.tool")` — parse JSON
114
+ - `bash("ls -la src/ | grep -E '\\.(ts|tsx)$'")` — filtered listing
115
+ - `bash("wc -l $(find . -name '*.py') | sort -n | tail -20")` — largest files
116
+ - For **dev servers**: `bash("npm run dev", background=True, cwd_subdir="frontend")`
117
+ - For **subfolders**: `bash("cargo build --release", cwd_subdir="backend")`
118
+
119
+ - **`run_command`** — simple single-executable calls without shell features:
120
+ - `run_command("npm", args=["install", "--legacy-peer-deps"])` — clean npm install
121
+ - `run_command("python3", args=["-m", "pytest", "--version"])` — version check
122
+ - Use `extra_env_keys`/`extra_env_values` for non-interactive scaffolding tools.
123
+
124
+ ### File operations
125
+ - **`read_file`** — read code/config. Use `start_line`/`end_line` for large files:
126
+ - `read_file("src/server.py", start_line=100, end_line=200)` — read a section
127
+ - `read_file("long_file.py", start_line=500)` — from line 500 to end
128
+ - Always read before editing.
129
+
130
+ - **`grep_content`** — search with regex. Use `context_lines` to see surrounding code:
131
+ - `grep_content("def authenticate", "**/*.py", context_lines=4)` — function + context
132
+ - `grep_content("TODO|FIXME|HACK", "**/*.ts")` — multiple patterns (regex alternation)
133
+ - `grep_content("import React", "**/*.tsx", case_sensitive=False)` — case-insensitive
134
+ - `grep_content("class.*Error", "**/*.py", context_lines=2)` — error classes
135
+
136
+ - **`glob_files`** — find files by name pattern:
137
+ - `glob_files("**/*.test.ts")`, `glob_files("**/config*.json")`, `glob_files("src/**/*.py")`
138
+
139
+ - **`list_directory`** — explore directory structure:
140
+ - `list_directory(".")`, `list_directory("src/api")`, `list_directory("Desktop")`
141
+
142
+ - **`write_file`** — create or overwrite files. Read first if the file exists.
143
+ - **`search_replace`** — targeted in-place edits. Provide enough context in `old_string` to be unique.
144
+ - **`move_file`** — rename or reorganize files/directories within the project.
145
+ - **`delete_file`** — remove a single file.
146
+
147
+ ### Research and documentation
148
+ - **`web_fetch`** — fetch docs, APIs, changelogs, READMEs from the web:
149
+ - `web_fetch("https://docs.python.org/3/library/asyncio.html")` — official docs
150
+ - `web_fetch("https://api.github.com/repos/owner/repo/releases/latest")` — API data
151
+ - `web_fetch("https://registry.npmjs.org/react/latest")` — npm package info
152
+ - Use when you need to look up an API, check the latest version, or read documentation.
153
+
154
+ ### Planning
155
+ - **`todo_write`** — track work items. Use for any task with 3+ steps.
156
+ - Create at task start, mark completed as you finish, merge updates.
157
+
158
+ ## Multi-step task execution
159
+ One user message = many model↔tool rounds (up to 256 LLM calls by default). This is intentional — you are expected to do complete tasks autonomously.
160
+
161
+ **Standard workflow for complex tasks:**
162
+ 1. `todo_write` — plan the work items
163
+ 2. Explore — `bash("find . ...")` or `list_directory` or `glob_files` to understand structure
164
+ 3. Read — `read_file` with line ranges on large files; `grep_content` for symbol search
165
+ 4. Edit — `write_file` / `search_replace` for changes
166
+ 5. Verify — `bash("pytest ...")` or `bash("npm run build ...")` or `bash("git diff")`
167
+ 6. Fix — iterate on failures, re-verify
168
+ 7. Update todos — mark done as you go
169
+
170
+ **Do not stop after step 2 or 3** — complete the full task.
171
+
172
+ ## Parallelism
173
+ Issue independent tool calls in the same turn when outputs don't depend on each other:
174
+ - Reading multiple files simultaneously ✓
175
+ - Grepping for different patterns at once ✓
176
+ - `list_directory` + `glob_files` in parallel ✓
177
+ Sequential: when step B needs step A's result.
178
+
179
+ ## Error recovery
180
+ - **Test/build failures**: read the full error, identify the exact line, fix, re-run.
181
+ - **Tool errors**: diagnose why it failed before retrying — don't repeat the exact same call.
182
+ - **After 2 failed attempts on the same problem**: stop and explain the blocker clearly.
183
+ - **Unexpected file content**: re-read the actual file rather than assuming your mental model is correct.
184
+
185
+ ## Risk and permissions
186
+ - State destructive operations clearly before doing them (deletes, force-push, data truncation).
187
+ - For `bash` commands that could be destructive (`rm -rf`, `git push --force`), confirm with the user first.
188
+ - If a tool is denied, adjust the plan — don't retry the same gated call.
189
+
190
+ ## Communication
191
+ - One short line before the first tool call in a turn (e.g. "Reading the auth module and checking the test suite...").
192
+ - Summarize tool results in plain language — the user doesn't see raw tool internals.
193
+ - After completing a task: clear summary of what changed, where, and why.
194
+ - If the user pastes UI copy / noise / error output, extract the real intent and act on source files.
195
+ - Prefer small, testable, accurate changes over broad rewrites.
196
+
197
+ ## Workspace scope
198
+ All file tools use paths **relative to the project root** (where GemCode was started). The root may be the home folder — subfolders like `Desktop`, `Desktop/code`, `Documents` are inside the sandbox. Call `list_directory("Desktop")` or `glob_files("**/*name*.ts")` instead of assuming access is blocked. Only treat access as denied when a tool returns an explicit `error`."""
199
+
200
+ tool_manifest = build_tool_manifest(cfg)
201
+
202
+ if tool_manifest:
203
+ base = f"{base}\n\n{tool_manifest}"
204
+ extra = _load_gemini_md(cfg.project_root)
205
+ if extra.strip():
206
+ return f"{base}\n\n## Project instructions (GEMINI.md)\n{extra}"
207
+ return base
208
+
209
+
210
+ def build_root_agent(cfg: GemCodeConfig, extra_tools: list | None = None) -> LlmAgent:
211
+ """Create the root LlmAgent with tools and callbacks (no Runner)."""
212
+ tools = build_function_tools(cfg)
213
+ if getattr(cfg, "enable_memory", False):
214
+ # ADK preload_memory injects retrieved memories into the next llm_request.
215
+ from google.adk.tools import preload_memory
216
+
217
+ tools = [preload_memory, *tools]
218
+ if extra_tools:
219
+ tools = [*tools, *extra_tools]
220
+
221
+ before_model = _chain_before_model_callbacks(
222
+ make_before_model_autocompact_callback(cfg),
223
+ make_before_model_context_shrink_callback(cfg),
224
+ make_before_model_callback(cfg),
225
+ make_before_model_limits_callback(cfg),
226
+ make_before_model_token_budget_callback(cfg),
227
+ )
228
+ cb_kwargs: dict = {
229
+ "before_tool_callback": make_before_tool_callback(cfg),
230
+ "after_tool_callback": make_after_tool_callback(cfg),
231
+ "after_model_callback": make_after_model_callback(cfg),
232
+ "on_tool_error_callback": make_on_tool_error_callback(cfg),
233
+ "on_model_error_callback": make_on_model_error_callback(cfg),
234
+ }
235
+ if before_model is not None:
236
+ cb_kwargs["before_model_callback"] = before_model
237
+
238
+ # Claude-like thinking: enabled by default (Gemini dynamic), but allow
239
+ # explicit overrides for disable/budgets/levels.
240
+ gen_cfg = None
241
+ thinking_cfg = build_thinking_config(cfg)
242
+ tool_cfg = None
243
+ model_id = getattr(cfg, "model", "") or ""
244
+ is_gemini_3 = "gemini-3" in model_id.lower()
245
+ comb_mode = (getattr(cfg, "tool_combination_mode", None) or "deep_research").lower()
246
+ enable_for_run = False
247
+ if comb_mode in ("auto", "deep_research"):
248
+ enable_for_run = bool(getattr(cfg, "enable_deep_research", False))
249
+ elif comb_mode == "always":
250
+ enable_for_run = True
251
+ elif comb_mode == "never":
252
+ enable_for_run = False
253
+ else:
254
+ # Unknown values: stay conservative.
255
+ enable_for_run = bool(getattr(cfg, "enable_deep_research", False))
256
+
257
+ if enable_for_run and is_gemini_3:
258
+ from google.genai import types
259
+
260
+ # Gemini "tool context circulation" enables built-in tools results to
261
+ # be combined with your client-side function tools in the same workflow.
262
+ tool_cfg = types.ToolConfig(include_server_side_tool_invocations=True)
263
+
264
+ if thinking_cfg is not None or tool_cfg is not None:
265
+ from google.genai import types
266
+
267
+ gen_cfg = types.GenerateContentConfig(
268
+ thinking_config=thinking_cfg,
269
+ tool_config=tool_cfg,
270
+ )
271
+
272
+ return LlmAgent(
273
+ model=cfg.model,
274
+ name="gemcode",
275
+ instruction=build_instruction(cfg),
276
+ tools=tools,
277
+ generate_content_config=gen_cfg,
278
+ **cb_kwargs,
279
+ )
280
+
281
+
282
+ def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None):
283
+ """Backward-compatible: prefer `gemcode.session_runtime.create_runner`."""
284
+ from gemcode.session_runtime import create_runner as _cr
285
+
286
+ return _cr(cfg, extra_tools=extra_tools)
@@ -259,18 +259,94 @@ class GemCodeConfig:
259
259
  else:
260
260
  self.allow_commands = frozenset(
261
261
  (
262
+ # Python
262
263
  "pytest",
263
264
  "python3",
264
265
  "python",
265
266
  "pip",
266
267
  "pip3",
267
- "npm",
268
- "npx",
269
- "git",
270
268
  "ruff",
271
269
  "uv",
270
+ "mypy",
271
+ "black",
272
+ "isort",
273
+ "flake8",
274
+ "pylint",
275
+ # Node / JS
276
+ "npm",
277
+ "npx",
278
+ "node",
279
+ "pnpm",
280
+ "yarn",
281
+ "bun",
282
+ "bunx",
283
+ "tsc",
284
+ "eslint",
285
+ "prettier",
286
+ "vite",
287
+ "next",
288
+ "turbo",
289
+ # Systems
272
290
  "cargo",
273
291
  "go",
292
+ "rustc",
293
+ "make",
294
+ "cmake",
295
+ "gradle",
296
+ "mvn",
297
+ "java",
298
+ "javac",
299
+ "dotnet",
300
+ # Version control
301
+ "git",
302
+ "gh",
303
+ # Shell utilities (safe read/inspect)
304
+ "cat",
305
+ "ls",
306
+ "echo",
307
+ "find",
308
+ "head",
309
+ "tail",
310
+ "wc",
311
+ "sort",
312
+ "uniq",
313
+ "cut",
314
+ "awk",
315
+ "sed",
316
+ "grep",
317
+ "rg",
318
+ "ag",
319
+ # File management
320
+ "mkdir",
321
+ "cp",
322
+ "mv",
323
+ "touch",
324
+ "chmod",
325
+ "chown",
326
+ # Networking (read)
327
+ "curl",
328
+ "wget",
329
+ # Containers
330
+ "docker",
331
+ "docker-compose",
332
+ "kubectl",
333
+ # Database
334
+ "psql",
335
+ "mysql",
336
+ "sqlite3",
337
+ # Misc
338
+ "jq",
339
+ "yq",
340
+ "which",
341
+ "env",
342
+ "printenv",
343
+ "date",
344
+ "pwd",
345
+ "diff",
346
+ "patch",
347
+ "zip",
348
+ "unzip",
349
+ "tar",
274
350
  )
275
351
  )
276
352
 
@@ -18,6 +18,7 @@ READ_ONLY_TOOLS: frozenset[str] = frozenset(
18
18
  "list_directory",
19
19
  "glob_files",
20
20
  "grep_content",
21
+ "web_fetch",
21
22
  }
22
23
  )
23
24
 
@@ -27,11 +28,12 @@ MUTATING_TOOLS: frozenset[str] = frozenset(
27
28
  "write_file",
28
29
  "search_replace",
29
30
  "delete_file",
31
+ "move_file",
30
32
  }
31
33
  )
32
34
 
33
- # Subprocess (allowlist enforced inside tool)
34
- SHELL_TOOLS: frozenset[str] = frozenset({"run_command"})
35
+ # Subprocess (allowlist or bash -c; require --yes / not strict)
36
+ SHELL_TOOLS: frozenset[str] = frozenset({"run_command", "bash"})
35
37
 
36
38
  # Session planning only (no disk / shell; no extra permission)
37
39
  PLANNING_TOOLS: frozenset[str] = frozenset({"todo_write"})
@@ -3,27 +3,34 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from gemcode.config import GemCodeConfig
6
+ from gemcode.tools.bash import make_bash_tool
6
7
  from gemcode.tools.edit import make_edit_tools
7
8
  from gemcode.tools.filesystem import make_filesystem_tools
8
9
  from gemcode.tools.search import make_grep_tool
9
10
  from gemcode.tools.shell import make_run_command
10
11
  from gemcode.tools.todo import make_todo_tool
12
+ from gemcode.tools.web import make_web_fetch_tool
11
13
 
12
14
 
13
15
  def build_function_tools(cfg: GemCodeConfig) -> list:
14
- read_file, list_directory, glob_files, delete_file = make_filesystem_tools(cfg)
16
+ read_file, list_directory, glob_files, delete_file, move_file = make_filesystem_tools(cfg)
15
17
  grep_content = make_grep_tool(cfg)
16
18
  run_command = make_run_command(cfg)
19
+ bash = make_bash_tool(cfg)
17
20
  write_file, search_replace = make_edit_tools(cfg)
18
21
  todo_write = make_todo_tool(cfg)
22
+ web_fetch = make_web_fetch_tool()
19
23
  return [
20
24
  todo_write,
21
25
  read_file,
22
26
  list_directory,
23
27
  glob_files,
24
- delete_file,
25
28
  grep_content,
29
+ bash,
26
30
  run_command,
27
31
  write_file,
28
32
  search_replace,
33
+ move_file,
34
+ delete_file,
35
+ web_fetch,
29
36
  ]
@@ -0,0 +1,127 @@
1
+ """
2
+ Arbitrary shell execution via bash -c.
3
+
4
+ Analogous to OpenClaude's BashTool — allows pipelines, redirects, multi-step
5
+ shell workflows that run_command (basename-only) cannot express.
6
+
7
+ Security model: same gating as run_command (SHELL_TOOLS category), requires
8
+ --yes / interactive approval. Timeout-bounded; no TTY for child processes.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import shutil
15
+ import subprocess
16
+ from pathlib import Path
17
+
18
+ from gemcode.config import GemCodeConfig
19
+ from gemcode.paths import PathEscapeError, resolve_under_root
20
+ from gemcode.trust import is_trusted_root
21
+
22
+
23
+ def make_bash_tool(cfg: GemCodeConfig):
24
+ root = cfg.project_root
25
+ trusted = is_trusted_root(root)
26
+
27
+ def bash(
28
+ command: str,
29
+ timeout_seconds: int = 120,
30
+ cwd_subdir: str = ".",
31
+ background: bool = False,
32
+ ) -> dict:
33
+ """
34
+ Run an arbitrary shell command via bash. Supports pipelines, redirects,
35
+ subshells, and multi-step workflows that run_command cannot express.
36
+
37
+ Use this for:
38
+ - Git operations: bash("git log --oneline -20")
39
+ - Pipelines: bash("cat package.json | python3 -m json.tool")
40
+ - Finding files: bash("find . -name '*.py' -newer setup.py | head -20")
41
+ - Complex builds: bash("cd frontend && npm ci && npm run build")
42
+ - Inspecting output: bash("ls -la | grep '.py'")
43
+ - Running tests with flags: bash("pytest -x -q --tb=short 2>&1 | head -100")
44
+
45
+ For long-running servers use background=True. cwd_subdir sets working
46
+ directory relative to the project root.
47
+
48
+ IMPORTANT: This runs real shell commands. Be precise and avoid destructive
49
+ operations (rm -rf, force-push, etc.) without explicit user approval.
50
+ """
51
+ if not trusted:
52
+ return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
53
+
54
+ if not command or not command.strip():
55
+ return {"error": "command must not be empty"}
56
+
57
+ if timeout_seconds < 1:
58
+ timeout_seconds = 1
59
+ if timeout_seconds > 600:
60
+ timeout_seconds = 600
61
+
62
+ bash_exe = shutil.which("bash")
63
+ if not bash_exe:
64
+ return {"error": "bash not found on PATH"}
65
+
66
+ if not (cwd_subdir or "").strip():
67
+ cwd_subdir = "."
68
+ try:
69
+ exec_cwd = resolve_under_root(root, cwd_subdir)
70
+ except PathEscapeError as e:
71
+ return {"error": str(e)}
72
+ if not exec_cwd.is_dir():
73
+ return {
74
+ "error": (
75
+ f"cwd_subdir={cwd_subdir!r} is not an existing directory under the project. "
76
+ "Create it first or fix the path."
77
+ )
78
+ }
79
+
80
+ env = {**os.environ}
81
+
82
+ if background:
83
+ try:
84
+ proc = subprocess.Popen(
85
+ [bash_exe, "-c", command],
86
+ cwd=str(exec_cwd),
87
+ env=env,
88
+ stdout=subprocess.DEVNULL,
89
+ stderr=subprocess.DEVNULL,
90
+ start_new_session=True,
91
+ )
92
+ except OSError as e:
93
+ return {"error": f"Failed to start background process: {e}"}
94
+ return {
95
+ "command": command,
96
+ "cwd": str(exec_cwd.relative_to(root)) if exec_cwd != root else ".",
97
+ "background": True,
98
+ "pid": proc.pid,
99
+ "note": "Process started in the background. Stop it with kill from the OS when done.",
100
+ }
101
+
102
+ try:
103
+ proc = subprocess.run(
104
+ [bash_exe, "-c", command],
105
+ cwd=str(exec_cwd),
106
+ capture_output=True,
107
+ text=True,
108
+ timeout=timeout_seconds,
109
+ env=env,
110
+ check=False,
111
+ )
112
+ stdout = proc.stdout[:80_000]
113
+ stderr = proc.stderr[:20_000]
114
+ result: dict = {
115
+ "command": command,
116
+ "cwd": str(exec_cwd.relative_to(root)) if exec_cwd != root else ".",
117
+ "exit_code": proc.returncode,
118
+ "stdout": stdout,
119
+ "stderr": stderr,
120
+ }
121
+ if proc.returncode != 0 and not stdout and not stderr:
122
+ result["note"] = f"Command exited {proc.returncode} with no output."
123
+ return result
124
+ except subprocess.TimeoutExpired:
125
+ return {"error": f"Timeout after {timeout_seconds}s", "command": command}
126
+
127
+ return bash