gemcode 0.3.82__py3-none-any.whl → 0.3.86__py3-none-any.whl
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/agent.py +42 -8
- gemcode/callbacks.py +20 -0
- gemcode/cli.py +114 -1
- gemcode/config.py +3 -1
- gemcode/ide_stdio.py +7 -1
- gemcode/plugins/terminal_hooks_plugin.py +31 -0
- gemcode/repl_commands.py +1 -0
- gemcode/repl_slash.py +80 -22
- gemcode/session_runtime.py +55 -0
- gemcode/skills.py +51 -6
- gemcode/tools/__init__.py +8 -0
- gemcode/veomem_bridge.py +134 -0
- gemcode-0.3.86.dist-info/METADATA +416 -0
- {gemcode-0.3.82.dist-info → gemcode-0.3.86.dist-info}/RECORD +18 -17
- gemcode-0.3.82.dist-info/METADATA +0 -800
- {gemcode-0.3.82.dist-info → gemcode-0.3.86.dist-info}/WHEEL +0 -0
- {gemcode-0.3.82.dist-info → gemcode-0.3.86.dist-info}/entry_points.txt +0 -0
- {gemcode-0.3.82.dist-info → gemcode-0.3.86.dist-info}/licenses/LICENSE +0 -0
- {gemcode-0.3.82.dist-info → gemcode-0.3.86.dist-info}/top_level.txt +0 -0
gemcode/agent.py
CHANGED
|
@@ -103,14 +103,15 @@ def _chain_before_model_callbacks(*callbacks):
|
|
|
103
103
|
|
|
104
104
|
def _load_gemini_md(project_root: Path) -> str:
|
|
105
105
|
"""
|
|
106
|
-
Load
|
|
106
|
+
Load project instruction markdown / .gemcode/NOTES.md from a interactive CLI–style hierarchy.
|
|
107
107
|
|
|
108
108
|
Priority (later entries override earlier ones, all are concatenated):
|
|
109
109
|
1. ~/.gemcode/GEMINI.md — user-global instructions (all projects)
|
|
110
|
-
2. Walk UP from project_root: each directory's
|
|
110
|
+
2. Walk UP from project_root: each directory's `gemcode.md` / `GEMINI.md`
|
|
111
111
|
(org-level files at higher dirs, project-level at project_root)
|
|
112
|
-
3. project_root/
|
|
113
|
-
4. project_root
|
|
112
|
+
3. project_root/gemcode.md — the primary project instructions
|
|
113
|
+
4. project_root/GEMINI.md — backward-compatible legacy location
|
|
114
|
+
5. project_root/.gemcode/GEMINI.md — alternative location
|
|
114
115
|
5. project_root/.gemcode/notes.md — agent auto-generated notes (read-only context)
|
|
115
116
|
|
|
116
117
|
Max total: 80,000 chars. Each file is capped at 30,000 chars.
|
|
@@ -118,7 +119,14 @@ def _load_gemini_md(project_root: Path) -> str:
|
|
|
118
119
|
"""
|
|
119
120
|
import re
|
|
120
121
|
|
|
121
|
-
_NAMES = (
|
|
122
|
+
_NAMES = (
|
|
123
|
+
"gemcode.md",
|
|
124
|
+
"GEMCODE.md",
|
|
125
|
+
"GEMINI.md",
|
|
126
|
+
"gemini.md",
|
|
127
|
+
".gemcode/GEMINI.md",
|
|
128
|
+
".gemcode/gemini.md",
|
|
129
|
+
)
|
|
122
130
|
_FILE_CAP = 30_000
|
|
123
131
|
_TOTAL_CAP = 80_000
|
|
124
132
|
_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
@@ -162,8 +170,15 @@ def _load_gemini_md(project_root: Path) -> str:
|
|
|
162
170
|
for name in _NAMES:
|
|
163
171
|
_add(ancestor / name)
|
|
164
172
|
|
|
165
|
-
# 3+
|
|
166
|
-
for name in (
|
|
173
|
+
# 3+5. Project-root level instructions (primary location + compatibility)
|
|
174
|
+
for name in (
|
|
175
|
+
"gemcode.md",
|
|
176
|
+
"GEMCODE.md",
|
|
177
|
+
"GEMINI.md",
|
|
178
|
+
"gemini.md",
|
|
179
|
+
".gemcode/GEMINI.md",
|
|
180
|
+
".gemcode/gemini.md",
|
|
181
|
+
):
|
|
167
182
|
_add(project_root / name)
|
|
168
183
|
|
|
169
184
|
# 5. Agent-generated notes (informational context, not instructions)
|
|
@@ -303,6 +318,25 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
303
318
|
except Exception:
|
|
304
319
|
curated_section = ""
|
|
305
320
|
|
|
321
|
+
# ── VeoMem recall (optional) ─────────────────────────────────────────────
|
|
322
|
+
veomem_section = ""
|
|
323
|
+
try:
|
|
324
|
+
t = getattr(cfg, "_veomem_wakeup_text", None)
|
|
325
|
+
if isinstance(t, str) and t.strip():
|
|
326
|
+
veomem_section = (
|
|
327
|
+
"\n\n## VeoMem recall (auto-captured, progressive)\n"
|
|
328
|
+
"This section is automatically generated from prior tool usage and summaries. "
|
|
329
|
+
"Treat it as helpful context; do not restate it verbatim to the user.\n"
|
|
330
|
+
"If you need deeper details about a specific prior observation, use the "
|
|
331
|
+
"3-step retrieval flow with tools:\n"
|
|
332
|
+
"- `veomem_search(query=...)` → get an index of relevant observation IDs\n"
|
|
333
|
+
"- `veomem_timeline(id=...)` → get compact neighbors around an anchor ID\n"
|
|
334
|
+
"- `veomem_get_observations(ids=...)` → fetch full text for selected IDs\n"
|
|
335
|
+
f"{t.strip()}\n"
|
|
336
|
+
)
|
|
337
|
+
except Exception:
|
|
338
|
+
veomem_section = ""
|
|
339
|
+
|
|
306
340
|
return f"""## Runtime facts (authoritative for this session)
|
|
307
341
|
- **Today's date:** {today}
|
|
308
342
|
- **Project root** — every filesystem tool path is relative to: `{root}`
|
|
@@ -316,7 +350,7 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
316
350
|
{kaira_section}
|
|
317
351
|
- **UI banner** phrases like "GemCode Pro" are terminal marketing, not a separate API tier.
|
|
318
352
|
- **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.
|
|
319
|
-
- **Working in subfolders** — call `list_directory(\"Desktop\")`, `glob_files(\"**/query.ts\")`, `read_file(\"testing/ai-edtech-app/src/app/page.tsx\")` directly. Never claim access is blocked unless a tool returned an explicit error.{git_section}{curated_section}"""
|
|
353
|
+
- **Working in subfolders** — call `list_directory(\"Desktop\")`, `glob_files(\"**/query.ts\")`, `read_file(\"testing/ai-edtech-app/src/app/page.tsx\")` directly. Never claim access is blocked unless a tool returned an explicit error.{git_section}{curated_section}{veomem_section}"""
|
|
320
354
|
|
|
321
355
|
|
|
322
356
|
def _build_memory_section(cfg: GemCodeConfig) -> str:
|
gemcode/callbacks.py
CHANGED
|
@@ -525,6 +525,26 @@ def make_after_tool_callback(cfg: GemCodeConfig):
|
|
|
525
525
|
except Exception:
|
|
526
526
|
pass
|
|
527
527
|
|
|
528
|
+
# ── VeoMem (optional): auto-capture tool usage ────────────────────────
|
|
529
|
+
try:
|
|
530
|
+
from gemcode.veomem_bridge import record_tool_use
|
|
531
|
+
sid = None
|
|
532
|
+
try:
|
|
533
|
+
if tool_context is not None and getattr(tool_context, "session", None) is not None:
|
|
534
|
+
sid = getattr(tool_context.session, "id", None)
|
|
535
|
+
except Exception:
|
|
536
|
+
sid = None
|
|
537
|
+
record_tool_use(
|
|
538
|
+
cfg.project_root,
|
|
539
|
+
session_id=str(sid) if sid else None,
|
|
540
|
+
tool_name=name,
|
|
541
|
+
args=args or {},
|
|
542
|
+
result=tool_response if isinstance(tool_response, dict) else {},
|
|
543
|
+
paths=(st.get(_RISK_FILES_TOUCHED, []) or []) if isinstance(st, dict) else None,
|
|
544
|
+
)
|
|
545
|
+
except Exception:
|
|
546
|
+
pass
|
|
547
|
+
|
|
528
548
|
if _maybe_tool_summary_enabled():
|
|
529
549
|
summary: dict[str, Any] = {
|
|
530
550
|
"phase": "tool_result",
|
gemcode/cli.py
CHANGED
|
@@ -7,6 +7,7 @@ import asyncio
|
|
|
7
7
|
import getpass
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
+
import re
|
|
10
11
|
import sys
|
|
11
12
|
import uuid
|
|
12
13
|
import warnings
|
|
@@ -129,18 +130,30 @@ def _initialize_gemcode_project(cfg: GemCodeConfig) -> None:
|
|
|
129
130
|
"""
|
|
130
131
|
root = cfg.project_root.resolve()
|
|
131
132
|
gem_dir = root / ".gemcode"
|
|
133
|
+
gemcode_md = root / "gemcode.md"
|
|
132
134
|
already_there = gem_dir.is_dir()
|
|
133
135
|
try:
|
|
134
136
|
gem_dir.mkdir(parents=True, exist_ok=True)
|
|
135
137
|
except OSError as e:
|
|
136
138
|
print(f"[gemcode] warning: could not create {gem_dir}: {e}", file=sys.stderr)
|
|
137
139
|
return
|
|
140
|
+
if not gemcode_md.exists():
|
|
141
|
+
try:
|
|
142
|
+
gemcode_md.write_text(
|
|
143
|
+
"# Project instructions\n\n"
|
|
144
|
+
"- Describe the project purpose here.\n"
|
|
145
|
+
"- Add build, test, and lint commands.\n"
|
|
146
|
+
"- Add architecture notes and conventions GemCode should follow.\n",
|
|
147
|
+
encoding="utf-8",
|
|
148
|
+
)
|
|
149
|
+
except OSError as e:
|
|
150
|
+
print(f"[gemcode] warning: could not create {gemcode_md}: {e}", file=sys.stderr)
|
|
138
151
|
if not already_there:
|
|
139
152
|
print(
|
|
140
153
|
"\n── GemCode · Project folder ready ──\n"
|
|
141
154
|
f" Workspace: {root}\n"
|
|
142
155
|
f" Config & session data: {gem_dir}/\n"
|
|
143
|
-
"
|
|
156
|
+
f" Project instructions: {gemcode_md.name}\n"
|
|
144
157
|
"── Ready. ──\n",
|
|
145
158
|
file=sys.stderr,
|
|
146
159
|
)
|
|
@@ -262,6 +275,100 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
262
275
|
"GemCode CLI is running. Type your prompt and press Enter. (Ctrl+D to exit)",
|
|
263
276
|
file=sys.stderr,
|
|
264
277
|
)
|
|
278
|
+
|
|
279
|
+
def _looks_like_new_skill_request(s: str) -> bool:
|
|
280
|
+
t = (s or "").strip().lower()
|
|
281
|
+
if not t or t.startswith("/"):
|
|
282
|
+
return False
|
|
283
|
+
# Natural language trigger: "make/create/build a new skill/gemskill"
|
|
284
|
+
return bool(
|
|
285
|
+
re.search(r"\b(new|create|make|build)\b", t)
|
|
286
|
+
and re.search(r"\b(gem\s*skill|gemskill|skill)\b", t)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def _prompt_nonempty(label: str, default: str | None = None) -> str:
|
|
290
|
+
while True:
|
|
291
|
+
try:
|
|
292
|
+
v = input(label).strip()
|
|
293
|
+
except EOFError:
|
|
294
|
+
return default or ""
|
|
295
|
+
if v:
|
|
296
|
+
return v
|
|
297
|
+
if default is not None:
|
|
298
|
+
return default
|
|
299
|
+
|
|
300
|
+
def _wizard_create_gemskill() -> str | None:
|
|
301
|
+
"""
|
|
302
|
+
Interactive CLI wizard that collects a GemSkill spec, then returns a single
|
|
303
|
+
model prompt instructing the agent to generate the skill folder/files.
|
|
304
|
+
"""
|
|
305
|
+
if not (hasattr(sys.stdin, "isatty") and sys.stdin.isatty()):
|
|
306
|
+
return None
|
|
307
|
+
print("\n── GemSkill wizard ──", file=sys.stderr)
|
|
308
|
+
print("We'll create a new skill under `.gemcode/skills/<name>/`.\n", file=sys.stderr)
|
|
309
|
+
name = _prompt_nonempty("skill name (kebab-case, e.g. api-review): ")
|
|
310
|
+
name = (name or "").strip().lower()
|
|
311
|
+
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", name):
|
|
312
|
+
print("Invalid name. Use lowercase letters/numbers/hyphens (max 64 chars).", file=sys.stderr)
|
|
313
|
+
return None
|
|
314
|
+
desc = _prompt_nonempty("one-line description: ")
|
|
315
|
+
when = _prompt_nonempty("when should it be used (1-2 sentences)? ")
|
|
316
|
+
inputs = _prompt_nonempty("inputs it should accept (bullets or short text): ", default="User request + $ARGUMENTS")
|
|
317
|
+
outputs = _prompt_nonempty("expected output format (short): ", default="Concise checklist + actionable steps")
|
|
318
|
+
tools_pref = _prompt_nonempty(
|
|
319
|
+
"tools: (a)uto, (r)ead-only, (w)eb-research-heavy? [a]: ",
|
|
320
|
+
default="a",
|
|
321
|
+
).strip().lower()
|
|
322
|
+
use_web = tools_pref.startswith("w")
|
|
323
|
+
read_only = tools_pref.startswith("r")
|
|
324
|
+
examples = _prompt_nonempty("example command(s) user will type (optional): ", default="")
|
|
325
|
+
|
|
326
|
+
root = cfg.project_root.resolve()
|
|
327
|
+
skill_dir = root / ".gemcode" / "skills" / name
|
|
328
|
+
skill_md = skill_dir / "SKILL.md"
|
|
329
|
+
|
|
330
|
+
prompt_parts: list[str] = [
|
|
331
|
+
"You are creating a **GemCode GemSkill**. Generate a new skill folder and files.\n\n",
|
|
332
|
+
"## Target\n",
|
|
333
|
+
f"- Project root: {root}\n",
|
|
334
|
+
f"- Skill directory: {skill_dir}\n",
|
|
335
|
+
f"- Primary file: {skill_md}\n\n",
|
|
336
|
+
"## Requirements\n",
|
|
337
|
+
f"- Skill name: {name}\n",
|
|
338
|
+
f"- Description: {desc}\n",
|
|
339
|
+
f"- When to use: {when}\n",
|
|
340
|
+
f"- Inputs: {inputs}\n",
|
|
341
|
+
f"- Output expectations: {outputs}\n",
|
|
342
|
+
]
|
|
343
|
+
if examples:
|
|
344
|
+
prompt_parts.append(f"- Example invocations: {examples}\n")
|
|
345
|
+
prompt_parts.extend(
|
|
346
|
+
[
|
|
347
|
+
"\n",
|
|
348
|
+
"- Write `SKILL.md` with YAML frontmatter using **multiline-friendly** fields when needed.\n",
|
|
349
|
+
"- Include: Purpose, When to use, When NOT to use (guardrails), Inputs, Output format, Workflow, Examples.\n",
|
|
350
|
+
"- Make it **token-efficient**: prefer short checklists and explicit decision gates.\n",
|
|
351
|
+
"- Avoid vague 'ALWAYS trigger' language; provide precise triggers.\n",
|
|
352
|
+
"- If you need templates/checklists, create supporting files in a `references/` subfolder and keep them small.\n",
|
|
353
|
+
"- Do not create CLAUDE.md/AGENTS.md or other vendor-specific files.\n\n",
|
|
354
|
+
"## Tooling / research policy\n",
|
|
355
|
+
(
|
|
356
|
+
"- You MAY use web research to find best practices, but only if it materially improves the skill.\n"
|
|
357
|
+
if use_web
|
|
358
|
+
else "- Avoid web research unless strictly necessary.\n"
|
|
359
|
+
),
|
|
360
|
+
("- Operate in read-only mode: do not write files.\n" if read_only else "- You are allowed to write the skill files.\n"),
|
|
361
|
+
"\n",
|
|
362
|
+
"## Execution steps\n",
|
|
363
|
+
"1. Create the skill directory if missing.\n",
|
|
364
|
+
"2. Write `SKILL.md` (and any supporting files).\n",
|
|
365
|
+
"3. Validate the YAML frontmatter parses and the skill is usable.\n",
|
|
366
|
+
f"4. Print a short confirmation: created files + how to invoke (e.g. `/skills list`, `/{name} ...`, `/gemskill {name}`).\n",
|
|
367
|
+
]
|
|
368
|
+
)
|
|
369
|
+
prompt = "".join(prompt_parts)
|
|
370
|
+
return prompt
|
|
371
|
+
|
|
265
372
|
while True:
|
|
266
373
|
try:
|
|
267
374
|
raw = input("> ")
|
|
@@ -274,6 +381,12 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
274
381
|
if prompt_text in (":q", "quit", "exit", "/exit"):
|
|
275
382
|
break
|
|
276
383
|
|
|
384
|
+
# Natural language shortcut: "I want to make a new skill" => wizard.
|
|
385
|
+
if _looks_like_new_skill_request(prompt_text):
|
|
386
|
+
wizard_prompt = _wizard_create_gemskill()
|
|
387
|
+
if wizard_prompt:
|
|
388
|
+
prompt_text = wizard_prompt
|
|
389
|
+
|
|
277
390
|
cfg.session_skill_expand_session_id = session_id
|
|
278
391
|
slash = await process_repl_slash(
|
|
279
392
|
cfg=cfg,
|
gemcode/config.py
CHANGED
|
@@ -232,7 +232,9 @@ class GemCodeConfig:
|
|
|
232
232
|
# Deep research model id used when routing selects deep research.
|
|
233
233
|
model_deep_research: str = field(
|
|
234
234
|
default_factory=lambda: os.environ.get(
|
|
235
|
-
|
|
235
|
+
# Leave empty by default so routing falls back to `cfg.model` unless the
|
|
236
|
+
# user explicitly configures a deep-research-capable model id.
|
|
237
|
+
"GEMCODE_MODEL_DEEP_RESEARCH", ""
|
|
236
238
|
)
|
|
237
239
|
)
|
|
238
240
|
|
gemcode/ide_stdio.py
CHANGED
|
@@ -322,7 +322,13 @@ async def run_stdio_loop() -> int:
|
|
|
322
322
|
continue
|
|
323
323
|
out_text = "".join(txt_parts).strip()
|
|
324
324
|
if out_text:
|
|
325
|
-
|
|
325
|
+
# Pseudo-streaming: emit deltas so IDE chat can feel alive even though
|
|
326
|
+
# we only have the final assistant text today.
|
|
327
|
+
emitter.send(make_event(event="text_start", id=req_id))
|
|
328
|
+
chunk_size = 450
|
|
329
|
+
for i in range(0, len(out_text), chunk_size):
|
|
330
|
+
emitter.send(make_event(event="text_delta", id=req_id, text=out_text[i : i + chunk_size]))
|
|
331
|
+
emitter.send(make_event(event="text_end", id=req_id))
|
|
326
332
|
emitter.send(make_response(id=req_id, ok=True, session=session_id))
|
|
327
333
|
|
|
328
334
|
finally:
|
|
@@ -189,5 +189,36 @@ class GemCodeTerminalHooksPlugin(BasePlugin):
|
|
|
189
189
|
{"phase": "post_turn_hook", "ok": False, "error": str(e)},
|
|
190
190
|
)
|
|
191
191
|
|
|
192
|
+
# ── VeoMem (optional): store final assistant summary ───────────────────
|
|
193
|
+
try:
|
|
194
|
+
# Best-effort: extract last assistant text from session events.
|
|
195
|
+
txt = ""
|
|
196
|
+
try:
|
|
197
|
+
events = callback_context.session.events or []
|
|
198
|
+
for ev in reversed(events):
|
|
199
|
+
if getattr(ev, "author", None) == getattr(agent, "name", "gemcode"):
|
|
200
|
+
content = getattr(ev, "content", None)
|
|
201
|
+
parts = getattr(content, "parts", None) if content is not None else None
|
|
202
|
+
if parts:
|
|
203
|
+
out_parts = []
|
|
204
|
+
for p in parts:
|
|
205
|
+
t = getattr(p, "text", None)
|
|
206
|
+
if t:
|
|
207
|
+
out_parts.append(t)
|
|
208
|
+
if out_parts:
|
|
209
|
+
txt = "".join(out_parts).strip()
|
|
210
|
+
break
|
|
211
|
+
except Exception:
|
|
212
|
+
txt = ""
|
|
213
|
+
if txt:
|
|
214
|
+
from gemcode.veomem_bridge import record_turn_summary
|
|
215
|
+
record_turn_summary(
|
|
216
|
+
self.cfg.project_root,
|
|
217
|
+
session_id=str(callback_context.session.id),
|
|
218
|
+
text=txt,
|
|
219
|
+
)
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
|
|
192
223
|
return None
|
|
193
224
|
|
gemcode/repl_commands.py
CHANGED
|
@@ -337,6 +337,7 @@ def slash_help_lines() -> list[str]:
|
|
|
337
337
|
" /notes clear Delete all notes",
|
|
338
338
|
" /notes edit Open notes in $EDITOR",
|
|
339
339
|
" /create gemskill <name> [description] Create a new GemSkill (SKILL.md scaffold)",
|
|
340
|
+
" Tip: you can also type “I want to make a new skill” and follow the wizard",
|
|
340
341
|
" /gemskill <name> Load an existing GemSkill into this session (system prompt)",
|
|
341
342
|
" /gemskill list|clear List skills or unload all session-loaded skills",
|
|
342
343
|
" /append gemskill <name> <request> Ask the agent to edit that skill file",
|
gemcode/repl_slash.py
CHANGED
|
@@ -8,8 +8,10 @@ Returns ``None`` when the line is not a slash command; otherwise a
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
|
+
import shlex
|
|
11
12
|
import uuid
|
|
12
13
|
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
13
15
|
from typing import Any, Callable, Iterable
|
|
14
16
|
|
|
15
17
|
from gemcode.config import GemCodeConfig
|
|
@@ -98,11 +100,26 @@ async def process_repl_slash(
|
|
|
98
100
|
out(" /attach clear Clear the queue")
|
|
99
101
|
out("Aliases: /file, /image, /img — same queue.")
|
|
100
102
|
out("Types: Gemini-supported MIME (e.g. images, PDF, audio, video, text). Default max ~20 MiB each.")
|
|
103
|
+
out("Inline prompt: /image <path> <prompt...> (or use `--` before the prompt)")
|
|
101
104
|
out("CLI: gemcode -C . --attach ./doc.pdf \"Summarize this\"")
|
|
102
105
|
out()
|
|
103
106
|
return ReplSlashResult(skip_model_turn=True)
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
|
|
108
|
+
# Parse args as:
|
|
109
|
+
# - list|clear (no further args)
|
|
110
|
+
# - <path> [--] <optional prompt...>
|
|
111
|
+
#
|
|
112
|
+
# We support quoted paths and best-effort unquoted paths with spaces by
|
|
113
|
+
# scanning for the longest prefix that resolves to an existing file.
|
|
114
|
+
try:
|
|
115
|
+
tokens = shlex.split(raw_i, posix=True)
|
|
116
|
+
except ValueError:
|
|
117
|
+
tokens = raw_i.split()
|
|
118
|
+
|
|
119
|
+
if not tokens:
|
|
120
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
121
|
+
|
|
122
|
+
if len(tokens) == 1 and tokens[0].strip().lower() == "list":
|
|
106
123
|
pend = cfg.pending_attachment_paths
|
|
107
124
|
if not pend:
|
|
108
125
|
out("(no attachments queued)")
|
|
@@ -112,15 +129,43 @@ async def process_repl_slash(
|
|
|
112
129
|
out(f" {i}. {p}")
|
|
113
130
|
out()
|
|
114
131
|
return ReplSlashResult(skip_model_turn=True)
|
|
115
|
-
if
|
|
132
|
+
if len(tokens) == 1 and tokens[0].strip().lower() == "clear":
|
|
116
133
|
cfg.pending_attachment_paths.clear()
|
|
117
134
|
out("Attachment queue cleared.")
|
|
118
135
|
out()
|
|
119
136
|
return ReplSlashResult(skip_model_turn=True)
|
|
120
|
-
|
|
137
|
+
|
|
138
|
+
# Find the longest token prefix that resolves to a real file.
|
|
139
|
+
best_i: int | None = None
|
|
140
|
+
best_resolved: Path | None = None
|
|
141
|
+
for i in range(1, len(tokens) + 1):
|
|
142
|
+
cand = " ".join(tokens[:i]).strip()
|
|
143
|
+
if not cand:
|
|
144
|
+
continue
|
|
145
|
+
resolved_try = resolve_attachment_path(cand, project_root=cfg.project_root)
|
|
146
|
+
if resolved_try.is_file():
|
|
147
|
+
best_i = i
|
|
148
|
+
best_resolved = resolved_try
|
|
149
|
+
|
|
150
|
+
# Fallback: treat first token as the path (keeps old behavior).
|
|
151
|
+
if best_i is None or best_resolved is None:
|
|
152
|
+
path_raw = tokens[0]
|
|
153
|
+
resolved = resolve_attachment_path(path_raw, project_root=cfg.project_root)
|
|
154
|
+
remainder_tokens = tokens[1:]
|
|
155
|
+
else:
|
|
156
|
+
path_raw = " ".join(tokens[:best_i]).strip()
|
|
157
|
+
resolved = best_resolved
|
|
158
|
+
remainder_tokens = tokens[best_i:]
|
|
159
|
+
|
|
160
|
+
if remainder_tokens and remainder_tokens[0] == "--":
|
|
161
|
+
remainder_tokens = remainder_tokens[1:]
|
|
162
|
+
trailing_prompt = " ".join(remainder_tokens).strip()
|
|
163
|
+
|
|
121
164
|
if not resolved.is_file():
|
|
122
|
-
out(f"Not a file: {
|
|
165
|
+
out(f"Not a file: {path_raw}")
|
|
123
166
|
out("(Resolved relative to cwd, then project root.)")
|
|
167
|
+
if trailing_prompt:
|
|
168
|
+
out("Tip: quote paths with spaces, e.g. /image \"./My File.png\" analyze this")
|
|
124
169
|
out()
|
|
125
170
|
return ReplSlashResult(skip_model_turn=True)
|
|
126
171
|
if len(cfg.pending_attachment_paths) >= 16:
|
|
@@ -128,6 +173,11 @@ async def process_repl_slash(
|
|
|
128
173
|
out()
|
|
129
174
|
return ReplSlashResult(skip_model_turn=True)
|
|
130
175
|
cfg.pending_attachment_paths.append(resolved)
|
|
176
|
+
if trailing_prompt:
|
|
177
|
+
out(f"Queued: {resolved} (attaching now)")
|
|
178
|
+
out()
|
|
179
|
+
return ReplSlashResult(model_prompt=trailing_prompt)
|
|
180
|
+
|
|
131
181
|
out(f"Queued: {resolved}")
|
|
132
182
|
out(f" ({len(cfg.pending_attachment_paths)} file(s) — send your next message to attach)")
|
|
133
183
|
out()
|
|
@@ -160,14 +210,22 @@ async def process_repl_slash(
|
|
|
160
210
|
out("Tip: /skills list")
|
|
161
211
|
out()
|
|
162
212
|
return ReplSlashResult(skip_model_turn=True)
|
|
163
|
-
|
|
213
|
+
# Token-efficient one-shot invocation: do NOT inline the full SKILL.md.
|
|
214
|
+
# Instead, point the model at the skill and require it to load/read it as needed.
|
|
164
215
|
files = list_supporting_files(s)
|
|
165
|
-
|
|
166
|
-
f"
|
|
167
|
-
f"##
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
216
|
+
prompt_parts = [
|
|
217
|
+
f"User invoked GemSkill `/{s.meta.name}`.\n\n",
|
|
218
|
+
f"## Arguments\n{sk_args or '(none)'}\n\n",
|
|
219
|
+
"## Instructions\n",
|
|
220
|
+
"1. Load the skill instructions using the `load_skill` tool (preferred) or by reading the SKILL.md file.\n",
|
|
221
|
+
"2. Only read supporting files if needed (keep it efficient).\n",
|
|
222
|
+
"3. Then carry out the user's request.\n\n",
|
|
223
|
+
f"## Skill file\n{s.skill_md}\n\n",
|
|
224
|
+
]
|
|
225
|
+
if files:
|
|
226
|
+
prompt_parts.append(f"## Supporting files (optional)\n{', '.join(files)}\n\n")
|
|
227
|
+
prompt_parts.append("Now proceed.")
|
|
228
|
+
prompt = "".join(prompt_parts)
|
|
171
229
|
return ReplSlashResult(model_prompt=prompt)
|
|
172
230
|
|
|
173
231
|
# ── /gemskill (load full skill into session system prompt) ────────────────
|
|
@@ -1114,15 +1172,15 @@ async def process_repl_slash(
|
|
|
1114
1172
|
return ReplSlashResult(skip_model_turn=True)
|
|
1115
1173
|
|
|
1116
1174
|
if name == "init":
|
|
1117
|
-
|
|
1118
|
-
if
|
|
1119
|
-
out(f"
|
|
1175
|
+
gemcode_md = cfg.project_root / "gemcode.md"
|
|
1176
|
+
if gemcode_md.exists() and (sc.args or "").strip().lower() not in ("force", "overwrite", "-f"):
|
|
1177
|
+
out(f"gemcode.md already exists at {gemcode_md}.")
|
|
1120
1178
|
out("Use /init force to regenerate it, or edit it manually.")
|
|
1121
1179
|
out()
|
|
1122
1180
|
return ReplSlashResult(skip_model_turn=True)
|
|
1123
|
-
# Dispatch to the model to analyze the project and write
|
|
1181
|
+
# Dispatch to the model to analyze the project and write gemcode.md.
|
|
1124
1182
|
init_prompt = (
|
|
1125
|
-
"Analyze this codebase and generate a
|
|
1183
|
+
"Analyze this codebase and generate a gemcode.md file for me.\n\n"
|
|
1126
1184
|
"To do this:\n"
|
|
1127
1185
|
"1. Run `list_directory('.')` to understand the project structure\n"
|
|
1128
1186
|
"2. Read `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, `README.md` "
|
|
@@ -1130,9 +1188,9 @@ async def process_repl_slash(
|
|
|
1130
1188
|
"3. Look at the source directory structure (src/, lib/, app/, etc.)\n"
|
|
1131
1189
|
"4. Check for test directories and test runner config\n"
|
|
1132
1190
|
"5. Look for linting/formatting config files (.eslintrc, .prettierrc, ruff.toml, etc.)\n\n"
|
|
1133
|
-
"Write **only** to `
|
|
1191
|
+
"Write **only** to `gemcode.md` at the project root. Do **not** create "
|
|
1134
1192
|
"`CLAUDE.md`, `AGENTS.md`, `.cursorrules`, or similar.\n\n"
|
|
1135
|
-
"Then write a
|
|
1193
|
+
"Then write a gemcode.md file at the project root containing:\n"
|
|
1136
1194
|
"# Project Name\n"
|
|
1137
1195
|
"One-sentence description.\n\n"
|
|
1138
1196
|
"## Build & Test\n"
|
|
@@ -1150,10 +1208,10 @@ async def process_repl_slash(
|
|
|
1150
1208
|
"## Workflow\n"
|
|
1151
1209
|
"- Any git branching rules from README or CONTRIBUTING\n"
|
|
1152
1210
|
"- PR/commit conventions\n\n"
|
|
1153
|
-
"Keep it under 200 lines. Write the file to
|
|
1211
|
+
"Keep it under 200 lines. Write the file to gemcode.md now."
|
|
1154
1212
|
)
|
|
1155
|
-
out("Analyzing project to generate
|
|
1156
|
-
out("(GemCode will read the project structure and write a starting
|
|
1213
|
+
out("Analyzing project to generate gemcode.md…")
|
|
1214
|
+
out("(GemCode will read the project structure and write a starting gemcode.md)")
|
|
1157
1215
|
out()
|
|
1158
1216
|
return ReplSlashResult(model_prompt=init_prompt)
|
|
1159
1217
|
|
gemcode/session_runtime.py
CHANGED
|
@@ -368,6 +368,21 @@ def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None) -> Runner
|
|
|
368
368
|
except Exception:
|
|
369
369
|
pass
|
|
370
370
|
|
|
371
|
+
# VeoMem wake-up snapshot (optional): load once per session and keep stable.
|
|
372
|
+
try:
|
|
373
|
+
import os
|
|
374
|
+
if os.environ.get("GEMCODE_VEOMEM", "").strip().lower() in ("1", "true", "yes", "on"):
|
|
375
|
+
from veomem.wakeup import build_wake_up_context # type: ignore[import-not-found]
|
|
376
|
+
# Ensure store exists; build_wake_up_context can work even if empty.
|
|
377
|
+
from veomem.store import init_store # type: ignore[import-not-found]
|
|
378
|
+
|
|
379
|
+
init_store(cfg.project_root)
|
|
380
|
+
snap2 = build_wake_up_context(cfg.project_root, max_chars=5000, limit=30)
|
|
381
|
+
txt = (snap2.get("text") if isinstance(snap2, dict) else "") or ""
|
|
382
|
+
object.__setattr__(cfg, "_veomem_wakeup_text", txt.strip() if isinstance(txt, str) else "")
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
|
|
371
386
|
# ── MCP toolsets from .gemcode/mcp.json ─────────────────────────────────
|
|
372
387
|
# Supports stdio, http (Streamable HTTP), and sse connection types.
|
|
373
388
|
try:
|
|
@@ -389,6 +404,46 @@ def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None) -> Runner
|
|
|
389
404
|
except Exception:
|
|
390
405
|
pass # OpenAPIToolset not in this ADK version — continue without
|
|
391
406
|
|
|
407
|
+
# ── AFC compatibility prompt (optional) ───────────────────────────────────
|
|
408
|
+
# Gemini Automatic Function Calling (AFC) only works when the tool list is
|
|
409
|
+
# composed of Python callables. Some ADK toolsets (MCP/OpenAPI/built-in
|
|
410
|
+
# declarations) may be non-callable and cause AFC to be disabled.
|
|
411
|
+
#
|
|
412
|
+
# If we're in an interactive terminal and the user opted into prompting,
|
|
413
|
+
# ask whether to keep "all tools" (AFC disabled) or restrict to callables
|
|
414
|
+
# (AFC enabled).
|
|
415
|
+
try:
|
|
416
|
+
import os
|
|
417
|
+
import sys
|
|
418
|
+
|
|
419
|
+
prompt_afc = os.environ.get("GEMCODE_AFC_PROMPT", "1").strip().lower() in ("1", "true", "yes", "on")
|
|
420
|
+
if prompt_afc and hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
|
|
421
|
+
tools_list = list(merged_extra_tools or [])
|
|
422
|
+
noncallable = [t for t in tools_list if not callable(t)]
|
|
423
|
+
if noncallable and getattr(cfg, "_afc_choice", None) not in ("all", "callables"):
|
|
424
|
+
print(
|
|
425
|
+
"\n[gemcode] AFC compatibility\n"
|
|
426
|
+
"Gemini Automatic Function Calling (AFC) works best with Python callables only.\n"
|
|
427
|
+
"Some enabled toolsets appear non-callable, which can disable AFC.\n\n"
|
|
428
|
+
"Choose tool mode for this session:\n"
|
|
429
|
+
" [Enter] all tools (recommended; AFC may be disabled)\n"
|
|
430
|
+
" c callable-only tools (keeps AFC; disables MCP/OpenAPI/toolsets)\n",
|
|
431
|
+
file=sys.stderr,
|
|
432
|
+
)
|
|
433
|
+
try:
|
|
434
|
+
ans = input("afc> ").strip().lower()
|
|
435
|
+
except EOFError:
|
|
436
|
+
ans = ""
|
|
437
|
+
if ans.startswith("c"):
|
|
438
|
+
object.__setattr__(cfg, "_afc_choice", "callables")
|
|
439
|
+
else:
|
|
440
|
+
object.__setattr__(cfg, "_afc_choice", "all")
|
|
441
|
+
|
|
442
|
+
if getattr(cfg, "_afc_choice", None) == "callables":
|
|
443
|
+
merged_extra_tools = [t for t in tools_list if callable(t)] or None
|
|
444
|
+
except Exception:
|
|
445
|
+
pass
|
|
446
|
+
|
|
392
447
|
# Computer-use: ADK ComputerUseToolset backed by our Playwright BrowserComputer.
|
|
393
448
|
# Probe Playwright BEFORE building the agent so model routing (which runs
|
|
394
449
|
# inside build_root_agent → pick_effective_model) never switches to
|
gemcode/skills.py
CHANGED
|
@@ -135,11 +135,16 @@ def _parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
|
|
|
135
135
|
|
|
136
136
|
Supported:
|
|
137
137
|
- key: value (single-line)
|
|
138
|
+
- key: >
|
|
139
|
+
multi-line
|
|
140
|
+
(folded; newlines become spaces)
|
|
141
|
+
- key: |
|
|
142
|
+
multi-line
|
|
143
|
+
(literal; newlines preserved)
|
|
138
144
|
- key: [a, b] (parsed as a string; caller can split if needed)
|
|
139
145
|
|
|
140
146
|
Not supported:
|
|
141
147
|
- nested objects
|
|
142
|
-
- multi-line scalars
|
|
143
148
|
- full YAML spec
|
|
144
149
|
"""
|
|
145
150
|
m = _FRONTMATTER_RE.match(text or "")
|
|
@@ -148,18 +153,58 @@ def _parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
|
|
|
148
153
|
raw = m.group(1)
|
|
149
154
|
body = (text or "")[m.end() :]
|
|
150
155
|
d: dict[str, str] = {}
|
|
151
|
-
|
|
156
|
+
lines = raw.splitlines()
|
|
157
|
+
i = 0
|
|
158
|
+
while i < len(lines):
|
|
159
|
+
line = lines[i]
|
|
152
160
|
if not line.strip() or line.strip().startswith("#"):
|
|
161
|
+
i += 1
|
|
153
162
|
continue
|
|
154
163
|
if ":" not in line:
|
|
164
|
+
i += 1
|
|
155
165
|
continue
|
|
156
166
|
k, v = line.split(":", 1)
|
|
157
167
|
k = k.strip()
|
|
158
|
-
v = v.
|
|
168
|
+
v = v.rstrip()
|
|
169
|
+
v_stripped = v.strip()
|
|
170
|
+
|
|
171
|
+
# Multi-line scalar blocks: `key: >` or `key: |` followed by indented lines.
|
|
172
|
+
if v_stripped in (">", "|"):
|
|
173
|
+
style = v_stripped
|
|
174
|
+
block: list[str] = []
|
|
175
|
+
i += 1
|
|
176
|
+
while i < len(lines):
|
|
177
|
+
nxt = lines[i]
|
|
178
|
+
# YAML block scalars require indentation. We accept 2+ spaces or a tab.
|
|
179
|
+
if not (nxt.startswith(" ") or nxt.startswith("\t")):
|
|
180
|
+
break
|
|
181
|
+
block.append(nxt.lstrip(" \t"))
|
|
182
|
+
i += 1
|
|
183
|
+
if style == ">":
|
|
184
|
+
# folded: join non-empty lines with spaces; keep paragraph breaks as newline.
|
|
185
|
+
paras: list[str] = []
|
|
186
|
+
cur: list[str] = []
|
|
187
|
+
for ln in block:
|
|
188
|
+
if not ln.strip():
|
|
189
|
+
if cur:
|
|
190
|
+
paras.append(" ".join(x.strip() for x in cur if x.strip()))
|
|
191
|
+
cur = []
|
|
192
|
+
continue
|
|
193
|
+
cur.append(ln)
|
|
194
|
+
if cur:
|
|
195
|
+
paras.append(" ".join(x.strip() for x in cur if x.strip()))
|
|
196
|
+
d[k.lower()] = "\n".join(paras).strip()
|
|
197
|
+
else:
|
|
198
|
+
d[k.lower()] = "\n".join(block).rstrip()
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
# Single-line scalar
|
|
202
|
+
v2 = v_stripped
|
|
159
203
|
# Strip simple surrounding quotes
|
|
160
|
-
if len(
|
|
161
|
-
|
|
162
|
-
d[k.lower()] =
|
|
204
|
+
if len(v2) >= 2 and ((v2[0] == v2[-1] == '"') or (v2[0] == v2[-1] == "'")):
|
|
205
|
+
v2 = v2[1:-1]
|
|
206
|
+
d[k.lower()] = v2
|
|
207
|
+
i += 1
|
|
163
208
|
return d, body
|
|
164
209
|
|
|
165
210
|
|