gemcode 0.3.80__py3-none-any.whl → 0.3.82__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 CHANGED
@@ -77,7 +77,8 @@ def build_global_instruction() -> str:
77
77
  "Think deeply about what the person actually wants before you do anything. "
78
78
  "Use exactly as many tools as the task genuinely requires — no more. "
79
79
  "Act fully and autonomously when action is needed. "
80
- "Always use read-only tools before shell or write tools."
80
+ "Always use read-only tools before shell or write tools. "
81
+ "Never create CLAUDE.md or AGENTS.md; use GEMINI.md for project instructions."
81
82
  )
82
83
 
83
84
 
@@ -580,6 +581,10 @@ You have native deep thinking capability — use it actively:
580
581
  Keep tool usage minimal. Prefer short, targeted calls and keep tool outputs small.
581
582
  If you need more tool usage examples, set `GEMCODE_VERBOSE_INSTRUCTIONS=1`.
582
583
 
584
+ ## Instruction files (GemCode — always follow)
585
+ - **Do not** create or modify `CLAUDE.md`, `AGENTS.md`, `claude.local.md`, `agents.local.md`, or `.cursorrules` unless the user **explicitly** asks for that exact filename. Those are for other assistants; GemCode reads **`GEMINI.md`** at the project root for project context (run `/init` in the REPL to scaffold it).
586
+ - If you need to capture project conventions, edit **`GEMINI.md`** or append to **`.gemcode/notes.md`** via the notes tools — not vendor-specific instruction filenames.
587
+
583
588
  """
584
589
 
585
590
  if not verbose_tools_guide:
@@ -895,10 +900,6 @@ You have two tools to persist project insights across sessions (auto-memory styl
895
900
  Notes are loaded at session start so future sessions inherit this knowledge.
896
901
 
897
902
  - **`read_project_notes()`** — read current notes **only when starting a real engineering task** (editing, debugging, building). Do NOT call this for greetings or general questions. If notes exist and you're about to work on a task, read them once to avoid re-discovering known information.
898
-
899
- ## Do not create vendor-specific instruction files
900
- - Do NOT create or modify `CLAUDE.md` or `AGENTS.md`. GemCode does not use these.
901
- - If project instructions are needed and the user asked for it, use `GEMINI.md` (repo root).
902
903
  """
903
904
 
904
905
  # Inject capability-specific strategy sections only when those caps are on.
gemcode/curated_memory.py CHANGED
@@ -22,6 +22,8 @@ from datetime import datetime
22
22
  from pathlib import Path
23
23
  from typing import Any
24
24
 
25
+ from gemcode.wal import append_wal_event, wal_text_fingerprint
26
+
25
27
 
