gemcode 0.3.30__tar.gz → 0.3.31__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.30/src/gemcode.egg-info → gemcode-0.3.31}/PKG-INFO +1 -1
- {gemcode-0.3.30 → gemcode-0.3.31}/pyproject.toml +1 -1
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/agent.py +96 -6
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/cli.py +4 -14
- gemcode-0.3.31/src/gemcode/mcp_loader.py +147 -0
- gemcode-0.3.31/src/gemcode/openapi_loader.py +127 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/repl_commands.py +8 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/repl_slash.py +111 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/session_runtime.py +21 -0
- gemcode-0.3.31/src/gemcode/tools/notes.py +106 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tui/input_handler.py +3 -0
- {gemcode-0.3.30 → gemcode-0.3.31/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode.egg-info/SOURCES.txt +2 -0
- gemcode-0.3.30/src/gemcode/mcp_loader.py +0 -57
- {gemcode-0.3.30 → gemcode-0.3.31}/LICENSE +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/MANIFEST.in +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/README.md +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/setup.cfg +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/config.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/version.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_credentials.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_paths.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_permissions.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_tools.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_workspace_hints.py +0 -0
|
@@ -49,11 +49,77 @@ def _chain_before_model_callbacks(*callbacks):
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def _load_gemini_md(project_root: Path) -> str:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
"""
|
|
53
|
+
Load GEMINI.md / .gemcode/NOTES.md from a Claude Code–style hierarchy.
|
|
54
|
+
|
|
55
|
+
Priority (later entries override earlier ones, all are concatenated):
|
|
56
|
+
1. ~/.gemcode/GEMINI.md — user-global instructions (all projects)
|
|
57
|
+
2. Walk UP from project_root: each directory's GEMINI.md / .gemcode/GEMINI.md
|
|
58
|
+
(org-level files at higher dirs, project-level at project_root)
|
|
59
|
+
3. project_root/GEMINI.md — the primary project instructions
|
|
60
|
+
4. project_root/.gemcode/GEMINI.md — alternative location
|
|
61
|
+
5. project_root/.gemcode/notes.md — agent auto-generated notes (read-only context)
|
|
62
|
+
|
|
63
|
+
Max total: 80,000 chars. Each file is capped at 30,000 chars.
|
|
64
|
+
HTML comments (<!-- ... -->) are stripped before injection (saves tokens).
|
|
65
|
+
"""
|
|
66
|
+
import re
|
|
67
|
+
|
|
68
|
+
_NAMES = ("GEMINI.md", "gemini.md", ".gemcode/GEMINI.md")
|
|
69
|
+
_FILE_CAP = 30_000
|
|
70
|
+
_TOTAL_CAP = 80_000
|
|
71
|
+
_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
72
|
+
|
|
73
|
+
def _read(p: Path) -> str:
|
|
74
|
+
if not p.is_file():
|
|
75
|
+
return ""
|
|
76
|
+
try:
|
|
77
|
+
raw = p.read_text(encoding="utf-8", errors="replace")[:_FILE_CAP]
|
|
78
|
+
# Strip HTML comments (like Claude Code does — saves tokens)
|
|
79
|
+
return _COMMENT_RE.sub("", raw).strip()
|
|
80
|
+
except OSError:
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
seen: set[Path] = set()
|
|
84
|
+
sections: list[str] = []
|
|
85
|
+
|
|
86
|
+
def _add(p: Path, label: str | None = None) -> None:
|
|
87
|
+
resolved = p.resolve()
|
|
88
|
+
if resolved in seen:
|
|
89
|
+
return
|
|
90
|
+
seen.add(resolved)
|
|
91
|
+
text = _read(p)
|
|
92
|
+
if text:
|
|
93
|
+
sections.append(f"<!-- {label or str(p)} -->\n{text}" if label else text)
|
|
94
|
+
|
|
95
|
+
# 1. User-global: ~/.gemcode/GEMINI.md
|
|
96
|
+
user_global = Path.home() / ".gemcode" / "GEMINI.md"
|
|
97
|
+
_add(user_global, "user-global (~/.gemcode/GEMINI.md)")
|
|
98
|
+
|
|
99
|
+
# 2. Walk UP from project_root to filesystem root — loads org / monorepo-level instructions
|
|
100
|
+
walk = project_root.resolve()
|
|
101
|
+
ancestors = []
|
|
102
|
+
while walk != walk.parent:
|
|
103
|
+
walk = walk.parent
|
|
104
|
+
if walk == Path.home() or walk == Path("/"):
|
|
105
|
+
break
|
|
106
|
+
ancestors.append(walk)
|
|
107
|
+
# Walk outer→inner (org first, closer dirs later — later = higher priority)
|
|
108
|
+
for ancestor in reversed(ancestors):
|
|
109
|
+
for name in _NAMES:
|
|
110
|
+
_add(ancestor / name)
|
|
111
|
+
|
|
112
|
+
# 3+4. Project-root level instructions (primary location)
|
|
113
|
+
for name in ("GEMINI.md", "gemini.md", ".gemcode/GEMINI.md", ".gemcode/gemini.md"):
|
|
114
|
+
_add(project_root / name)
|
|
115
|
+
|
|
116
|
+
# 5. Agent-generated notes (informational context, not instructions)
|
|
117
|
+
notes = project_root / ".gemcode" / "notes.md"
|
|
118
|
+
if notes.is_file():
|
|
119
|
+
_add(notes, "agent notes (.gemcode/notes.md)")
|
|
120
|
+
|
|
121
|
+
combined = "\n\n---\n\n".join(s for s in sections if s.strip())
|
|
122
|
+
return combined[:_TOTAL_CAP]
|
|
57
123
|
|
|
58
124
|
|
|
59
125
|
def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
@@ -527,7 +593,23 @@ For tasks where quality matters:
|
|
|
527
593
|
- Prefer small, testable, accurate changes over broad rewrites.
|
|
528
594
|
|
|
529
595
|
## Workspace scope
|
|
530
|
-
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`.
|
|
596
|
+
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`.
|
|
597
|
+
|
|
598
|
+
## Agent notes (.gemcode/notes.md)
|
|
599
|
+
You have two tools to persist project insights across sessions, like Claude Code's auto-memory:
|
|
600
|
+
|
|
601
|
+
- **`append_project_note(note)`** — write a note to `.gemcode/notes.md`. Use this proactively when you discover something worth remembering:
|
|
602
|
+
- Build/test/lint commands you discover ("Build: `npm run build` — requires Node 20")
|
|
603
|
+
- Key file locations ("Auth middleware: `src/middleware/auth.ts`")
|
|
604
|
+
- Known issues or patterns ("DB migrations: always run `prisma db push` after schema changes")
|
|
605
|
+
- User workflow preferences ("User prefers running tests before committing")
|
|
606
|
+
- Architecture decisions or tricky patterns
|
|
607
|
+
|
|
608
|
+
Call this **immediately** when you discover something useful — not just at the end of tasks.
|
|
609
|
+
Notes are loaded at session start so future sessions inherit this knowledge.
|
|
610
|
+
|
|
611
|
+
- **`read_project_notes()`** — read current notes before starting a new project task.
|
|
612
|
+
If notes exist and you haven't read them yet, read them first to avoid re-discovering known information."""
|
|
531
613
|
|
|
532
614
|
# Inject capability-specific strategy sections only when those caps are on.
|
|
533
615
|
if getattr(cfg, "enable_computer_use", False):
|
|
@@ -590,6 +672,14 @@ def build_root_agent(
|
|
|
590
672
|
except Exception:
|
|
591
673
|
pass
|
|
592
674
|
|
|
675
|
+
# Agent auto-notes: write project insights to .gemcode/notes.md (Claude Code MEMORY.md equivalent)
|
|
676
|
+
try:
|
|
677
|
+
from gemcode.tools.notes import build_notes_tools
|
|
678
|
+
notes_tools = build_notes_tools(cfg.project_root)
|
|
679
|
+
tools = [*tools, *notes_tools]
|
|
680
|
+
except Exception:
|
|
681
|
+
pass
|
|
682
|
+
|
|
593
683
|
if extra_tools:
|
|
594
684
|
tools = [*tools, *extra_tools]
|
|
595
685
|
|
|
@@ -151,13 +151,8 @@ async def _run_prompt(
|
|
|
151
151
|
_maybe_prompt_google_api_key()
|
|
152
152
|
require_google_api_key()
|
|
153
153
|
_initialize_gemcode_project(cfg)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
from gemcode.mcp_loader import load_mcp_toolsets
|
|
157
|
-
|
|
158
|
-
extra = load_mcp_toolsets(cfg)
|
|
159
|
-
|
|
160
|
-
runner = create_runner(cfg, extra_tools=extra or None)
|
|
154
|
+
# MCP and OpenAPI toolsets are now loaded inside create_runner() directly.
|
|
155
|
+
runner = create_runner(cfg, extra_tools=None)
|
|
161
156
|
try:
|
|
162
157
|
collected = await run_turn(
|
|
163
158
|
runner,
|
|
@@ -184,13 +179,8 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
184
179
|
require_google_api_key()
|
|
185
180
|
_initialize_gemcode_project(cfg)
|
|
186
181
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
from gemcode.mcp_loader import load_mcp_toolsets
|
|
190
|
-
|
|
191
|
-
extra = load_mcp_toolsets(cfg)
|
|
192
|
-
|
|
193
|
-
runner = create_runner(cfg, extra_tools=extra or None)
|
|
182
|
+
# MCP and OpenAPI toolsets are now loaded inside create_runner() directly.
|
|
183
|
+
runner = create_runner(cfg, extra_tools=None)
|
|
194
184
|
try:
|
|
195
185
|
# For CLI UX, show concise tool summaries (helps users see what ran).
|
|
196
186
|
if os.environ.get("GEMCODE_EMIT_TOOL_USE_SUMMARIES") is None:
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional MCP toolsets from `.gemcode/mcp.json`.
|
|
3
|
+
|
|
4
|
+
Now supports all three ADK connection types — like Claude Code's MCP integration:
|
|
5
|
+
|
|
6
|
+
Schema (example):
|
|
7
|
+
{
|
|
8
|
+
"servers": [
|
|
9
|
+
{
|
|
10
|
+
"name": "filesystem",
|
|
11
|
+
"stdio": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] }
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "github",
|
|
15
|
+
"http": {
|
|
16
|
+
"url": "https://api.githubcopilot.com/mcp/",
|
|
17
|
+
"headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "notion",
|
|
22
|
+
"sse": { "url": "https://mcp.notion.com/sse" }
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Connection types:
|
|
28
|
+
stdio — local subprocess (npx, python, etc.) — legacy, always worked
|
|
29
|
+
http — Streamable HTTP (remote servers, Cloud Run, Smithery.ai)
|
|
30
|
+
sse — Server-Sent Events (older remote servers; use http when possible)
|
|
31
|
+
|
|
32
|
+
Header values support ${ENV_VAR} substitution from environment.
|
|
33
|
+
|
|
34
|
+
Requires: pip install gemcode[mcp]
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import os
|
|
41
|
+
import re
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
from gemcode.config import GemCodeConfig
|
|
46
|
+
|
|
47
|
+
_ENV_VAR_RE = re.compile(r"\$\{([A-Z_][A-Z0-9_]*)\}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _expand_env(value: str) -> str:
|
|
51
|
+
"""Expand ${VAR} placeholders from environment variables."""
|
|
52
|
+
return _ENV_VAR_RE.sub(lambda m: os.environ.get(m.group(1), m.group(0)), value)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _expand_headers(headers: dict) -> dict:
|
|
56
|
+
return {k: _expand_env(str(v)) for k, v in headers.items()}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_mcp_toolsets(cfg: GemCodeConfig) -> list:
|
|
60
|
+
path = cfg.project_root / ".gemcode" / "mcp.json"
|
|
61
|
+
if not path.is_file():
|
|
62
|
+
return []
|
|
63
|
+
try:
|
|
64
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
65
|
+
except json.JSONDecodeError as e:
|
|
66
|
+
raise ValueError(f"Invalid mcp.json: {e}") from e
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
|
|
70
|
+
from mcp import StdioServerParameters
|
|
71
|
+
except ImportError as e:
|
|
72
|
+
raise ImportError("Install MCP extras: pip install gemcode[mcp]") from e
|
|
73
|
+
|
|
74
|
+
servers = data.get("servers") or []
|
|
75
|
+
toolsets: list[Any] = []
|
|
76
|
+
|
|
77
|
+
for s in servers:
|
|
78
|
+
prefix = s.get("name") or "mcp"
|
|
79
|
+
tool_filter: list[str] | None = s.get("tools") or None # optional allowlist
|
|
80
|
+
|
|
81
|
+
# ── stdio (local subprocess) ─────────────────────────────────────────
|
|
82
|
+
if "stdio" in s:
|
|
83
|
+
stdio = s["stdio"]
|
|
84
|
+
cmd = stdio.get("command")
|
|
85
|
+
args = stdio.get("args") or []
|
|
86
|
+
env_extra = {k: _expand_env(str(v)) for k, v in (stdio.get("env") or {}).items()}
|
|
87
|
+
if not cmd:
|
|
88
|
+
continue
|
|
89
|
+
params = StdioServerParameters(
|
|
90
|
+
command=cmd,
|
|
91
|
+
args=args,
|
|
92
|
+
env={**os.environ, **env_extra} if env_extra else None,
|
|
93
|
+
)
|
|
94
|
+
kw: dict[str, Any] = dict(connection_params=params, tool_name_prefix=prefix)
|
|
95
|
+
if tool_filter:
|
|
96
|
+
kw["tool_filter"] = tool_filter
|
|
97
|
+
toolsets.append(McpToolset(**kw))
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
# ── http (Streamable HTTP — modern, Cloud Run-friendly) ──────────────
|
|
101
|
+
if "http" in s:
|
|
102
|
+
http_cfg = s["http"]
|
|
103
|
+
url = _expand_env(http_cfg.get("url", ""))
|
|
104
|
+
if not url:
|
|
105
|
+
continue
|
|
106
|
+
headers = _expand_headers(http_cfg.get("headers") or {})
|
|
107
|
+
try:
|
|
108
|
+
from google.adk.tools.mcp_tool.mcp_session_manager import (
|
|
109
|
+
StreamableHTTPConnectionParams,
|
|
110
|
+
)
|
|
111
|
+
params_http = StreamableHTTPConnectionParams(url=url, headers=headers or None)
|
|
112
|
+
kw = dict(connection_params=params_http, tool_name_prefix=prefix)
|
|
113
|
+
if tool_filter:
|
|
114
|
+
kw["tool_filter"] = tool_filter
|
|
115
|
+
toolsets.append(McpToolset(**kw))
|
|
116
|
+
except ImportError:
|
|
117
|
+
# Fallback: try SseConnectionParams for older ADK builds
|
|
118
|
+
try:
|
|
119
|
+
from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams
|
|
120
|
+
params_sse_fb = SseConnectionParams(url=url, headers=headers or None)
|
|
121
|
+
kw = dict(connection_params=params_sse_fb, tool_name_prefix=prefix)
|
|
122
|
+
if tool_filter:
|
|
123
|
+
kw["tool_filter"] = tool_filter
|
|
124
|
+
toolsets.append(McpToolset(**kw))
|
|
125
|
+
except ImportError:
|
|
126
|
+
pass # ADK version doesn't support remote MCP — skip silently
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
# ── sse (Server-Sent Events — older remote servers) ──────────────────
|
|
130
|
+
if "sse" in s:
|
|
131
|
+
sse_cfg = s["sse"]
|
|
132
|
+
url = _expand_env(sse_cfg.get("url", ""))
|
|
133
|
+
if not url:
|
|
134
|
+
continue
|
|
135
|
+
headers = _expand_headers(sse_cfg.get("headers") or {})
|
|
136
|
+
try:
|
|
137
|
+
from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams
|
|
138
|
+
params_sse = SseConnectionParams(url=url, headers=headers or None)
|
|
139
|
+
kw = dict(connection_params=params_sse, tool_name_prefix=prefix)
|
|
140
|
+
if tool_filter:
|
|
141
|
+
kw["tool_filter"] = tool_filter
|
|
142
|
+
toolsets.append(McpToolset(**kw))
|
|
143
|
+
except ImportError:
|
|
144
|
+
pass # ADK version too old — skip
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
return toolsets
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-loads OpenAPI specs from `.gemcode/openapi/` as ADK OpenAPIToolset instances.
|
|
3
|
+
|
|
4
|
+
Inspired by the ADK community pattern: drop an OpenAPI spec in a directory and
|
|
5
|
+
the agent automatically gets REST API tools for it — no manual tool wiring needed.
|
|
6
|
+
|
|
7
|
+
Supported formats: .yaml, .yml, .json
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
.gemcode/openapi/
|
|
11
|
+
├── github.yaml → tools: mcp__github__list_repos, mcp__github__create_pr, …
|
|
12
|
+
├── sentry.json → tools: mcp__sentry__list_issues, …
|
|
13
|
+
└── internal_api.yaml → tools from your company's internal REST API
|
|
14
|
+
|
|
15
|
+
The filename (without extension) is used as the tool name prefix.
|
|
16
|
+
So `github.yaml` → tools prefixed with `github_`.
|
|
17
|
+
|
|
18
|
+
Auth:
|
|
19
|
+
Each spec file can have a matching `.auth` sidecar file with JSON:
|
|
20
|
+
{
|
|
21
|
+
"type": "api_key", // "api_key" | "bearer" | "basic" | "oauth2"
|
|
22
|
+
"header": "X-API-Key",
|
|
23
|
+
"value": "${GITHUB_TOKEN}" // ${ENV_VAR} is expanded from environment
|
|
24
|
+
}
|
|
25
|
+
For oauth2: {"type": "oauth2", "token_url": "...", "client_id": "...", "client_secret": "${VAR}"}
|
|
26
|
+
|
|
27
|
+
Requires: pip install google-adk (OpenAPIToolset is included in the main package)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import logging
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
_ENV_VAR_RE = re.compile(r"\$\{([A-Z_][A-Z0-9_]*)\}")
|
|
42
|
+
_OPENAPI_DIR = ".gemcode/openapi"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _expand(value: str) -> str:
|
|
46
|
+
return _ENV_VAR_RE.sub(lambda m: os.environ.get(m.group(1), m.group(0)), value)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _load_auth(spec_path: Path):
|
|
50
|
+
"""Try to load auth config from <spec_stem>.auth next to the spec file."""
|
|
51
|
+
auth_path = spec_path.with_suffix(".auth")
|
|
52
|
+
if not auth_path.is_file():
|
|
53
|
+
auth_path = spec_path.parent / (spec_path.stem + ".auth.json")
|
|
54
|
+
if not auth_path.is_file():
|
|
55
|
+
return None, None
|
|
56
|
+
try:
|
|
57
|
+
auth_data = json.loads(auth_path.read_text())
|
|
58
|
+
except Exception:
|
|
59
|
+
return None, None
|
|
60
|
+
|
|
61
|
+
auth_type = auth_data.get("type", "").lower()
|
|
62
|
+
try:
|
|
63
|
+
from google.adk.auth import AuthCredential, AuthCredentialTypes
|
|
64
|
+
if auth_type == "api_key":
|
|
65
|
+
return None, AuthCredential(
|
|
66
|
+
auth_type=AuthCredentialTypes.API_KEY,
|
|
67
|
+
api_key=_expand(auth_data.get("value", "")),
|
|
68
|
+
)
|
|
69
|
+
if auth_type in ("bearer", "token"):
|
|
70
|
+
return None, AuthCredential(
|
|
71
|
+
auth_type=AuthCredentialTypes.HTTP,
|
|
72
|
+
http={"scheme": "bearer", "token": _expand(auth_data.get("value", ""))},
|
|
73
|
+
)
|
|
74
|
+
except ImportError:
|
|
75
|
+
pass
|
|
76
|
+
return None, None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_openapi_toolsets(project_root: Path) -> list[Any]:
|
|
80
|
+
"""
|
|
81
|
+
Scan .gemcode/openapi/ for spec files and return OpenAPIToolset instances.
|
|
82
|
+
|
|
83
|
+
Returns an empty list if the directory doesn't exist or no valid specs are found.
|
|
84
|
+
Errors loading individual specs are logged but don't crash GemCode.
|
|
85
|
+
"""
|
|
86
|
+
openapi_dir = project_root / _OPENAPI_DIR
|
|
87
|
+
if not openapi_dir.is_dir():
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_toolset import (
|
|
92
|
+
OpenAPIToolset,
|
|
93
|
+
)
|
|
94
|
+
except ImportError:
|
|
95
|
+
log.debug("[openapi] OpenAPIToolset not available in this ADK version — skipping")
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
toolsets: list[Any] = []
|
|
99
|
+
|
|
100
|
+
for spec_file in sorted(openapi_dir.iterdir()):
|
|
101
|
+
if spec_file.suffix.lower() not in (".yaml", ".yml", ".json"):
|
|
102
|
+
continue
|
|
103
|
+
if spec_file.stem.endswith(".auth"):
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
prefix = spec_file.stem # e.g. "github" from "github.yaml"
|
|
107
|
+
fmt = "json" if spec_file.suffix.lower() == ".json" else "yaml"
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
spec_str = spec_file.read_text(encoding="utf-8", errors="replace")
|
|
111
|
+
except OSError as e:
|
|
112
|
+
log.warning("[openapi] Could not read %s: %s", spec_file.name, e)
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
_, auth_credential = _load_auth(spec_file)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
kwargs: dict[str, Any] = dict(spec_str=spec_str, spec_str_type=fmt)
|
|
119
|
+
if auth_credential is not None:
|
|
120
|
+
kwargs["auth_credential"] = auth_credential
|
|
121
|
+
toolset = OpenAPIToolset(**kwargs)
|
|
122
|
+
toolsets.append(toolset)
|
|
123
|
+
log.info("[openapi] Loaded spec '%s' from %s", prefix, spec_file.name)
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
log.warning("[openapi] Failed to load %s: %s", spec_file.name, exc)
|
|
126
|
+
|
|
127
|
+
return toolsets
|
|
@@ -189,6 +189,14 @@ def slash_help_lines() -> list[str]:
|
|
|
189
189
|
" (CLI) gemcode -C DIR Use a project folder as root (recommended vs. ~ )",
|
|
190
190
|
" (CLI) gemcode login Save or change API key (~/.gemcode/credentials.json)",
|
|
191
191
|
"",
|
|
192
|
+
" Project setup:",
|
|
193
|
+
" /init Analyze project structure and generate GEMINI.md",
|
|
194
|
+
" /init force Regenerate GEMINI.md even if it already exists",
|
|
195
|
+
" /cost Show token usage and estimated cost for this session",
|
|
196
|
+
" /notes Show agent auto-notes (.gemcode/notes.md)",
|
|
197
|
+
" /notes clear Delete all notes",
|
|
198
|
+
" /notes edit Open notes in $EDITOR",
|
|
199
|
+
"",
|
|
192
200
|
" Session:",
|
|
193
201
|
" /help Show this help",
|
|
194
202
|
" /status Full status: model, capabilities, thinking, limits",
|
|
@@ -154,6 +154,117 @@ async def process_repl_slash(
|
|
|
154
154
|
out()
|
|
155
155
|
return ReplSlashResult(skip_model_turn=True)
|
|
156
156
|
|
|
157
|
+
# ── /init ─────────────────────────────────────────────────────────────────
|
|
158
|
+
# ── /notes ────────────────────────────────────────────────────────────────
|
|
159
|
+
if name == "notes":
|
|
160
|
+
sub = (sc.args or "").strip().lower()
|
|
161
|
+
notes_path = cfg.project_root / ".gemcode" / "notes.md"
|
|
162
|
+
if sub == "clear":
|
|
163
|
+
if notes_path.exists():
|
|
164
|
+
notes_path.unlink()
|
|
165
|
+
out("Notes cleared.")
|
|
166
|
+
else:
|
|
167
|
+
out("No notes file found.")
|
|
168
|
+
out()
|
|
169
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
170
|
+
if sub == "edit":
|
|
171
|
+
import subprocess
|
|
172
|
+
editor = os.environ.get("EDITOR", "nano")
|
|
173
|
+
notes_path.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
if not notes_path.exists():
|
|
175
|
+
notes_path.write_text("# GemCode Agent Notes\n*Auto-generated project notes.*\n\n", encoding="utf-8")
|
|
176
|
+
subprocess.run([editor, str(notes_path)])
|
|
177
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
178
|
+
# Default: show notes
|
|
179
|
+
if notes_path.exists():
|
|
180
|
+
content = notes_path.read_text(encoding="utf-8", errors="replace")
|
|
181
|
+
out(f"Agent notes ({notes_path}):")
|
|
182
|
+
out("─" * 60)
|
|
183
|
+
out(content)
|
|
184
|
+
out("─" * 60)
|
|
185
|
+
out(" /notes clear Delete all notes")
|
|
186
|
+
out(" /notes edit Open in $EDITOR")
|
|
187
|
+
else:
|
|
188
|
+
out(f"No agent notes yet ({notes_path}).")
|
|
189
|
+
out("The agent will auto-create notes as it discovers project insights.")
|
|
190
|
+
out()
|
|
191
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
192
|
+
|
|
193
|
+
# ── /cost ─────────────────────────────────────────────────────────────────
|
|
194
|
+
if name == "cost":
|
|
195
|
+
from gemcode.pricing import format_cost, format_tokens
|
|
196
|
+
stats = getattr(cfg, "_last_turn_stats", None)
|
|
197
|
+
out("Session cost summary")
|
|
198
|
+
out("─" * 40)
|
|
199
|
+
if stats:
|
|
200
|
+
out(f" Last turn input tokens : {format_tokens(stats.get('in', 0) or 0)}")
|
|
201
|
+
out(f" Last turn output tokens: {format_tokens(stats.get('out', 0) or 0)}")
|
|
202
|
+
think = stats.get("think", 0) or 0
|
|
203
|
+
if think:
|
|
204
|
+
out(f" Last turn thinking : {format_tokens(think)}")
|
|
205
|
+
cache = stats.get("cache", 0) or 0
|
|
206
|
+
if cache:
|
|
207
|
+
out(f" Last turn cache read : {format_tokens(cache)}")
|
|
208
|
+
lc = stats.get("turn_cost")
|
|
209
|
+
out(f" Last turn cost : {format_cost(lc) if lc is not None else '(unknown model pricing)'}")
|
|
210
|
+
out()
|
|
211
|
+
out(f" Session total tokens : {format_tokens(stats.get('session_total', 0) or 0)}")
|
|
212
|
+
sc = stats.get("session_cost")
|
|
213
|
+
if sc and sc > 0:
|
|
214
|
+
out(f" Session total cost : {format_cost(sc)}")
|
|
215
|
+
else:
|
|
216
|
+
out(" Session total cost : (accumulating — will show after first turn)")
|
|
217
|
+
else:
|
|
218
|
+
out(" No turn completed yet. Send a message to see token/cost stats.")
|
|
219
|
+
out()
|
|
220
|
+
out(f" Model: {getattr(cfg, 'model', 'unknown')}")
|
|
221
|
+
out()
|
|
222
|
+
out(" Note: costs are estimates based on published Gemini pricing.")
|
|
223
|
+
out(" Thinking tokens billed at same rate as output tokens.")
|
|
224
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
225
|
+
|
|
226
|
+
if name == "init":
|
|
227
|
+
gemini_md = cfg.project_root / "GEMINI.md"
|
|
228
|
+
if gemini_md.exists() and (sc.args or "").strip().lower() not in ("force", "overwrite", "-f"):
|
|
229
|
+
out(f"GEMINI.md already exists at {gemini_md}.")
|
|
230
|
+
out("Use /init force to regenerate it, or edit it manually.")
|
|
231
|
+
out()
|
|
232
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
233
|
+
# Dispatch to the model to analyze the project and write GEMINI.md.
|
|
234
|
+
init_prompt = (
|
|
235
|
+
"Analyze this codebase and generate a GEMINI.md file for me.\n\n"
|
|
236
|
+
"To do this:\n"
|
|
237
|
+
"1. Run `list_directory('.')` to understand the project structure\n"
|
|
238
|
+
"2. Read `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, `README.md` "
|
|
239
|
+
"or equivalent to understand the project type and dependencies\n"
|
|
240
|
+
"3. Look at the source directory structure (src/, lib/, app/, etc.)\n"
|
|
241
|
+
"4. Check for test directories and test runner config\n"
|
|
242
|
+
"5. Look for linting/formatting config files (.eslintrc, .prettierrc, ruff.toml, etc.)\n\n"
|
|
243
|
+
"Then write a GEMINI.md file at the project root containing:\n"
|
|
244
|
+
"# Project Name\n"
|
|
245
|
+
"One-sentence description.\n\n"
|
|
246
|
+
"## Build & Test\n"
|
|
247
|
+
"- How to install dependencies\n"
|
|
248
|
+
"- How to build\n"
|
|
249
|
+
"- How to run tests\n"
|
|
250
|
+
"- How to lint/format\n\n"
|
|
251
|
+
"## Architecture\n"
|
|
252
|
+
"- Key directories and what they contain\n"
|
|
253
|
+
"- Important entry points\n"
|
|
254
|
+
"- Key abstractions or patterns used\n\n"
|
|
255
|
+
"## Coding standards\n"
|
|
256
|
+
"- Language/framework conventions\n"
|
|
257
|
+
"- Any style requirements found in config files\n\n"
|
|
258
|
+
"## Workflow\n"
|
|
259
|
+
"- Any git branching rules from README or CONTRIBUTING\n"
|
|
260
|
+
"- PR/commit conventions\n\n"
|
|
261
|
+
"Keep it under 200 lines. Write the file to GEMINI.md now."
|
|
262
|
+
)
|
|
263
|
+
out("Analyzing project to generate GEMINI.md…")
|
|
264
|
+
out("(GemCode will read the project structure and write a starting GEMINI.md)")
|
|
265
|
+
out()
|
|
266
|
+
return ReplSlashResult(model_prompt=init_prompt)
|
|
267
|
+
|
|
157
268
|
if name == "hooks":
|
|
158
269
|
out("\n".join(format_hooks_lines(cfg)))
|
|
159
270
|
out()
|
|
@@ -56,6 +56,27 @@ def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None) -> Runner
|
|
|
56
56
|
else:
|
|
57
57
|
merged_extra_tools = modality_tools or None
|
|
58
58
|
|
|
59
|
+
# ── MCP toolsets from .gemcode/mcp.json ─────────────────────────────────
|
|
60
|
+
# Supports stdio, http (Streamable HTTP), and sse connection types.
|
|
61
|
+
try:
|
|
62
|
+
from gemcode.mcp_loader import load_mcp_toolsets
|
|
63
|
+
mcp_tools = load_mcp_toolsets(cfg)
|
|
64
|
+
if mcp_tools:
|
|
65
|
+
merged_extra_tools = list(merged_extra_tools or []) + mcp_tools
|
|
66
|
+
except Exception:
|
|
67
|
+
pass # MCP not installed or mcp.json invalid — continue without
|
|
68
|
+
|
|
69
|
+
# ── OpenAPI toolsets from .gemcode/openapi/ ──────────────────────────────
|
|
70
|
+
# Drop any *.yaml / *.json OpenAPI spec in .gemcode/openapi/ to auto-generate
|
|
71
|
+
# REST API tools for that service (GitHub, Sentry, internal APIs, etc.)
|
|
72
|
+
try:
|
|
73
|
+
from gemcode.openapi_loader import load_openapi_toolsets
|
|
74
|
+
oa_tools = load_openapi_toolsets(cfg.project_root)
|
|
75
|
+
if oa_tools:
|
|
76
|
+
merged_extra_tools = list(merged_extra_tools or []) + oa_tools
|
|
77
|
+
except Exception:
|
|
78
|
+
pass # OpenAPIToolset not in this ADK version — continue without
|
|
79
|
+
|
|
59
80
|
# Computer-use: ADK ComputerUseToolset backed by our Playwright BrowserComputer.
|
|
60
81
|
# Also inject standalone browser inspection tools (screenshot, get_text, etc.)
|
|
61
82
|
# so the agent can read page state without performing side-effecting actions.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent auto-notes tool — inspired by Claude Code's MEMORY.md auto-writing.
|
|
3
|
+
|
|
4
|
+
Claude Code lets Claude write its own notes to ~/.claude/projects/<proj>/MEMORY.md.
|
|
5
|
+
GemCode provides an equivalent: the agent can write structured notes to
|
|
6
|
+
.gemcode/notes.md which are loaded back at the start of each session
|
|
7
|
+
(via the GEMINI.md hierarchy in agent.py).
|
|
8
|
+
|
|
9
|
+
This is different from the persistent memory system (memories.jsonl):
|
|
10
|
+
- memories.jsonl: semantic search-backed cross-session knowledge (requires /memory on)
|
|
11
|
+
- notes.md: a free-form markdown notes file the agent curates manually
|
|
12
|
+
— always loaded, zero-config, like a sticky notepad for project insights
|
|
13
|
+
|
|
14
|
+
The tool exposes two operations:
|
|
15
|
+
- append_project_note(note): add a bullet point or section to notes.md
|
|
16
|
+
- read_project_notes(): read current notes.md content
|
|
17
|
+
|
|
18
|
+
Usage by agent:
|
|
19
|
+
When the agent discovers something worth remembering (build command, a tricky
|
|
20
|
+
pattern, architecture insight, gotcha), it should call append_project_note to
|
|
21
|
+
persist it for future sessions. The agent instruction tells it when to do this.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _notes_path(project_root_str: str | None = None) -> Path:
|
|
33
|
+
root = Path(project_root_str) if project_root_str else Path.cwd()
|
|
34
|
+
return root / ".gemcode" / "notes.md"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_notes_tools(project_root: Path) -> list:
|
|
38
|
+
"""Return [append_project_note, read_project_notes] bound to this project."""
|
|
39
|
+
|
|
40
|
+
notes_file = project_root / ".gemcode" / "notes.md"
|
|
41
|
+
|
|
42
|
+
def append_project_note(note: str) -> dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Append a note to the project notes file (.gemcode/notes.md).
|
|
45
|
+
|
|
46
|
+
Use this to record important insights, patterns, commands, or gotchas that
|
|
47
|
+
will be useful in future sessions — like a persistent sticky notepad.
|
|
48
|
+
|
|
49
|
+
Best for:
|
|
50
|
+
- Build/test commands you discovered
|
|
51
|
+
- Architecture patterns or important abstractions
|
|
52
|
+
- Known issues or tricky edge cases
|
|
53
|
+
- User preferences or workflow conventions
|
|
54
|
+
- Important file locations or entry points
|
|
55
|
+
|
|
56
|
+
Not for:
|
|
57
|
+
- Every single thing the user says (be selective — only durable insights)
|
|
58
|
+
- Sensitive data or credentials
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
note: The note to append. Use markdown. Start with a heading or bullet.
|
|
62
|
+
Example: "- **Build**: `npm run build` (requires Node 20+)"
|
|
63
|
+
Example: "## Architecture\\nAuth is handled by src/auth/middleware.ts"
|
|
64
|
+
"""
|
|
65
|
+
if not note or not note.strip():
|
|
66
|
+
return {"error": "note cannot be empty"}
|
|
67
|
+
try:
|
|
68
|
+
notes_file.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
70
|
+
# If file doesn't exist, create with header
|
|
71
|
+
if not notes_file.exists():
|
|
72
|
+
header = f"# GemCode Agent Notes\n*Auto-generated project notes. Edit freely.*\n\n"
|
|
73
|
+
notes_file.write_text(header, encoding="utf-8")
|
|
74
|
+
current = notes_file.read_text(encoding="utf-8", errors="replace")
|
|
75
|
+
# Avoid duplicating identical notes
|
|
76
|
+
stripped = note.strip()
|
|
77
|
+
if stripped in current:
|
|
78
|
+
return {"status": "already_exists", "note": stripped}
|
|
79
|
+
entry = f"\n<!-- {timestamp} -->\n{stripped}\n"
|
|
80
|
+
notes_file.write_text(current + entry, encoding="utf-8")
|
|
81
|
+
return {"status": "appended", "notes_path": str(notes_file)}
|
|
82
|
+
except OSError as e:
|
|
83
|
+
return {"error": f"Could not write notes: {e}"}
|
|
84
|
+
|
|
85
|
+
def read_project_notes() -> dict[str, Any]:
|
|
86
|
+
"""
|
|
87
|
+
Read the current contents of .gemcode/notes.md.
|
|
88
|
+
|
|
89
|
+
Use this to check what has been previously noted about this project before
|
|
90
|
+
starting a task — avoids re-discovering things already documented.
|
|
91
|
+
|
|
92
|
+
Returns the full notes content as a string, or an empty string if no notes exist.
|
|
93
|
+
"""
|
|
94
|
+
if not notes_file.exists():
|
|
95
|
+
return {"content": "", "exists": False, "path": str(notes_file)}
|
|
96
|
+
try:
|
|
97
|
+
content = notes_file.read_text(encoding="utf-8", errors="replace")
|
|
98
|
+
return {"content": content, "exists": True, "path": str(notes_file), "chars": len(content)}
|
|
99
|
+
except OSError as e:
|
|
100
|
+
return {"error": f"Could not read notes: {e}"}
|
|
101
|
+
|
|
102
|
+
# Bind the project root to the function names so they show correctly in tool manifests
|
|
103
|
+
append_project_note.__name__ = "append_project_note"
|
|
104
|
+
read_project_notes.__name__ = "read_project_notes"
|
|
105
|
+
|
|
106
|
+
return [append_project_note, read_project_notes]
|
|
@@ -41,6 +41,9 @@ except ImportError:
|
|
|
41
41
|
# ---------------------------------------------------------------------------
|
|
42
42
|
SLASH_COMMANDS: list[tuple[str, str]] = [
|
|
43
43
|
("help", "List all available commands"),
|
|
44
|
+
("init", "Analyze project and generate GEMINI.md project instructions"),
|
|
45
|
+
("cost", "Show session token usage and estimated USD cost breakdown"),
|
|
46
|
+
("notes", "View agent auto-generated project notes (.gemcode/notes.md)"),
|
|
44
47
|
("clear", "Start a fresh session (clears history) · alias: /session new"),
|
|
45
48
|
("model", "View or switch model · /model use <id> · /model list"),
|
|
46
49
|
("mode", "Set model mode · /mode fast|balanced|quality|auto"),
|
|
@@ -27,6 +27,7 @@ src/gemcode/mcp_loader.py
|
|
|
27
27
|
src/gemcode/modality_tools.py
|
|
28
28
|
src/gemcode/model_errors.py
|
|
29
29
|
src/gemcode/model_routing.py
|
|
30
|
+
src/gemcode/openapi_loader.py
|
|
30
31
|
src/gemcode/paths.py
|
|
31
32
|
src/gemcode/permissions.py
|
|
32
33
|
src/gemcode/pricing.py
|
|
@@ -70,6 +71,7 @@ src/gemcode/tools/bash.py
|
|
|
70
71
|
src/gemcode/tools/browser.py
|
|
71
72
|
src/gemcode/tools/edit.py
|
|
72
73
|
src/gemcode/tools/filesystem.py
|
|
74
|
+
src/gemcode/tools/notes.py
|
|
73
75
|
src/gemcode/tools/search.py
|
|
74
76
|
src/gemcode/tools/shell.py
|
|
75
77
|
src/gemcode/tools/shell_gate.py
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Optional MCP toolsets from `.gemcode/mcp.json`.
|
|
3
|
-
|
|
4
|
-
Schema (example):
|
|
5
|
-
{
|
|
6
|
-
"servers": [
|
|
7
|
-
{
|
|
8
|
-
"name": "docs",
|
|
9
|
-
"stdio": { "command": "npx", "args": ["-y", "@some/mcp-server"] }
|
|
10
|
-
}
|
|
11
|
-
]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
Requires: pip install gemcode[mcp]
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import json
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
from typing import Any
|
|
22
|
-
|
|
23
|
-
from gemcode.config import GemCodeConfig
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def load_mcp_toolsets(cfg: GemCodeConfig) -> list:
|
|
27
|
-
path = cfg.project_root / ".gemcode" / "mcp.json"
|
|
28
|
-
if not path.is_file():
|
|
29
|
-
return []
|
|
30
|
-
try:
|
|
31
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
32
|
-
except json.JSONDecodeError as e:
|
|
33
|
-
raise ValueError(f"Invalid mcp.json: {e}") from e
|
|
34
|
-
|
|
35
|
-
try:
|
|
36
|
-
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
|
|
37
|
-
from mcp import StdioServerParameters
|
|
38
|
-
except ImportError as e:
|
|
39
|
-
raise ImportError("Install MCP extras: pip install gemcode[mcp]") from e
|
|
40
|
-
|
|
41
|
-
servers = data.get("servers") or []
|
|
42
|
-
toolsets: list[Any] = []
|
|
43
|
-
for s in servers:
|
|
44
|
-
stdio = s.get("stdio") or {}
|
|
45
|
-
cmd = stdio.get("command")
|
|
46
|
-
args = stdio.get("args") or []
|
|
47
|
-
if not cmd:
|
|
48
|
-
continue
|
|
49
|
-
params = StdioServerParameters(command=cmd, args=args)
|
|
50
|
-
prefix = s.get("name") or "mcp"
|
|
51
|
-
toolsets.append(
|
|
52
|
-
McpToolset(
|
|
53
|
-
connection_params=params,
|
|
54
|
-
tool_name_prefix=prefix,
|
|
55
|
-
)
|
|
56
|
-
)
|
|
57
|
-
return toolsets
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|