26
28
  _SUSPICIOUS = [
27
29
  "api_key",
@@ -106,5 +108,15 @@ def append_fact(project_root: Path, *, target: str, text: str) -> dict[str, Any]
106
108
  ts = datetime.now().strftime("%Y-%m-%d %H:%M")
107
109
  entry = f"\n<!-- {ts} -->\n- {stripped}\n"
108
110
  p.write_text(cur + entry, encoding="utf-8")
111
+ # Best-effort WAL: do not store raw text.
112
+ append_wal_event(
113
+ project_root,
114
+ type="curated_memory.append_fact",
115
+ data={
116
+ "target": (target or "").strip().lower() or "memory",
117
+ "path": str(p),
118
+ "fingerprint": wal_text_fingerprint(stripped),
119
+ },
120
+ )
109
121
  return {"status": "appended", "path": str(p)}
110
122
 
gemcode/modality_tools.py CHANGED
@@ -13,6 +13,7 @@ from pathlib import Path
13
13
  from typing import Any
14
14
 
15
15
  from gemcode.config import GemCodeConfig
16
+ from gemcode.query_sanitizer import sanitize_tool_query
16
17
 
17
18
 
18
19
  def _get_embedding_client():
@@ -76,6 +77,10 @@ async def semantic_search_files(
76
77
  """
77
78
  if not isinstance(query, str) or not query.strip():
78
79
  return {"error": "query must be a non-empty string"}
80
+ s = sanitize_tool_query(query)
81
+ query = str(s.get("clean_query") or "").strip()
82
+ if not query:
83
+ return {"error": "query must be a non-empty string"}
79
84
 
80
85
  root = Path(project_root).resolve() if project_root else None
81
86
  if root is None:
@@ -165,7 +170,13 @@ async def semantic_search_files(
165
170
  snippet = chunks[idx][:500].replace("\n", " ")
166
171
  matches.append({"path": rel, "snippet": snippet, "score": score})
167
172
 
168
- return {"query": query, "backend": "embeddings", "matches": matches}
173
+ return {
174
+ "query": query,
175
+ "query_sanitized": bool(s.get("was_sanitized")),
176
+ "query_sanitizer_method": str(s.get("method")),
177
+ "backend": "embeddings",
178
+ "matches": matches,
179
+ }
169
180
 
170
181
 
171
182
  def build_extra_tools(cfg: GemCodeConfig) -> list[Any]:
gemcode/output_styles.py CHANGED
@@ -16,11 +16,18 @@ def _is_valid_name(name: str) -> bool:
16
16
  return bool(re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", name or ""))
17
17
 
18
18
 
19
+ def _builtin_style_dir() -> Path:
20
+ # Built-in styles shipped with GemCode (lowest priority).
21
+ # Located under the python package so they work out-of-the-box.
22
+ return Path(__file__).resolve().parent / "builtin" / "output-styles"
23
+
24
+
19
25
  def _style_dirs_for_project(project_root: Path) -> list[Path]:
20
- # Project has priority over personal.
26
+ # Priority (highest last): project > personal > built-in.
21
27
  return [
22
28
  project_root / ".gemcode" / "output-styles",
23
29
  Path.home() / ".gemcode" / "output-styles",
30
+ _builtin_style_dir(),
24
31
  ]
25
32
 
26
33
 
@@ -0,0 +1,87 @@
1
+ """
2
+ Mitigate "system prompt contamination" in tool queries.
3
+
4
+ Inspired by MemPalace's query sanitizer. Goal: prevent catastrophic search degradation
5
+ when an agent accidentally prefixes a long system instruction to a short query.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+ MAX_QUERY_LENGTH = 250
13
+ SAFE_QUERY_LENGTH = 200
14
+ MIN_QUERY_LENGTH = 10
15
+
16
+ _SENTENCE_SPLIT = re.compile(r"[.!?。!?\n]+")
17
+ _QUESTION_MARK = re.compile(r'[??]\s*["\']?\s*$')
18
+ _QUOTE_CHARS = {"'", '"'}
19
+
20
+
21
+ def sanitize_tool_query(raw_query: str) -> dict[str, object]:
22
+ """
23
+ Return a best-effort clean query.
24
+
25
+ Returns dict:
26
+ clean_query: str
27
+ was_sanitized: bool
28
+ method: str
29
+ """
30
+ if not raw_query or not str(raw_query).strip():
31
+ return {"clean_query": "", "was_sanitized": False, "method": "empty"}
32
+
33
+ raw_query = str(raw_query).strip()
34
+ n = len(raw_query)
35
+
36
+ def _strip_wrapping_quotes(s: str) -> str:
37
+ s = (s or "").strip()
38
+ while len(s) >= 2 and s[:1] in _QUOTE_CHARS and s[:1] == s[-1:]:
39
+ s = s[1:-1].strip()
40
+ if not s:
41
+ return ""
42
+ if s[:1] in _QUOTE_CHARS:
43
+ s = s[1:].strip()
44
+ if s[-1:] in _QUOTE_CHARS:
45
+ s = s[:-1].strip()
46
+ return s
47
+
48
+ def _trim_candidate(s: str) -> str:
49
+ s = _strip_wrapping_quotes(s)
50
+ if len(s) <= MAX_QUERY_LENGTH:
51
+ return s
52
+ frags = [_strip_wrapping_quotes(x) for x in _SENTENCE_SPLIT.split(s) if x.strip()]
53
+ for frag in reversed(frags):
54
+ if MIN_QUERY_LENGTH <= len(frag) <= MAX_QUERY_LENGTH:
55
+ return frag
56
+ return s[-MAX_QUERY_LENGTH:].strip()
57
+
58
+ if n <= SAFE_QUERY_LENGTH:
59
+ return {"clean_query": raw_query, "was_sanitized": False, "method": "passthrough"}
60
+
61
+ # Prefer last question-looking segment.
62
+ segments = [s.strip() for s in raw_query.split("\n") if s.strip()]
63
+ question_candidates: list[str] = []
64
+ for seg in reversed(segments):
65
+ if _QUESTION_MARK.search(seg):
66
+ question_candidates.append(seg)
67
+ if not question_candidates:
68
+ sentences = [s.strip() for s in _SENTENCE_SPLIT.split(raw_query) if s.strip()]
69
+ for sent in reversed(sentences):
70
+ if "?" in sent or "?" in sent:
71
+ question_candidates.append(sent)
72
+
73
+ if question_candidates:
74
+ cand = _trim_candidate(question_candidates[0])
75
+ if len(cand) >= MIN_QUERY_LENGTH:
76
+ return {"clean_query": cand, "was_sanitized": True, "method": "question_extraction"}
77
+
78
+ # Otherwise take the last meaningful segment.
79
+ for seg in reversed(segments):
80
+ cand = _trim_candidate(seg)
81
+ if len(cand) >= MIN_QUERY_LENGTH:
82
+ return {"clean_query": cand, "was_sanitized": True, "method": "tail_sentence"}
83
+
84
+ # Fallback: tail truncation.
85
+ cand = raw_query[-MAX_QUERY_LENGTH:].strip()
86
+ return {"clean_query": cand, "was_sanitized": True, "method": "tail_truncation"}
87
+
gemcode/repl_commands.py CHANGED
@@ -208,6 +208,8 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
208
208
  ("audit", "Tail audit.log · /logs same"),
209
209
  ("autotune", "Branch + eval ledger · /autotune init <tag> · /autotune eval"),
210
210
  ("batch", "Built-in batch GemSkill (large parallel changes)"),
211
+ ("caveman", "Terse output mode · /caveman lite|full|ultra|wenyan|off"),
212
+ ("caveman:compress", "Compress memory file · /caveman:compress <path> [lite|full|ultra]"),
211
213
  ("budget", "Per-turn token budget · /token-budget same"),
212
214
  ("caps", "Capabilities · /capabilities /capability same"),
213
215
  ("clear", "Fresh session · same as /session new"),
@@ -338,6 +340,7 @@ def slash_help_lines() -> list[str]:
338
340
  " /gemskill <name> Load an existing GemSkill into this session (system prompt)",
339
341
  " /gemskill list|clear List skills or unload all session-loaded skills",
340
342
  " /append gemskill <name> <request> Ask the agent to edit that skill file",
343
+ " /caveman [level]|off Terse output mode (like caveman-speak). Levels: lite|full|ultra|wenyan-lite|wenyan|wenyan-ultra",
341
344
  " /style List available output styles",
342
345
  " /style <name>|off Activate an output style for this session",
343
346
  " /rules Show loaded rule files (from .gemcode/rules/)",
gemcode/repl_slash.py CHANGED
@@ -419,6 +419,91 @@ async def process_repl_slash(
419
419
  out()
420
420
  return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
421
421
 
422
+ # ── /caveman (shortcut to built-in output styles) ─────────────────────────
423
+ if name == "caveman":
424
+ args = (sc.args or "").strip().lower()
425
+ # Levels map to built-in output styles (still overridable by project/user styles).
426
+ mapping = {
427
+ "": "caveman",
428
+ "full": "caveman",
429
+ "lite": "caveman-lite",
430
+ "ultra": "caveman-ultra",
431
+ "wenyan": "caveman-wenyan",
432
+ "wenyan-full": "caveman-wenyan",
433
+ "wenyan-lite": "caveman-wenyan-lite",
434
+ "wenyan-ultra": "caveman-wenyan-ultra",
435
+ }
436
+ if args in ("off", "stop", "normal", "none", "clear", "reset", "default"):
437
+ setattr(cfg, "output_style", None)
438
+ out("caveman: off (output_style cleared)")
439
+ out("Runner will rebuild on next turn to apply changes.")
440
+ out()
441
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
442
+ if args not in mapping:
443
+ out("Usage:")
444
+ out(" /caveman (full)")
445
+ out(" /caveman lite|full|ultra")
446
+ out(" /caveman wenyan-lite|wenyan|wenyan-ultra")
447
+ out(" /caveman off")
448
+ out()
449
+ return ReplSlashResult(skip_model_turn=True)
450
+
451
+ choice = mapping[args]
452
+ styles = discover_output_styles(cfg.project_root)
453
+ if choice not in styles or load_output_style(cfg.project_root, choice) is None:
454
+ out(f"caveman: style unavailable: {choice}")
455
+ out("Tip: update GemCode, or create a custom style at .gemcode/output-styles/")
456
+ out()
457
+ return ReplSlashResult(skip_model_turn=True)
458
+
459
+ setattr(cfg, "output_style", choice)
460
+ out(f"caveman: on — output_style: {choice}")
461
+ out("Runner will rebuild on next turn to apply changes.")
462
+ out()
463
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
464
+
465
+ # ── /caveman:compress (alias for /compress-memory) ────────────────────────
466
+ if name in ("caveman:compress", "caveman-compress", "caveman:compress-memory"):
467
+ args = (sc.args or "").strip()
468
+ parts = args.split()
469
+ if not parts:
470
+ out("Usage:")
471
+ out(" /caveman:compress <path> [lite|full|ultra]")
472
+ out("Note: mode defaults based on active /caveman level (or full).")
473
+ out()
474
+ return ReplSlashResult(skip_model_turn=True)
475
+
476
+ target = parts[0]
477
+ mode = (parts[1].strip().lower() if len(parts) >= 2 else "")
478
+ if mode and mode not in ("lite", "full", "ultra"):
479
+ out(f"Unknown mode: {mode}")
480
+ out("Use: lite|full|ultra (or omit to auto-pick from current caveman level)")
481
+ out()
482
+ return ReplSlashResult(skip_model_turn=True)
483
+
484
+ # Auto-pick mode from current output_style if not provided.
485
+ if not mode:
486
+ os_ = (getattr(cfg, "output_style", None) or "").lower()
487
+ if os_ in ("caveman-lite",):
488
+ mode = "lite"
489
+ elif os_ in ("caveman-ultra",):
490
+ mode = "ultra"
491
+ else:
492
+ mode = "full"
493
+
494
+ # Dispatch as a model turn so the agent runs the tool and reports results.
495
+ prompt = (
496
+ "Compress a memory file now.\n\n"
497
+ f"- target: `{target}`\n"
498
+ f"- mode: `{mode}`\n\n"
499
+ "Call `compress_memory_file(path=..., mode=...)` and report:\n"
500
+ "- ok/error\n"
501
+ "- path + backup_path\n"
502
+ "- warnings (if any)\n"
503
+ "- chars_before/chars_after\n"
504
+ )
505
+ return ReplSlashResult(model_prompt=prompt)
506
+
422
507
  # ── /rules ────────────────────────────────────────────────────────────────
423
508
  if name == "rules":
424
509
  rules = _load_rules(cfg.project_root, touched_paths=None)
@@ -1045,6 +1130,8 @@ async def process_repl_slash(
1045
1130
  "3. Look at the source directory structure (src/, lib/, app/, etc.)\n"
1046
1131
  "4. Check for test directories and test runner config\n"
1047
1132
  "5. Look for linting/formatting config files (.eslintrc, .prettierrc, ruff.toml, etc.)\n\n"
1133
+ "Write **only** to `GEMINI.md` at the project root. Do **not** create "
1134
+ "`CLAUDE.md`, `AGENTS.md`, `.cursorrules`, or similar.\n\n"
1048
1135
  "Then write a GEMINI.md file at the project root containing:\n"
1049
1136
  "# Project Name\n"
1050
1137
  "One-sentence description.\n\n"
gemcode/skills.py CHANGED
@@ -71,6 +71,40 @@ _BUILTIN_SKILLS: dict[str, tuple[GemSkillMeta, str]] = {
71
71
  "- **Final**: verification + next steps\n"
72
72
  ),
73
73
  ),
74
+ "compress-memory": (
75
+ GemSkillMeta(
76
+ name="compress-memory",
77
+ description=(
78
+ "Compress a markdown memory file (GEMINI.md, .gemcode notes, todos) into a terse style "
79
+ "to reduce input tokens, while preserving code blocks, headings, and URLs."
80
+ ),
81
+ disable_model_invocation=False,
82
+ user_invocable=True,
83
+ ),
84
+ (
85
+ "## Compress memory file\n"
86
+ "Use this skill to rewrite a markdown-like memory file into a more token-efficient form.\n\n"
87
+ "### When to use\n"
88
+ "- The user asks to compress GEMINI.md, .gemcode notes, preferences, or other prose-heavy markdown.\n"
89
+ "- The user wants fewer input tokens each session.\n\n"
90
+ "### Safety and boundaries\n"
91
+ "- ONLY run on markdown-like files (.md/.txt/.rst, or extensionless files under .gemcode/).\n"
92
+ "- NEVER run on secret/credential/key files (.env, credentials, .ssh, .aws, *.pem, etc.).\n"
93
+ "- This operation sends file content to the Gemini API.\n"
94
+ "- Tool will create a backup: `<stem>.original.md` and abort if backup already exists.\n"
95
+ "- Tool validates: headings, fenced code blocks, URLs. On failure, it restores the original.\n\n"
96
+ "### How to run\n"
97
+ "1. Confirm target file path from `$ARGUMENTS`.\n"
98
+ "2. Pick mode: `lite`, `full`, or `ultra` (default `full`).\n"
99
+ "3. Call the tool:\n\n"
100
+ "```python\n"
101
+ "compress_memory_file(path=\"$ARGUMENTS\", mode=\"$ARGUMENTS[1]\")\n"
102
+ "```\n\n"
103
+ "If no mode provided, call with `mode=\"full\"`.\n\n"
104
+ "### Output\n"
105
+ "- Report: ok/error, file path, backup path, and any warnings.\n"
106
+ ),
107
+ ),
74
108
  }
75
109
 
76
110
 
gemcode/tools/__init__.py CHANGED
@@ -18,6 +18,7 @@ from gemcode.tools.web import make_web_fetch_tool
18
18
  from gemcode.tools.web_search import make_web_search_tool
19
19
  from gemcode.checkpoints import list_checkpoints as _list_checkpoints, undo_checkpoint as _undo_checkpoint
20
20
  from gemcode.tools.curated_memory import make_curated_memory_tools
21
+ from gemcode.tools.compress_memory import make_compress_memory_tool
21
22
  from gemcode.tools.skills import make_skill_tools
22
23
 
23
24
 
@@ -87,6 +88,7 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
87
88
  load_tool_result = _make_load_tool_result_tool(cfg)
88
89
  repo_map = make_repo_map_tool(cfg)
89
90
  remember_fact, read_curated_memory = make_curated_memory_tools(cfg)
91
+ compress_memory_file = make_compress_memory_tool(cfg)
90
92
  list_skills, load_skill, skills_manifest = make_skill_tools(cfg)
91
93
 
92
94
  def checkpoints_list(limit: int = 20) -> dict:
@@ -149,6 +151,8 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
149
151
  # Evolving: curated memory (safe-to-inject facts)
150
152
  remember_fact,
151
153
  read_curated_memory,
154
+ # Optional: compress memory files (markdown only; safe guards apply)
155
+ compress_memory_file,
152
156
  # GemSkills (on-demand playbooks)
153
157
  list_skills,
154
158
  load_skill,
@@ -0,0 +1,333 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from gemcode.paths import PathEscapeError, resolve_under_allowed_roots
9
+ from gemcode.wal import append_wal_event
10
+
11
+
12
+ _OUTER_FENCE_REGEX = re.compile(r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL)
13
+ _URL_REGEX = re.compile(r"https?://[^\s)]+")
14
+ _FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$")
15
+ _HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE)
16
+
17
+ # Files and paths that almost certainly contain secrets/PII. This tool sends file content
18
+ # to the Gemini API; refuse high-risk targets to avoid accidental exfiltration.
19
+ _SENSITIVE_BASENAME_REGEX = re.compile(
20
+ r"(?ix)^("
21
+ r"\.env(\..+)?"
22
+ r"|\.netrc"
23
+ r"|credentials(\..+)?"
24
+ r"|secrets?(\..+)?"
25
+ r"|passwords?(\..+)?"
26
+ r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?"
27
+ r"|authorized_keys"
28
+ r"|known_hosts"
29
+ r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)"
30
+ r")$"
31
+ )
32
+ _SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"})
33
+ _SENSITIVE_NAME_TOKENS = (
34
+ "secret",
35
+ "credential",
36
+ "password",
37
+ "passwd",
38
+ "apikey",
39
+ "accesskey",
40
+ "token",
41
+ "privatekey",
42
+ )
43
+
44
+
45
+ def _is_sensitive_path(filepath: Path) -> bool:
46
+ name = filepath.name
47
+ if _SENSITIVE_BASENAME_REGEX.match(name):
48
+ return True
49
+ lowered_parts = {p.lower() for p in filepath.parts}
50
+ if lowered_parts & _SENSITIVE_PATH_COMPONENTS:
51
+ return True
52
+ lower = re.sub(r"[_\-\s.]", "", name.lower())
53
+ return any(tok in lower for tok in _SENSITIVE_NAME_TOKENS)
54
+
55
+
56
+ def _strip_llm_wrapper(text: str) -> str:
57
+ """Strip a single outer ```markdown ...``` fence if it wraps the entire output."""
58
+ m = _OUTER_FENCE_REGEX.match(text)
59
+ if m:
60
+ return m.group(2)
61
+ return text
62
+
63
+
64
+ def _extract_urls(text: str) -> set[str]:
65
+ return set(_URL_REGEX.findall(text or ""))
66
+
67
+
68
+ def _extract_headings(text: str) -> list[tuple[str, str]]:
69
+ return [(level, title.strip()) for level, title in _HEADING_REGEX.findall(text or "")]
70
+
71
+
72
+ def _extract_code_blocks(text: str) -> list[str]:
73
+ """Line-based fenced code block extractor supporting variable fence lengths."""
74
+ blocks: list[str] = []
75
+ lines = (text or "").split("\n")
76
+ i = 0
77
+ n = len(lines)
78
+ while i < n:
79
+ m = _FENCE_OPEN_REGEX.match(lines[i])
80
+ if not m:
81
+ i += 1
82
+ continue
83
+ fence_char = m.group(2)[0]
84
+ fence_len = len(m.group(2))
85
+ block_lines = [lines[i]]
86
+ i += 1
87
+ closed = False
88
+ while i < n:
89
+ close_m = _FENCE_OPEN_REGEX.match(lines[i])
90
+ if (
91
+ close_m
92
+ and close_m.group(2)[0] == fence_char
93
+ and len(close_m.group(2)) >= fence_len
94
+ and close_m.group(3).strip() == ""
95
+ ):
96
+ block_lines.append(lines[i])
97
+ closed = True
98
+ i += 1
99
+ break
100
+ block_lines.append(lines[i])
101
+ i += 1
102
+ if closed:
103
+ blocks.append("\n".join(block_lines))
104
+ return blocks
105
+
106
+
107
+ @dataclass
108
+ class _ValidationResult:
109
+ is_valid: bool
110
+ errors: list[str]
111
+ warnings: list[str]
112
+
113
+
114
+ def _validate_markdown(original: str, compressed: str) -> _ValidationResult:
115
+ errors: list[str] = []
116
+ warnings: list[str] = []
117
+
118
+ # Headings: keep count and order.
119
+ h1 = _extract_headings(original)
120
+ h2 = _extract_headings(compressed)
121
+ if len(h1) != len(h2):
122
+ errors.append(f"Heading count mismatch: {len(h1)} vs {len(h2)}")
123
+ if h1 != h2:
124
+ warnings.append("Heading text/order changed")
125
+
126
+ # Code blocks and URLs are strict invariants.
127
+ if _extract_code_blocks(original) != _extract_code_blocks(compressed):
128
+ errors.append("Code blocks not preserved exactly")
129
+
130
+ u1 = _extract_urls(original)
131
+ u2 = _extract_urls(compressed)
132
+ if u1 != u2:
133
+ errors.append(f"URL mismatch: lost={sorted(u1 - u2)[:6]}, added={sorted(u2 - u1)[:6]}")
134
+
135
+ return _ValidationResult(is_valid=(not errors), errors=errors, warnings=warnings)
136
+
137
+
138
+ def _looks_like_natural_language(path: Path) -> bool:
139
+ # Conservative: compress only markdown-ish inputs.
140
+ ext = path.suffix.lower()
141
+ if ext in (".md", ".markdown", ".txt", ".rst"):
142
+ return True
143
+ # Allow extensionless, but only under .gemcode/ by convention.
144
+ if not ext and ".gemcode" in {p.lower() for p in path.parts}:
145
+ return True
146
+ return False
147
+
148
+
149
+ def _build_prompt(original: str, *, mode: str) -> str:
150
+ style = {
151
+ "lite": "Lite: remove filler/hedging, keep full sentences.",
152
+ "full": "Full: drop articles, fragments ok, short synonyms.",
153
+ "ultra": "Ultra: telegraphic, abbreviate, arrows for causality.",
154
+ }.get(mode, "Full: drop articles, fragments ok, short synonyms.")
155
+
156
+ return f"""
157
+ Compress this markdown into a terse style (caveman-like).
158
+
159
+ TARGET STYLE: {style}
160
+
161
+ STRICT RULES:
162
+ - Do NOT modify anything inside fenced code blocks (``` or ~~~). Copy them EXACTLY.
163
+ - Do NOT modify anything inside inline backticks. Copy EXACTLY.
164
+ - Preserve ALL URLs exactly.
165
+ - Preserve ALL headings exactly (same heading lines, same order).
166
+ - Return ONLY the compressed markdown body (no outer ```markdown fence).
167
+
168
+ Only compress natural language prose outside code/backticks.
169
+
170
+ TEXT:
171
+ {original}
172
+ """.strip()
173
+
174
+
175
+ def _build_fix_prompt(original: str, compressed: str, errors: list[str]) -> str:
176
+ errors_str = "\n".join(f"- {e}" for e in errors)
177
+ return f"""You are fixing a compressed markdown file. Specific validation errors were found.
178
+
179
+ CRITICAL RULES:
180
+ - DO NOT recompress or rephrase the whole file
181
+ - ONLY fix the listed errors — leave everything else exactly as-is
182
+ - The ORIGINAL is reference only (to restore missing content)
183
+ - Preserve the terse style in untouched sections
184
+
185
+ ERRORS TO FIX:
186
+ {errors_str}
187
+
188
+ ORIGINAL (reference only):
189
+ {original}
190
+
191
+ COMPRESSED (fix this):
192
+ {compressed}
193
+
194
+ Return ONLY the fixed compressed file. No explanation.
195
+ """
196
+
197
+
198
+ def make_compress_memory_tool(cfg):
199
+ """
200
+ Build a function tool that compresses markdown-like memory files.
201
+ """
202
+ project_root = cfg.project_root
203
+
204
+ def compress_memory_file(
205
+ path: str,
206
+ *,
207
+ mode: str = "full",
208
+ max_bytes: int = 500_000,
209
+ backup_ext: str = ".original.md",
210
+ ) -> dict:
211
+ """
212
+ Compress a markdown-like memory file to reduce input tokens.
213
+
214
+ Safety:
215
+ - Refuses paths that look like secrets (keys, credentials, .ssh, .aws, etc.)
216
+ - Size-capped before any model call
217
+ - Writes backup as <stem>{backup_ext}; aborts if backup exists
218
+ - Validates headings/URLs/code blocks; restores original on failure
219
+ """
220
+ extra_roots = getattr(cfg, "_added_dirs", None) or {}
221
+ try:
222
+ p, _scope = resolve_under_allowed_roots(project_root, path, extra_roots=extra_roots)
223
+ except PathEscapeError as e:
224
+ return {"ok": False, "error": str(e)}
225
+
226
+ if not p.exists():
227
+ return {"ok": False, "error": f"File not found: {p}"}
228
+ if not p.is_file():
229
+ return {"ok": False, "error": f"Not a file: {p}"}
230
+ if p.stat().st_size > max_bytes:
231
+ return {"ok": False, "error": f"File too large (max {max_bytes} bytes): {p}"}
232
+ if _is_sensitive_path(p):
233
+ return {
234
+ "ok": False,
235
+ "error": (
236
+ f"Refusing to compress {p}: filename/path looks sensitive. "
237
+ "This tool sends file content to the Gemini API."
238
+ ),
239
+ }
240
+ if not _looks_like_natural_language(p):
241
+ return {"ok": False, "error": f"Refusing: not a markdown-like file: {p.name}"}
242
+ if p.name.endswith(backup_ext):
243
+ return {"ok": False, "error": f"Refusing: backup file target: {p.name}"}
244
+
245
+ original = p.read_text(encoding="utf-8", errors="replace")
246
+ backup_path = p.with_name(p.stem + backup_ext)
247
+ if backup_path.exists():
248
+ return {"ok": False, "error": f"Backup already exists: {backup_path}"}
249
+
250
+ # Call Gemini (local-first but does cross the API boundary).
251
+ try:
252
+ from google.genai import Client
253
+ except Exception as e:
254
+ return {"ok": False, "error": f"google-genai unavailable: {e}"}
255
+
256
+ api_key = os.environ.get("GOOGLE_API_KEY")
257
+ if not api_key:
258
+ return {"ok": False, "error": "GOOGLE_API_KEY is not set"}
259
+
260
+ model = os.environ.get("GEMCODE_COMPRESS_MODEL") or getattr(cfg, "model_alt", None) or cfg.model
261
+
262
+ client = Client(api_key=api_key)
263
+ prompt = _build_prompt(original, mode=mode)
264
+ try:
265
+ resp = client.models.generate_content(
266
+ model=model,
267
+ contents=prompt,
268
+ )
269
+ text = getattr(resp, "text", None) or ""
270
+ except Exception as e:
271
+ return {"ok": False, "error": f"Gemini call failed: {type(e).__name__}: {e}"}
272
+
273
+ compressed = _strip_llm_wrapper((text or "").strip())
274
+
275
+ # Write backup + compressed, then validate with repair loop.
276
+ backup_path.write_text(original, encoding="utf-8")
277
+ p.write_text(compressed, encoding="utf-8")
278
+
279
+ max_retries = 2
280
+ for attempt in range(max_retries + 1):
281
+ res = _validate_markdown(original, compressed)
282
+ if res.is_valid:
283
+ # Best-effort WAL: metadata only (no content).
284
+ append_wal_event(
285
+ project_root,
286
+ type="compress_memory_file",
287
+ data={
288
+ "path": str(p),
289
+ "backup_path": str(backup_path),
290
+ "mode": mode,
291
+ "chars_before": len(original),
292
+ "chars_after": len(compressed),
293
+ "warnings": res.warnings,
294
+ },
295
+ )
296
+ return {
297
+ "ok": True,
298
+ "path": str(p),
299
+ "backup_path": str(backup_path),
300
+ "warnings": res.warnings,
301
+ "chars_before": len(original),
302
+ "chars_after": len(compressed),
303
+ }
304
+ if attempt >= max_retries:
305
+ # Restore original.
306
+ p.write_text(original, encoding="utf-8")
307
+ try:
308
+ backup_path.unlink()
309
+ except OSError:
310
+ pass
311
+ return {"ok": False, "error": f"Validation failed: {res.errors}", "warnings": res.warnings}
312
+
313
+ # Targeted fix
314
+ fix_prompt = _build_fix_prompt(original, compressed, res.errors)
315
+ try:
316
+ resp2 = client.models.generate_content(model=model, contents=fix_prompt)
317
+ fixed = _strip_llm_wrapper(((getattr(resp2, "text", None) or "")).strip())
318
+ except Exception as e:
319
+ p.write_text(original, encoding="utf-8")
320
+ try:
321
+ backup_path.unlink()
322
+ except OSError:
323
+ pass
324
+ return {"ok": False, "error": f"Fix attempt failed: {type(e).__name__}: {e}"}
325
+
326
+ compressed = fixed
327
+ p.write_text(compressed, encoding="utf-8")
328
+
329
+ return {"ok": False, "error": "Unexpected failure"}
330
+
331
+ compress_memory_file.__name__ = "compress_memory_file"
332
+ return compress_memory_file
333
+
gemcode/tools/edit.py CHANGED
@@ -23,11 +23,16 @@ def make_edit_tools(cfg: GemCodeConfig):
23
23
  except Exception:
24
24
  pass
25
25
 
26
- # Block writes to common non-GemCode agent instruction filenames.
27
- _BLOCKED_SPECIAL_FILES = {
28
- "claude.md",
29
- "agents.md",
30
- }
26
+ # Block writes to common non-GemCode / third-party agent instruction filenames.
27
+ _BLOCKED_SPECIAL_FILES = frozenset(
28
+ {
29
+ "claude.md",
30
+ "agents.md",
31
+ "claude.local.md",
32
+ "agents.local.md",
33
+ ".cursorrules",
34
+ }
35
+ )
31
36
 
32
37
  def _blocked_special_path(path: str) -> str | None:
33
38
  try:
@@ -18,6 +18,8 @@ import urllib.request
18
18
  from html.parser import HTMLParser
19
19
  from typing import Any
20
20
 
21
+ from gemcode.query_sanitizer import sanitize_tool_query
22
+
21
23
 
22
24
  class _DDGResultParser(HTMLParser):
23
25
  """Parse DuckDuckGo HTML search results page into a list of hits."""
@@ -222,7 +224,11 @@ def make_web_search_tool():
222
224
  """
223
225
  if not query or not query.strip():
224
226
  return {"error": "query must not be empty"}
225
- query = query.strip()
227
+ raw = query.strip()
228
+ s = sanitize_tool_query(raw)
229
+ query = str(s.get("clean_query") or "").strip()
230
+ if not query:
231
+ return {"error": "query must not be empty"}
226
232
  max_results = max(1, min(int(max_results), 20))
227
233
 
228
234
  # Try the richer duckduckgo_search package first
@@ -239,6 +245,8 @@ def make_web_search_tool():
239
245
 
240
246
  return {
241
247
  "query": query,
248
+ "query_sanitized": bool(s.get("was_sanitized")),
249
+ "query_sanitizer_method": str(s.get("method")),
242
250
  "results": results,
243
251
  "count": len(results),
244
252
  "tip": "Use web_fetch(url) to read the full content of any result.",
gemcode/wal.py ADDED
@@ -0,0 +1,62 @@
1
+ """
2
+ Write-ahead log (WAL) for durable, user-visible state mutations.
3
+
4
+ Goal: auditability and safer debugging for memory-related writes without logging
5
+ full sensitive content.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ from dataclasses import dataclass
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+
18
+ def wal_path(project_root: Path) -> Path:
19
+ return project_root / ".gemcode" / "wal.jsonl"
20
+
21
+
22
+ def _utc_now_iso() -> str:
23
+ return datetime.now(timezone.utc).isoformat()
24
+
25
+
26
+ def _sha256_hex(text: str) -> str:
27
+ return hashlib.sha256((text or "").encode("utf-8", errors="ignore")).hexdigest()
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class WalEvent:
32
+ type: str
33
+ ts: str
34
+ data: dict[str, Any]
35
+
36
+
37
+ def append_wal_event(project_root: Path, *, type: str, data: dict[str, Any]) -> dict[str, Any]:
38
+ """
39
+ Append a JSONL WAL record under .gemcode/wal.jsonl.
40
+
41
+ This function must be best-effort: if WAL writing fails, do not block the
42
+ primary operation.
43
+ """
44
+ try:
45
+ p = wal_path(project_root)
46
+ p.parent.mkdir(parents=True, exist_ok=True)
47
+ ev = WalEvent(type=type, ts=_utc_now_iso(), data=data)
48
+ line = json.dumps({"type": ev.type, "ts": ev.ts, "data": ev.data}, ensure_ascii=False)
49
+ with p.open("a", encoding="utf-8") as f:
50
+ f.write(line + "\n")
51
+ return {"ok": True, "path": str(p)}
52
+ except Exception as e:
53
+ return {"ok": False, "error": f"{type(e).__name__}: {e}"}
54
+
55
+
56
+ def wal_text_fingerprint(text: str) -> dict[str, Any]:
57
+ """
58
+ Content fingerprint for WAL without storing raw text.
59
+ """
60
+ t = (text or "").strip()
61
+ return {"chars": len(t), "sha256": _sha256_hex(t)}
62
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.80
3
+ Version: 0.3.82
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -381,6 +381,7 @@ State is **project-local** (unless noted).
381
381
  | `policy.json` | Self-tuning profile for dynamic token / evidence budgets. |
382
382
  | `memories.jsonl` | Embedding-backed memory when `GEMCODE_ENABLE_MEMORY=1`. |
383
383
  | `notes.md` | Agent notes surfaced via `/notes`. |
384
+ | `wal.jsonl` | Write-ahead log for curated memory + compression actions (metadata only). |
384
385
  | `evals/last_eval.json` | Latest `gemcode eval` record. |
385
386
  | `evals/autotune_ledger.jsonl` | Rows from `gemcode autotune eval`. |
386
387
  | `skills/<name>/SKILL.md` | **GemSkills** (project-scoped; see [GemSkills](#gemskills)). |
@@ -473,7 +474,7 @@ Tools are registered in `gemcode/tools/` and exposed to the model as ADK functio
473
474
  - **Computer use:** ADK `ComputerUseToolset` + Playwright (separate install and flags).
474
475
  - **MCP:** Tools loaded from configured servers.
475
476
 
476
- **Vendor file policy:** Writes to certain vendor-specific instruction filenames (e.g. `CLAUDE.md`, `AGENTS.md`) are blocked; use project conventions like `GEMINI.md` and curated memory files instead.
477
+ **Vendor file policy:** Writes to certain third-party instruction filenames (`CLAUDE.md`, `AGENTS.md`, `*.local` variants, `.cursorrules`, …) are blocked; use `GEMINI.md` and `.gemcode/notes.md` instead. The agent instruction always states this; `write_file` / `search_replace` enforce it.
477
478
 
478
479
  ---
479
480
 
@@ -590,6 +591,13 @@ Legacy filenames **`.gemcode/MEMORY.md`** and **`.gemcode/USER.md`** are still r
590
591
 
591
592
  Content is **sanitized** before append (length and sensitivity heuristics). The REPL command **`/curated`** prints a bounded snapshot of what would be injected. When the **memory** capability is enabled, curated material can be combined with broader memory tools—see **`remember_fact`**, **`read_curated_memory`** in the [function tools](#function-tools-catalog) table.
592
593
 
594
+ ### Query sanitization (tooling)
595
+
596
+ Some tools sanitize long “contaminated” queries (e.g. accidental system-prompt prefixes) down to a short, likely-intended query. This currently applies to:
597
+
598
+ - `web_search(query=...)`
599
+ - `semantic_search_files(query=...)` (when `--embeddings` is on)
600
+
593
601
  ---
594
602
 
595
603
  ## Workspace trust
@@ -1,6 +1,6 @@
1
1
  gemcode/__init__.py,sha256=l0DCRYqK7KM7Fb7u49fqh-5_SlpeIL7r3LjMeJWMgSg,112
2
2
  gemcode/__main__.py,sha256=EX2s1hxq2Yvli_-tnBN3w5Qv4bOjsBBbjyISF0pDIQw,37
3
- gemcode/agent.py,sha256=_v00pQSOzAI9nyaaKC1wn-aTalD4SV52KDzwlyIHJAo,56867
3
+ gemcode/agent.py,sha256=NB2eP7RV8vNp4flk4P4aB_LIIV8TvZtB7ZEXA4O3h-Q,57262
4
4
  gemcode/audit.py,sha256=bh9uhXaeh8wqxqoZtz3ZAowd8Ndk1ss-mw9993Vlrgo,469
5
5
  gemcode/autocompact.py,sha256=OE3QbGx2gWN2WVXy28Sd0xyfAJgVR1x6ml9HrX2CB7I,6719
6
6
  gemcode/autotune.py,sha256=zcTGDKC8LSnw0fHuoOcnnh1rz0by8K6MTUKl0_GhT9s,2704
@@ -13,7 +13,7 @@ gemcode/config.py,sha256=Nmq0ClbebkbqfCPZOw8ETGjpjHuGCtByzpEMU1V5Wvg,18208
13
13
  gemcode/context_budget.py,sha256=nctVwzpUg6kyBL0FHM7xcrvVvZpX9qj7cZK4IczfKAg,10534
14
14
  gemcode/context_warning.py,sha256=YwkkNx6AM2ugOckg8QhRWYcU6D3UJXUOHiBwjKQT2rs,4199
15
15
  gemcode/credentials.py,sha256=o1gQ4oQXAjjA1IANXgCalM3WjTkfVkbkXsf-lNVQ55I,1300
16
- gemcode/curated_memory.py,sha256=5GfMS7JgSfEkAgnjFQle1Zy4VpoyKXI0osifnhMhZv8,3215
16
+ gemcode/curated_memory.py,sha256=bhq8UesUCXJEWvIoOZ2pF9HoUVDheZCkErq-zJ_BVjA,3557
17
17
  gemcode/dynamic_policy.py,sha256=nWgBN6ffSn1Te4aDI1MURxRaQjTclzIuSf4KaAskE9U,4662
18
18
  gemcode/hitl_session.py,sha256=oNiI7odFJGUcmqPavjKLJOEumZKrgklLvwjjrIG9GPg,281
19
19
  gemcode/hooks.py,sha256=tAzzZgfALC-nqSGoUdnEvHyGJNM2iTqKN5Yrfm8wFo8,6350
@@ -28,25 +28,26 @@ gemcode/limits.py,sha256=3j6N8V643X7-nP-cAIf37Xg9bkGpQlEJB3nPptApQWk,2504
28
28
  gemcode/live_audio_engine.py,sha256=wxdGp5ciAgQ7WIeBpcW_hm8lbMDsh0KPJbqIS9uAGxA,3406
29
29
  gemcode/logging_config.py,sha256=nw_FyJbXM3StzaAx6b6BLLNvzuxsGg-sdQ3QRw7BWgs,1080
30
30
  gemcode/mcp_loader.py,sha256=alipHTl5aA7ZCPG6Rq9cyy3UZLsdCra0CETb_fRJl_k,4964
31
- gemcode/modality_tools.py,sha256=tCcqY6Ca8a_kaO58GBC6OmrmLrrZs_jcKv6fTvt5esE,6879
31
+ gemcode/modality_tools.py,sha256=L2U756l0YM4SOUx5YxcKEkjSF1QOGzFOSE4o3U0q-4Y,7213
32
32
  gemcode/model_errors.py,sha256=j1nb1dopJyZ6MQQvuuADBqvmcqdL80kQWACuWKMkP4Q,4185
33
33
  gemcode/model_routing.py,sha256=_8mnXNwxMPA8wAfl-Yx5lWNgjhyWYTCCCMGlqdGAw90,5174
34
34
  gemcode/multimodal_input.py,sha256=FrfwcmqgDKnPDWxj76GOltl34D3PNQHxgbSR7ykL8SU,4450
35
35
  gemcode/openapi_loader.py,sha256=g_NZD8YL9_9iIJJ9qykhdbBrylJ1195A4FyHGC0mroc,4157
36
- gemcode/output_styles.py,sha256=29LSK3GGQLcodQhtXjSozgxCj7Z6VZZK8gIodOAC5EA,1910
36
+ gemcode/output_styles.py,sha256=6ZgbEsEM5qbdGDZMuRFRHAmHCoKpi53SqPqXZ2sfbS4,2189
37
37
  gemcode/paths.py,sha256=UQp4R4sUBv7HsM2OVoGlPxyOIOQZE5wpY53G6nHpB5Q,2671
38
38
  gemcode/permissions.py,sha256=QqaWxSv9oYtGOV0xxftPces7MUgHlGQ2z4DcwlQANEc,6892
39
39
  gemcode/policy_profile.py,sha256=kcaKJQwLxAo3RjqfJJHl_G7B5GgTYKco0z3k5QcXsVY,3861
40
40
  gemcode/pricing.py,sha256=lftp0SwyDqOzHqC2-6XzgZZhjif5PLdCe1Q3wY-p6kQ,3558
41
41
  gemcode/prompt_suggestions.py,sha256=RNEclxtoorRqu-wUlzuyUJ7OLFVOOryGOZBpbaCducI,2544
42
+ gemcode/query_sanitizer.py,sha256=KqXo5U7igNSgOAH4YpyANlgS--1WnZEdpO4AU4SNQUs,2746
42
43
  gemcode/refine.py,sha256=BijEZ4Z32wGa9aK_WottyAhZF-j0xEqRg5UpjedNv2A,7653
43
- gemcode/repl_commands.py,sha256=XKGXLkA_KfQZubg9_oNjKdd2NXnxOMi8KiwVU8Lllvc,18892
44
- gemcode/repl_slash.py,sha256=HNtKDiV2EHCMxvr3uWC8x1XkcpwGlRmunS6_ySwyr1E,85938
44
+ gemcode/repl_commands.py,sha256=fR2vgaNbpOKNhLLs3rEEnWUarZyjy9EPZWopKApdXL0,19204
45
+ gemcode/repl_slash.py,sha256=ENG0gE1_aN-V8YCwmIAbcZKHhRaHmQCCJ5zgSxfpq5c,89444
45
46
  gemcode/review_agent.py,sha256=4t7_5-aE60b4-EheJ_eSB_H2eQYf9GppKoui6jw0TME,5264
46
47
  gemcode/rules.py,sha256=Itg02VpifOo6jqGj5xwna_ahaPPb0OVtaeR2cNI0pLE,3018
47
48
  gemcode/session_runtime.py,sha256=MQr34s6dw4bmk_VPSgBvkJTMwUBZQhvWDXl_iC5Pp6s,20867
48
49
  gemcode/session_store.py,sha256=POUT_QQf715c74jbXj0s5vCd4dlAgJz_CLsIWuEUoO0,6051
49
- gemcode/skills.py,sha256=sT22J1dMzGuLn1DIwf3hJg3frSSlb9LfdES59SrFvW4,9595
50
+ gemcode/skills.py,sha256=Sbg6bGz4DRg-NC3T3MrlRc9hHwVAB7jAObfIhp4NE5g,11244
50
51
  gemcode/slash_commands.py,sha256=bcD-S_H7p7AlTli6g2dLPPG46HejPje0Imb3ScDTCaQ,798
51
52
  gemcode/thinking.py,sha256=-1TVkOMG-7CSQN0Mc18EqINkUxWOMBgeTlF7CX9zYL4,4641
52
53
  gemcode/tool_prompt_manifest.py,sha256=BbM88yj6hN5CTDBzyHa7DZ1S6gpzIZQe9G7dFqkMmtA,8681
@@ -56,6 +57,7 @@ gemcode/tools_inspector.py,sha256=ZAuD1oVIsZvyD_vrzaWBws2ezpFT6cIsU_w4yc_1PPA,40
56
57
  gemcode/trust.py,sha256=kZi1dll1T8RUZtSY42WbsEh-ZIkMRup_IZPfYlJMbw4,1457
57
58
  gemcode/version.py,sha256=uwynYS-RmK8CDoqGtt8976kFkJv0zELkEAlwebnp_io,380
58
59
  gemcode/vertex.py,sha256=Fy8zxuU8jWkObt0WDRI0XmgnjNznILXVLVwKjImNz9Q,643
60
+ gemcode/wal.py,sha256=moUldC__j0YmuNhVuawIIDYSoIgZ9_HpwKFvU9vieq8,1645
59
61
  gemcode/workspace_hints.py,sha256=WQFwyoGnVrzzYSl0s5MNcd1-UP11Ao8KGgrdFIB5m9g,621
60
62
  gemcode/computer_use/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
63
  gemcode/computer_use/browser_computer.py,sha256=bWXXgj7K6ZrDqT49iPEcnBd_ZJBwRxSgKhzKaJpn4cQ,13212
@@ -73,11 +75,12 @@ gemcode/query/engine.py,sha256=RuAx7jWLjkvUQcTjRry22GOY27GvAPhkt6r6AZGpokw,1416
73
75
  gemcode/query/stop_hooks.py,sha256=wBInW-hmHoFdTs91Jc7YY52Tx9GT63CU-P9Gbd9ijNQ,1842
74
76
  gemcode/query/token_budget.py,sha256=lmoFx1RNRdafPisO3bxt7p7bkr5lyCMV7N2T7caHwKA,2957
75
77
  gemcode/query/transitions.py,sha256=30xfeexEF1C2pjqvr00dvkdSsnsZ4UQTfi_c4wIhmd0,929
76
- gemcode/tools/__init__.py,sha256=PiVHym3WCkRCC3Y45y_bORnhplY91LaIOaloRYjHPRY,5746
78
+ gemcode/tools/__init__.py,sha256=BrS0vs0D2l6rPTYq26Ayl8eEN_gIfluvk9zxapMzsg4,5969
77
79
  gemcode/tools/bash.py,sha256=kedqaL2CU6O9WLPT83FToqJSaRBEGHesJSBH4pRLHlU,13649
78
80
  gemcode/tools/browser.py,sha256=StWRttiyGkR4qaG5urRviJgdoj2hiFB2OuHPItaVzJY,7250
81
+ gemcode/tools/compress_memory.py,sha256=rI8Uu8DO_io6ltRYgTdlNhQLZjyJCpSObnQ15CBVlQI,10396
79
82
  gemcode/tools/curated_memory.py,sha256=HgpZS81Ncvf0aoSiQ9zDdJdMTA_NWTL4bjLy34Z9a_A,1044
80
- gemcode/tools/edit.py,sha256=_n45v10YQyKKE2muLDIyeFImzFs0fRztG4xv-HOLnDw,7807
83
+ gemcode/tools/edit.py,sha256=XQbN9iZNiUmzieSDNMPiK-I3LWt5EkJdDPN9slyUJn0,7942
81
84
  gemcode/tools/filesystem.py,sha256=1GtLPOKzgjsGNoQA1UvjaUA2MDzm4RjhxUsolGm2ImA,8330
82
85
  gemcode/tools/notebook.py,sha256=SF7c-iBDz8heBRK2hERYq39tFeEixhP-1d0kIHBruW4,9208
83
86
  gemcode/tools/notes.py,sha256=y1UJOMKntDDB5e8bBPkVVMDfie3ZgKmaoO8r5Cc-owA,4448
@@ -91,7 +94,7 @@ gemcode/tools/tasks.py,sha256=4kDjMuoxgD3kJtM4fy2bOQl0ak6CljdA-nN1-tJG-dw,7668
91
94
  gemcode/tools/think.py,sha256=bch9bsz1bs24uia5l3utnNSkT84mIyU_EzMgi82e60Y,1624
92
95
  gemcode/tools/todo.py,sha256=dlGfcNce1WsJ5Y9txrDL3SoF6Hv2rms9r1cvGPs6qIs,5798
93
96
  gemcode/tools/web.py,sha256=I-6-GgCVKblc9zVFfilWLHoJZfri7_pC2MpT52ZZarE,5078
94
- gemcode/tools/web_search.py,sha256=YqIvwAoOXK3TMqrrMVeSrg5Nyt7Ou5nRljrYQ4784_4,8816
97
+ gemcode/tools/web_search.py,sha256=UsO3W2FDRSJYtXQTT0jildzEQLt6P3XeR7KyUi-Dxqs,9163
95
98
  gemcode/tui/input_handler.py,sha256=Az8SbPaPHksIoibjph8gevMnfjagR1b-34_wpKbEhgQ,8259
96
99
  gemcode/tui/scrollback.py,sha256=1LcH39ZRiO2BFHWBFEcWZdWpe6-XFhUYWAePCyx91PI,33534
97
100
  gemcode/tui/spinner.py,sha256=dExs_enBPOWjkmRtodDzRw3E-MYh-xgtqDo54Q82sco,4892
@@ -101,9 +104,9 @@ gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
101
104
  gemcode/web/sse_adapter.py,sha256=fXhKxn_bdJJUGqlmvkxLNSYL-ZiIZDaLHtQCF_BheRc,7108
102
105
  gemcode/web/terminal_repl.py,sha256=fQt895g0qcr6VBhXfv_5b_bsC5zHT5-MO0ysBdgi2Fg,3886
103
106
  gemcode/web/web_sse_compat.py,sha256=9A2s-GI7El7AotJqhO263FrLwppCXXkdydZ5EiOQbao,504
104
- gemcode-0.3.80.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
105
- gemcode-0.3.80.dist-info/METADATA,sha256=E3OS-VQUnxZ0QMNe5fEWh5bQfhtT72E1XzwwTcM5q9s,41106
106
- gemcode-0.3.80.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
107
- gemcode-0.3.80.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
108
- gemcode-0.3.80.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
109
- gemcode-0.3.80.dist-info/RECORD,,
107
+ gemcode-0.3.82.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
108
+ gemcode-0.3.82.dist-info/METADATA,sha256=GuFoQn-JSFIaaQA0cM64yDPmnvjh7SUYOPQ_kcUpgV8,41577
109
+ gemcode-0.3.82.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
110
+ gemcode-0.3.82.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
111
+ gemcode-0.3.82.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
112
+ gemcode-0.3.82.dist-info/RECORD,,