gemcode 0.3.78__py3-none-any.whl → 0.3.81__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/cli.py CHANGED
@@ -147,7 +147,12 @@ def _initialize_gemcode_project(cfg: GemCodeConfig) -> None:
147
147
 
148
148
 
149
149
  async def _run_prompt(
150
- cfg: GemCodeConfig, prompt: str, session_id: str, *, use_mcp: bool
150
+ cfg: GemCodeConfig,
151
+ prompt: str,
152
+ session_id: str,
153
+ *,
154
+ use_mcp: bool,
155
+ attachment_paths: list[Path] | None = None,
151
156
  ) -> str:
152
157
  load_cli_environment()
153
158
  _maybe_prompt_trust(cfg)
@@ -164,6 +169,7 @@ async def _run_prompt(
164
169
  prompt=prompt,
165
170
  max_llm_calls=cfg.max_llm_calls,
166
171
  cfg=cfg,
172
+ attachment_paths=attachment_paths,
167
173
  )
168
174
  return _events_to_text(collected)
169
175
  finally:
@@ -296,6 +302,8 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
296
302
 
297
303
  apply_capability_routing(cfg, prompt_text, context="prompt")
298
304
  cfg.model = pick_effective_model(cfg, prompt_text)
305
+ _repl_attach = list(cfg.pending_attachment_paths)
306
+ cfg.pending_attachment_paths.clear()
299
307
  collected = await run_turn(
300
308
  runner,
301
309
  user_id="local",
@@ -303,6 +311,7 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
303
311
  prompt=prompt_text,
304
312
  max_llm_calls=cfg.max_llm_calls,
305
313
  cfg=cfg,
314
+ attachment_paths=_repl_attach if _repl_attach else None,
306
315
  )
307
316
  out = _events_to_text(collected)
308
317
  if out:
@@ -795,6 +804,17 @@ def main() -> None:
795
804
  metavar="N",
796
805
  help="Cap model↔tool iterations for this message (maps to ADK RunConfig.max_llm_calls)",
797
806
  )
807
+ parser.add_argument(
808
+ "--attach",
809
+ "--image",
810
+ dest="attachments",
811
+ action="append",
812
+ default=[],
813
+ metavar="PATH",
814
+ help="Attach file(s) for this message (repeatable): images, PDF, audio, video, text, etc. "
815
+ "(Gemini-supported MIME). Default max ~20 MiB each (GEMCODE_MAX_ATTACHMENT_BYTES). "
816
+ "REPL: /attach or /image <path>.",
817
+ )
798
818
  args = parser.parse_args()
799
819
 
800
820
  load_cli_environment()
@@ -846,7 +866,16 @@ def main() -> None:
846
866
  prompt_text = prompt.strip()
847
867
  apply_capability_routing(cfg, prompt_text, context="prompt")
848
868
  cfg.model = pick_effective_model(cfg, prompt_text)
849
- out = asyncio.run(_run_prompt(cfg, prompt_text, session_id, use_mcp=args.mcp))
869
+ _cli_attach = list(args.attachments) if getattr(args, "attachments", None) else []
870
+ out = asyncio.run(
871
+ _run_prompt(
872
+ cfg,
873
+ prompt_text,
874
+ session_id,
875
+ use_mcp=args.mcp,
876
+ attachment_paths=_cli_attach if _cli_attach else None,
877
+ )
878
+ )
850
879
  if out:
851
880
  print(out)
852
881
  print(f"\n[gemcode] session_id={session_id}", file=sys.stderr)
gemcode/config.py CHANGED
@@ -218,6 +218,9 @@ class GemCodeConfig:
218
218
  # Substitutes ${GEMCODE_SESSION_ID} when expanding loaded skills for prompts.
219
219
  session_skill_expand_session_id: str | None = None
220
220
 
221
+ # REPL/TUI: paths queued with /attach (or /image …), sent on the next message then cleared.
222
+ pending_attachment_paths: list[Path] = field(default_factory=list)
223
+
221
224
  # Modality toggles (tool injection + routing).
222
225
  enable_deep_research: bool = field(
223
226
  default_factory=lambda: _truthy_env("GEMCODE_ENABLE_DEEP_RESEARCH", default=False)
gemcode/ide_protocol.py CHANGED
@@ -8,6 +8,14 @@ Design goals:
8
8
  - Human-readable JSONL (easy to debug)
9
9
  - Streaming (token deltas + progress)
10
10
  - Safe editing (engine proposes; IDE applies via WorkspaceEdit)
11
+
12
+ Attachments on ``action: turn`` may include:
13
+
14
+ - Textual: ``type: selection`` / ``file`` (appended into the prompt as fenced blocks).
15
+ - Binary / multimodal: ``type: inline`` | ``binary`` | ``blob`` with ``data`` or ``base64``
16
+ (standard base64), plus optional ``filename`` / ``name`` and ``mimeType`` / ``mime_type``.
17
+ The engine writes bytes to a temp file and passes them to Gemini as inline parts
18
+ (same limits as CLI ``--attach``).
11
19
  """
12
20
 
13
21
  from __future__ import annotations
gemcode/ide_stdio.py CHANGED
@@ -16,8 +16,13 @@ GemCode is responsible for:
16
16
  from __future__ import annotations
17
17
 
18
18
  import asyncio
19
+ import base64
20
+ import binascii
21
+ import mimetypes
19
22
  import os
20
23
  import sys
24
+ import tempfile
25
+ from pathlib import Path
21
26
  from typing import Any
22
27
 
23
28
  from gemcode.config import GemCodeConfig, load_cli_environment
@@ -38,6 +43,107 @@ def _truthy(v: Any, default: bool = False) -> bool:
38
43
  return default
39
44
 
40
45
 
46
+ def _max_ide_inline_bytes() -> int:
47
+ raw = os.environ.get("GEMCODE_MAX_ATTACHMENT_BYTES")
48
+ if raw:
49
+ try:
50
+ v = int(raw, 10)
51
+ if v > 0:
52
+ return v
53
+ except ValueError:
54
+ pass
55
+ return 20 * 1024 * 1024
56
+
57
+
58
+ def _suffix_for_inline_attachment(name: str, mime: str) -> str:
59
+ p = Path(name or "")
60
+ if p.suffix and len(p.suffix) <= 12:
61
+ return p.suffix
62
+ m = (mime or "").strip().lower().split(";")[0].strip()
63
+ if m:
64
+ ext = mimetypes.guess_extension(m, strict=False)
65
+ if ext == ".jpe":
66
+ ext = ".jpeg"
67
+ if ext:
68
+ return ext
69
+ return ".bin"
70
+
71
+
72
+ def prepare_inline_attachment_paths(
73
+ attachments: list[Any] | None,
74
+ *,
75
+ max_bytes: int | None = None,
76
+ max_count: int = 16,
77
+ ) -> tuple[list[Path], list[str]]:
78
+ """
79
+ Materialize IDE ``inline`` / ``binary`` / ``blob`` attachment dicts as temp files.
80
+
81
+ Expected keys (camelCase accepted): ``data`` or ``base64``, optional ``name`` /
82
+ ``filename``, ``mime_type`` / ``mimeType``.
83
+
84
+ Returns ``(paths, errors)``. Caller must unlink paths when done.
85
+ """
86
+ cap = max_bytes if max_bytes is not None else _max_ide_inline_bytes()
87
+ max_b64 = int(cap * 4 / 3) + 32
88
+ paths: list[Path] = []
89
+ errors: list[str] = []
90
+ if not attachments:
91
+ return paths, errors
92
+
93
+ for a in attachments:
94
+ if len(paths) >= max_count:
95
+ errors.append(f"inline attachments: max {max_count} files")
96
+ break
97
+ if not isinstance(a, dict):
98
+ continue
99
+ at = str(a.get("type") or "").strip().lower()
100
+ if at not in ("inline", "binary", "blob"):
101
+ continue
102
+ b64 = a.get("data") if isinstance(a.get("data"), str) else None
103
+ if b64 is None:
104
+ b64 = a.get("base64") if isinstance(a.get("base64"), str) else None
105
+ if not b64:
106
+ errors.append("inline attachment missing base64 data")
107
+ continue
108
+ if len(b64) > max_b64:
109
+ errors.append("inline attachment too large (base64)")
110
+ continue
111
+ try:
112
+ raw = base64.b64decode(b64, validate=True)
113
+ except (binascii.Error, ValueError) as e:
114
+ errors.append(f"inline attachment: invalid base64 ({e})")
115
+ continue
116
+ if len(raw) > cap:
117
+ errors.append(f"inline attachment exceeds max {cap} bytes")
118
+ continue
119
+ name = str(a.get("name") or a.get("filename") or "attachment")
120
+ mime = str(a.get("mime_type") or a.get("mimeType") or "")
121
+ suffix = _suffix_for_inline_attachment(name, mime)
122
+ try:
123
+ fd, fspath = tempfile.mkstemp(prefix="gemcode_ide_", suffix=suffix)
124
+ with os.fdopen(fd, "wb") as f:
125
+ f.write(raw)
126
+ paths.append(Path(fspath))
127
+ except OSError as e:
128
+ errors.append(f"inline attachment temp file failed: {e}")
129
+
130
+ return paths, errors
131
+
132
+
133
+ def _textual_attachments_only(attachments: list[dict] | None) -> list[dict]:
134
+ if not attachments:
135
+ return []
136
+ out: list[dict] = []
137
+ for a in attachments:
138
+ if not isinstance(a, dict):
139
+ continue
140
+ at = str(a.get("type") or "").strip().lower()
141
+ if at in ("inline", "binary", "blob"):
142
+ continue
143
+ out.append(a)
144
+ return out
145
+
146
+
41
147
  def _build_prompt(prompt: str, attachments: list[dict] | None) -> str:
42
148
  # Keep it simple: attachments are appended as fenced blocks.
43
149
  if not attachments:
@@ -47,6 +153,8 @@ def _build_prompt(prompt: str, attachments: list[dict] | None) -> str:
47
153
  if not isinstance(a, dict):
48
154
  continue
49
155
  at = (a.get("type") or "").strip().lower()
156
+ if at in ("inline", "binary", "blob"):
157
+ continue
50
158
  if at == "selection":
51
159
  txt = a.get("text") or ""
52
160
  path = a.get("path") or ""
@@ -139,7 +247,6 @@ async def run_stdio_loop() -> int:
139
247
  if cfg is None:
140
248
  root = msg.get("project_root") or os.getcwd()
141
249
  model = msg.get("model") or os.environ.get("GEMCODE_MODEL") or ""
142
- from pathlib import Path
143
250
  cfg = GemCodeConfig(project_root=Path(str(root)), model=str(model))
144
251
  # Attach emitter + proposal mode flags (used by tool wrappers).
145
252
  object.__setattr__(cfg, "_ide_emitter", emitter)
@@ -151,7 +258,25 @@ async def run_stdio_loop() -> int:
151
258
 
152
259
  prompt = str(msg.get("prompt") or "")
153
260
  attachments = msg.get("attachments") if isinstance(msg.get("attachments"), list) else None
154
- full_prompt = _build_prompt(prompt, attachments)
261
+ att_dicts = [a for a in (attachments or []) if isinstance(a, dict)]
262
+ inline_paths, inline_err = prepare_inline_attachment_paths(attachments)
263
+ if inline_err:
264
+ for p in inline_paths:
265
+ try:
266
+ p.unlink()
267
+ except OSError:
268
+ pass
269
+ emitter.send(
270
+ make_response(
271
+ id=req_id,
272
+ ok=False,
273
+ error="; ".join(inline_err),
274
+ session=session_id,
275
+ )
276
+ )
277
+ continue
278
+
279
+ full_prompt = _build_prompt(prompt, _textual_attachments_only(att_dicts))
155
280
 
156
281
  # Per-turn allow flags (the engine still only proposes in IDE mode; the IDE applies).
157
282
  allow_write = _truthy(msg.get("allowWrite"), default=False)
@@ -161,14 +286,22 @@ async def run_stdio_loop() -> int:
161
286
 
162
287
  emitter.send(make_event(event="turn_start", id=req_id, session=session_id))
163
288
  try:
164
- events = await run_turn(
165
- runner,
166
- user_id="local",
167
- session_id=session_id,
168
- prompt=full_prompt,
169
- max_llm_calls=cfg.max_llm_calls,
170
- cfg=cfg,
171
- )
289
+ try:
290
+ events = await run_turn(
291
+ runner,
292
+ user_id="local",
293
+ session_id=session_id,
294
+ prompt=full_prompt,
295
+ max_llm_calls=cfg.max_llm_calls,
296
+ cfg=cfg,
297
+ attachment_paths=inline_paths if inline_paths else None,
298
+ )
299
+ finally:
300
+ for p in inline_paths:
301
+ try:
302
+ p.unlink()
303
+ except OSError:
304
+ pass
172
305
  except Exception as e:
173
306
  emitter.send(make_response(id=req_id, ok=False, error=f"{type(e).__name__}: {e}", session=session_id))
174
307
  continue
gemcode/invoke.py CHANGED
@@ -9,8 +9,9 @@ from __future__ import annotations
9
9
  import asyncio
10
10
  import os
11
11
  import sys
12
- from typing import Any
12
+ from pathlib import Path
13
13
  from threading import Lock
14
+ from typing import Any, Sequence
14
15
 
15
16
  from google.adk.agents.run_config import RunConfig
16
17
  from google.adk.runners import Runner
@@ -66,6 +67,7 @@ async def run_turn(
66
67
  prompt: str,
67
68
  max_llm_calls: int | None = None,
68
69
  cfg: "GemCodeConfig | None" = None,
70
+ attachment_paths: Sequence[Path | str] | None = None,
69
71
  ) -> list:
70
72
  """Execute one user message; collect all Events (caller aggregates text)."""
71
73
  # Dynamic risk score: updated each user message; later refined by tool outcomes.
@@ -86,6 +88,8 @@ async def run_turn(
86
88
  risk += 0.2
87
89
  if re.search(r"\\b(test|pytest|ci|build|deploy|release)\\b", p, re.I):
88
90
  risk += 0.1
91
+ if attachment_paths:
92
+ risk = min(1.0, risk + 0.12)
89
93
  # Multi-file hints
90
94
  if p.count("/") >= 6 or p.count(".py") + p.count(".ts") + p.count(".tsx") >= 3:
91
95
  risk += 0.1
@@ -167,10 +171,22 @@ async def run_turn(
167
171
 
168
172
  state_delta = token_budget_invocation_reset()
169
173
 
170
- # The first message is plain user text.
171
- current_message = types.Content(
172
- role="user", parts=[types.Part(text=prompt)]
173
- )
174
+ # First message: optional inline files + text (Gemini multimodal).
175
+ if attachment_paths:
176
+ from gemcode.multimodal_input import build_user_content
177
+
178
+ root = cfg.project_root if cfg is not None else Path.cwd()
179
+ current_message, attach_warn = build_user_content(
180
+ prompt,
181
+ attachment_paths,
182
+ project_root=root,
183
+ )
184
+ for w in attach_warn:
185
+ print(f"[gemcode] {w}", file=sys.stderr)
186
+ else:
187
+ current_message = types.Content(
188
+ role="user", parts=[types.Part(text=prompt)]
189
+ )
174
190
 
175
191
  async def _await_runner_events(
176
192
  *, next_message: types.Content, do_reset: bool
@@ -0,0 +1,144 @@
1
+ """
2
+ Build multimodal user Content (text + inline files) for Gemini.
3
+
4
+ Paths may be absolute or relative to the current working directory, then project root.
5
+ MIME types are inferred from the filename (``mimetypes``) and optionally from file
6
+ headers (PDF, common images, some audio/video) so PDFs and other Gemini-supported
7
+ types work—not only images.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import mimetypes
13
+ import os
14
+ from pathlib import Path
15
+ from typing import Sequence
16
+
17
+ from google.genai import types
18
+
19
+ _MAX_ATTACHMENTS = 16
20
+
21
+
22
+ def _max_attachment_bytes() -> int:
23
+ raw = os.environ.get("GEMCODE_MAX_ATTACHMENT_BYTES")
24
+ if raw:
25
+ try:
26
+ v = int(raw, 10)
27
+ if v > 0:
28
+ return v
29
+ except ValueError:
30
+ pass
31
+ return 20 * 1024 * 1024
32
+
33
+
34
+ def resolve_attachment_path(p: Path | str, *, project_root: Path) -> Path:
35
+ path = Path(p).expanduser()
36
+ if path.is_absolute():
37
+ return path.resolve()
38
+ cwd_try = (Path.cwd() / path).resolve()
39
+ if cwd_try.is_file():
40
+ return cwd_try
41
+ root_try = (project_root / path).resolve()
42
+ if root_try.is_file():
43
+ return root_try
44
+ return (Path.cwd() / path).resolve()
45
+
46
+
47
+ # Backward-compatible name from the images-only era.
48
+ resolve_image_path = resolve_attachment_path
49
+
50
+
51
+ def _sniff_mime(head: bytes) -> str | None:
52
+ if len(head) >= 5 and head[:5] == b"%PDF-":
53
+ return "application/pdf"
54
+ if len(head) >= 8 and head[:8] == b"\x89PNG\r\n\x1a\n":
55
+ return "image/png"
56
+ if len(head) >= 3 and head[:3] == b"\xff\xd8\xff":
57
+ return "image/jpeg"
58
+ if len(head) >= 6 and head[:6] in (b"GIF87a", b"GIF89a"):
59
+ return "image/gif"
60
+ if len(head) >= 12 and head[:4] == b"RIFF" and head[8:12] == b"WEBP":
61
+ return "image/webp"
62
+ if len(head) >= 2 and head[:2] == b"BM":
63
+ return "image/bmp"
64
+ if len(head) >= 12 and head[4:8] == b"ftyp":
65
+ return "video/mp4"
66
+ if len(head) >= 4 and head[:4] == b"\x1a\x45\xdf\xa3":
67
+ return "video/webm"
68
+ if len(head) >= 4 and head[:4] == b"OggS":
69
+ return "audio/ogg"
70
+ if len(head) >= 12 and head[:4] == b"RIFF" and head[8:12] == b"WAVE":
71
+ return "audio/wav"
72
+ if len(head) >= 4 and head[:4] == b"fLaC":
73
+ return "audio/flac"
74
+ if len(head) >= 3 and head[:3] in (b"ID3", b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"):
75
+ return "audio/mpeg"
76
+ return None
77
+
78
+
79
+ def _infer_mime(path: Path, data: bytes) -> tuple[str, list[str]]:
80
+ warnings: list[str] = []
81
+ head = data[:512]
82
+ guess, _ = mimetypes.guess_type(path.name, strict=False)
83
+ sniff = _sniff_mime(head)
84
+
85
+ if sniff and (not guess or guess == "application/octet-stream"):
86
+ return sniff, warnings
87
+ if guess and guess != "application/octet-stream":
88
+ return guess, warnings
89
+ if sniff:
90
+ return sniff, warnings
91
+ if guess:
92
+ return guess, warnings
93
+ warnings.append(
94
+ f"could not infer MIME type for {path}; using application/octet-stream "
95
+ "(Gemini may reject unsupported types)"
96
+ )
97
+ return "application/octet-stream", warnings
98
+
99
+
100
+ def build_user_content(
101
+ prompt: str,
102
+ attachment_paths: Sequence[Path | str] | None,
103
+ *,
104
+ project_root: Path,
105
+ ) -> tuple[types.Content, list[str]]:
106
+ """
107
+ Build ``Content`` with inline file parts first, then the text part.
108
+
109
+ Returns ``(content, warnings)`` — warnings are non-fatal skips or hints (stderr them).
110
+ """
111
+ warnings: list[str] = []
112
+ parts: list[types.Part] = []
113
+ max_b = _max_attachment_bytes()
114
+ if attachment_paths:
115
+ for raw in list(attachment_paths)[:_MAX_ATTACHMENTS]:
116
+ p = resolve_attachment_path(raw, project_root=project_root)
117
+ if not p.is_file():
118
+ warnings.append(f"attachment not found: {raw}")
119
+ continue
120
+ try:
121
+ size = p.stat().st_size
122
+ except OSError as e:
123
+ warnings.append(f"attachment stat failed {p}: {e}")
124
+ continue
125
+ if size > max_b:
126
+ warnings.append(
127
+ f"attachment too large ({size} bytes, max {max_b}): {p} "
128
+ "(set GEMCODE_MAX_ATTACHMENT_BYTES or use a smaller file)"
129
+ )
130
+ continue
131
+ try:
132
+ data = p.read_bytes()
133
+ except OSError as e:
134
+ warnings.append(f"attachment read failed {p}: {e}")
135
+ continue
136
+ mime, mw = _infer_mime(p, data)
137
+ warnings.extend(mw)
138
+ parts.append(types.Part(inline_data=types.Blob(data=data, mime_type=mime)))
139
+
140
+ text = (prompt or "").strip() or (
141
+ "(User attached file(s) only — describe or analyze them.)" if parts else ""
142
+ )
143
+ parts.append(types.Part(text=text))
144
+ return types.Content(role="user", parts=parts), warnings
gemcode/repl_commands.py CHANGED
@@ -227,7 +227,11 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
227
227
  ("exit", "Leave the REPL · /quit same"),
228
228
  ("help", "Short help · /? same"),
229
229
  ("hooks", "Post-turn hook configuration"),
230
+ ("attach", "Queue file(s) for next message (PDF, images, …) · /image /file /img · list · clear"),
230
231
  ("init", "Generate GEMINI.md project instructions"),
232
+ ("file", "Alias of /attach"),
233
+ ("image", "Alias of /attach (same queue)"),
234
+ ("img", "Alias of /attach"),
231
235
  ("kaira", "Background job scheduler — how to run gemcode kaira"),
232
236
  ("limits", "Execution limits (calls, context, …)"),
233
237
  ("live-audio", "How to run gemcode live-audio · /liveaudio same"),
@@ -320,6 +324,8 @@ def slash_help_lines() -> list[str]:
320
324
  " (CLI) gemcode login Save or change API key (~/.gemcode/credentials.json)",
321
325
  "",
322
326
  " Project setup:",
327
+ " /attach <path> Queue file(s) for the **next** message (PDF, images, …); /attach list|clear",
328
+ " Aliases: /image /img /file",
323
329
  " /trust Show workspace trust status (file/shell tools)",
324
330
  " /trust on|off Trust or revoke trust for this project root (~/.gemcode/trust.json)",
325
331
  " /init Analyze project structure and generate GEMINI.md",
gemcode/repl_slash.py CHANGED
@@ -29,6 +29,7 @@ from gemcode.repl_commands import (
29
29
  slash_help_lines,
30
30
  )
31
31
  from gemcode.curated_memory import load_snapshot as _curated_load_snapshot
32
+ from gemcode.multimodal_input import resolve_attachment_path
32
33
  from gemcode.slash_commands import parse_slash_command
33
34
  from gemcode.skills import discover_skill_metas, expand_skill_text, list_supporting_files, load_skill
34
35
  from gemcode.output_styles import discover_output_styles, load_output_style
@@ -87,6 +88,51 @@ async def process_repl_slash(
87
88
  out()
88
89
  return ReplSlashResult(skip_model_turn=True)
89
90
 
91
+ # ── /attach (queue files for the next user message: PDF, images, audio, …) ─
92
+ if name in ("attach", "file", "image", "img"):
93
+ raw_i = (sc.args or "").strip()
94
+ if not raw_i or raw_i.lower() in ("help", "?"):
95
+ out("Usage:")
96
+ out(" /attach <path> Queue a file for the **next** message (repeat for multiple, max 16).")
97
+ out(" /attach list Show queued paths")
98
+ out(" /attach clear Clear the queue")
99
+ out("Aliases: /file, /image, /img — same queue.")
100
+ out("Types: Gemini-supported MIME (e.g. images, PDF, audio, video, text). Default max ~20 MiB each.")
101
+ out("CLI: gemcode -C . --attach ./doc.pdf \"Summarize this\"")
102
+ out()
103
+ return ReplSlashResult(skip_model_turn=True)
104
+ low_i = raw_i.lower()
105
+ if low_i == "list":
106
+ pend = cfg.pending_attachment_paths
107
+ if not pend:
108
+ out("(no attachments queued)")
109
+ else:
110
+ out("Queued for next message:")
111
+ for i, p in enumerate(pend, 1):
112
+ out(f" {i}. {p}")
113
+ out()
114
+ return ReplSlashResult(skip_model_turn=True)
115
+ if low_i == "clear":
116
+ cfg.pending_attachment_paths.clear()
117
+ out("Attachment queue cleared.")
118
+ out()
119
+ return ReplSlashResult(skip_model_turn=True)
120
+ resolved = resolve_attachment_path(raw_i, project_root=cfg.project_root)
121
+ if not resolved.is_file():
122
+ out(f"Not a file: {raw_i}")
123
+ out("(Resolved relative to cwd, then project root.)")
124
+ out()
125
+ return ReplSlashResult(skip_model_turn=True)
126
+ if len(cfg.pending_attachment_paths) >= 16:
127
+ out("Queue full (16 files max). Use /attach clear first.")
128
+ out()
129
+ return ReplSlashResult(skip_model_turn=True)
130
+ cfg.pending_attachment_paths.append(resolved)
131
+ out(f"Queued: {resolved}")
132
+ out(f" ({len(cfg.pending_attachment_paths)} file(s) — send your next message to attach)")
133
+ out()
134
+ return ReplSlashResult(skip_model_turn=True)
135
+
90
136
  # ── /skills and /<skill-name> ──────────────────────────────────────────────
91
137
  if name in ("skills", "skill"):
92
138
  args = (sc.args or "").strip()
@@ -999,6 +1045,8 @@ async def process_repl_slash(
999
1045
  "3. Look at the source directory structure (src/, lib/, app/, etc.)\n"
1000
1046
  "4. Check for test directories and test runner config\n"
1001
1047
  "5. Look for linting/formatting config files (.eslintrc, .prettierrc, ruff.toml, etc.)\n\n"
1048
+ "Write **only** to `GEMINI.md` at the project root. Do **not** create "
1049
+ "`CLAUDE.md`, `AGENTS.md`, `.cursorrules`, or similar.\n\n"
1002
1050
  "Then write a GEMINI.md file at the project root containing:\n"
1003
1051
  "# Project Name\n"
1004
1052
  "One-sentence description.\n\n"
@@ -1073,6 +1121,8 @@ async def process_repl_slash(
1073
1121
  out(f"project_root: {cfg.project_root}")
1074
1122
  _lg = getattr(cfg, "session_loaded_skill_names", None) or []
1075
1123
  out(f"loaded_skills: {', '.join(_lg) if _lg else '(none)'} (/gemskill)")
1124
+ _pq = getattr(cfg, "pending_attachment_paths", None) or []
1125
+ out(f"queued_attachments: {len(_pq)} (/attach list)")
1076
1126
  out()
1077
1127
  out("Capabilities:")
1078
1128
  out(f" deep_research: {'on ✓' if cfg.enable_deep_research else 'off'}")
@@ -1178,6 +1228,7 @@ async def process_repl_slash(
1178
1228
  # /clear or /session new — start fresh session
1179
1229
  if name == "clear" or args_lower in ("new", "reset"):
1180
1230
  _clear_session_loaded_skills(cfg)
1231
+ cfg.pending_attachment_paths.clear()
1181
1232
  new_id = str(uuid.uuid4())
1182
1233
  out(f"new session_id: {new_id}")
1183
1234
  out()
@@ -1232,6 +1283,7 @@ async def process_repl_slash(
1232
1283
  out()
1233
1284
  return ReplSlashResult(skip_model_turn=True)
1234
1285
  _clear_session_loaded_skills(cfg)
1286
+ cfg.pending_attachment_paths.clear()
1235
1287
  out(f"Resuming session {found[:8]}…")
1236
1288
  out()
1237
1289
  return ReplSlashResult(
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:
gemcode/tui/scrollback.py CHANGED
@@ -12,6 +12,8 @@ from rich.console import Console
12
12
  from rich.markdown import Markdown as _RichMarkdown
13
13
  from rich.padding import Padding as _RichPadding
14
14
 
15
+ from gemcode.multimodal_input import build_user_content
16
+
15
17
  from gemcode.capability_routing import apply_capability_routing
16
18
  from gemcode.config import load_cli_environment
17
19
  from gemcode.model_routing import pick_effective_model
@@ -613,7 +615,18 @@ async def run_gemcode_scrollback_tui(
613
615
  from gemcode.session_runtime import create_runner as _create_runner_rt
614
616
  runner = _create_runner_rt(cfg, extra_tools=extra_tools)
615
617
 
616
- current_message = types.Content(role="user", parts=[types.Part(text=prompt)])
618
+ _attach = list(cfg.pending_attachment_paths)
619
+ cfg.pending_attachment_paths.clear()
620
+ if _attach:
621
+ current_message, _attach_warn = build_user_content(
622
+ prompt,
623
+ _attach,
624
+ project_root=cfg.project_root,
625
+ )
626
+ for w in _attach_warn:
627
+ print(f"[gemcode] {w}", file=sys.stderr)
628
+ else:
629
+ current_message = types.Content(role="user", parts=[types.Part(text=prompt)])
617
630
  do_reset = True
618
631
  def _normalize_ws(s: str) -> str:
619
632
  # Gemini can sometimes return identical content for both "thinking" and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.78
3
+ Version: 0.3.81
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -330,6 +330,18 @@ Non-interactive environments (CI, pipes) must set `GOOGLE_API_KEY` explicitly an
330
330
  | `gemcode kaira` | Stdin-line → queued jobs scheduler (see [Kaira](#kaira-scheduler)). |
331
331
  | `gemcode ide --stdio` | **JSONL IDE protocol** on stdin/stdout for editor extensions (hidden entry; used by VS Code). |
332
332
 
333
+ **Files with a prompt (multimodal):** attach one or more files Gemini can read (images, **PDF**, audio, video, plain text, etc.) for that message only:
334
+
335
+ ```bash
336
+ gemcode -C . --attach ./screenshot.png "Why does this UI look wrong?"
337
+ gemcode -C . --attach ./report.pdf "What are the main conclusions?"
338
+ gemcode -C . --image a.png --image b.png "Compare these two layouts"
339
+ ```
340
+
341
+ (`--image` is an alias of `--attach`.)
342
+
343
+ In the **REPL / TUI**, queue files for the **next** message: `/attach path/to.pdf` (repeat for multiple), then type your question. Use `/attach list`, `/attach clear`. Aliases: `/image`, `/img`, `/file`.
344
+
333
345
  ---
334
346
 
335
347
  ## Main CLI flags
@@ -349,6 +361,7 @@ Non-interactive environments (CI, pipes) must set `GOOGLE_API_KEY` explicitly an
349
361
  | `--tool-combination-mode` | Gemini 3 **tool context circulation**: `deep_research\|always\|never\|auto`. |
350
362
  | `--mcp` | Load MCP toolsets from `.gemcode/mcp.json` (requires `pip install -e ".[mcp]"`). |
351
363
  | `--max-llm-calls` | Cap model↔tool iterations (`RunConfig.max_llm_calls`). |
364
+ | `--attach PATH` | Attach file(s) for **this** CLI message only (repeat flag). Gemini-supported MIME (e.g. images, PDF, audio, video). Default max ~20 MiB each (`GEMCODE_MAX_ATTACHMENT_BYTES`). Alias: `--image`. |
352
365
 
353
366
  Kaira and `live-audio` accept overlapping options (project root, `--yes`, research/embeddings, etc.); run `gemcode kaira -h` / `gemcode live-audio -h` for full lists.
354
367
 
@@ -460,7 +473,7 @@ Tools are registered in `gemcode/tools/` and exposed to the model as ADK functio
460
473
  - **Computer use:** ADK `ComputerUseToolset` + Playwright (separate install and flags).
461
474
  - **MCP:** Tools loaded from configured servers.
462
475
 
463
- **Vendor file policy:** Writes to certain vendor-specific instruction filenames (e.g. `CLAUDE.md`, `AGENTS.md`) are blocked; use project conventions like `GEMINI.md` and curated memory files instead.
476
+ **Vendor file policy:** Writes to certain third-party instruction filenames (`CLAUDE.md`, `AGENTS.md`, `*.local` variants, `.cursorrules`, …) are blocked; use `GEMINI.md` and `.gemcode/notes.md` instead. The agent instruction always states this; `write_file` / `search_replace` enforce it.
464
477
 
465
478
  ---
466
479
 
@@ -472,6 +485,7 @@ In interactive mode, lines starting with `/` are **slash commands** (see `repl_c
472
485
 
473
486
  | Command | Purpose |
474
487
  |---------|---------|
488
+ | `/attach <path>`, `/attach list`, `/attach clear` | Queue file(s) for the **next** message (Gemini multimodal). Aliases: **`/image`**, **`/img`**, **`/file`**. |
475
489
  | `/trust`, `/trust on`, `/trust off` | Show, grant, or revoke **workspace trust** for the project root (stored in `~/.gemcode/trust.json`; required for file/shell/git tools). |
476
490
  | `/init` \| `/init force` | Analyze the repo and generate or overwrite `GEMINI.md`. |
477
491
  | `/cost` | Token usage and estimated cost for the session. |
@@ -1,15 +1,15 @@
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
7
7
  gemcode/callbacks.py,sha256=QR98bz-FeK2kp9N9JdzaeHRD2Ga2_rgz0XAPiYFafU0,31038
8
8
  gemcode/capability_routing.py,sha256=ZScRb2Bn41ZJUEmw_QaYYDVUPaNnbGBbK216tm5G4mI,3456
9
9
  gemcode/checkpoints.py,sha256=ptz2YW7P56mkqOrAf7-MXA0_Op3y4fgAg5ly_etuKvw,3936
10
- gemcode/cli.py,sha256=2R5pvpx3T8sUnfgbBRmkFjv5-uxCPeo_HuXMsaz23s8,28429
10
+ gemcode/cli.py,sha256=DX8Y2TaZA3Xjv3Pw5EEfexCufnFvGTnJ_DlAHDJoByo,29281
11
11
  gemcode/compaction.py,sha256=9YtA_qa23_8dHWVHx7AJwUduuI7jJQtq-m6sT8jgPWI,1186
12
- gemcode/config.py,sha256=OTJrLFIX-sr6kMWVI4l8rM2kJqkt3X2NcNFBEg6Nfdg,18042
12
+ 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
@@ -17,11 +17,11 @@ gemcode/curated_memory.py,sha256=5GfMS7JgSfEkAgnjFQle1Zy4VpoyKXI0osifnhMhZv8,321
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
20
- gemcode/ide_protocol.py,sha256=CDcrbB7u35-O_K7wgcGn2lPhL0GtSi22IxJk8CsmJPY,1669
21
- gemcode/ide_stdio.py,sha256=4Z5e8t3mKAiltQ3iVune4_ZtDfCRYPiPKkpWCr31k9w,6799
20
+ gemcode/ide_protocol.py,sha256=WJO4KdwyxjQcH1O_vTn7SPuy1ZZMm0eC8_xRLA9RYQo,2108
21
+ gemcode/ide_stdio.py,sha256=dL6xZCW0PV2OmnStmcSiF76P8n5ac5UrliKi8bkvTzA,10699
22
22
  gemcode/intent_classifier.py,sha256=YfRVEe8gHeKlRkjuSWef1bZ0MPBwyYMp5jymP5Vig5U,8507
23
23
  gemcode/interactions.py,sha256=B0b3QNE_I2i5_HtiebX4ehhjlc4Nbqjf_XbvcTLyJT0,641
24
- gemcode/invoke.py,sha256=j_AhfesXRA57-tBQ8i8zO53z7Ll3V_snr1nbJ-OnA3c,10262
24
+ gemcode/invoke.py,sha256=wyX39MHj_R_ttGVQGG7ORlcsTehUlpAJ6VMsDZ0qSD8,10856
25
25
  gemcode/kaira_daemon.py,sha256=Bzkpc96HocfYAV9D5skid_Gi4bJDOLgO5YlD8vbTgyY,6960
26
26
  gemcode/learning.py,sha256=o4Ivczm626NPRiNbSEb7-RvKJMefnv0ZpYt4UB2C3JA,3856
27
27
  gemcode/limits.py,sha256=3j6N8V643X7-nP-cAIf37Xg9bkGpQlEJB3nPptApQWk,2504
@@ -31,6 +31,7 @@ gemcode/mcp_loader.py,sha256=alipHTl5aA7ZCPG6Rq9cyy3UZLsdCra0CETb_fRJl_k,4964
31
31
  gemcode/modality_tools.py,sha256=tCcqY6Ca8a_kaO58GBC6OmrmLrrZs_jcKv6fTvt5esE,6879
32
32
  gemcode/model_errors.py,sha256=j1nb1dopJyZ6MQQvuuADBqvmcqdL80kQWACuWKMkP4Q,4185
33
33
  gemcode/model_routing.py,sha256=_8mnXNwxMPA8wAfl-Yx5lWNgjhyWYTCCCMGlqdGAw90,5174
34
+ gemcode/multimodal_input.py,sha256=FrfwcmqgDKnPDWxj76GOltl34D3PNQHxgbSR7ykL8SU,4450
34
35
  gemcode/openapi_loader.py,sha256=g_NZD8YL9_9iIJJ9qykhdbBrylJ1195A4FyHGC0mroc,4157
35
36
  gemcode/output_styles.py,sha256=29LSK3GGQLcodQhtXjSozgxCj7Z6VZZK8gIodOAC5EA,1910
36
37
  gemcode/paths.py,sha256=UQp4R4sUBv7HsM2OVoGlPxyOIOQZE5wpY53G6nHpB5Q,2671
@@ -39,8 +40,8 @@ gemcode/policy_profile.py,sha256=kcaKJQwLxAo3RjqfJJHl_G7B5GgTYKco0z3k5QcXsVY,386
39
40
  gemcode/pricing.py,sha256=lftp0SwyDqOzHqC2-6XzgZZhjif5PLdCe1Q3wY-p6kQ,3558
40
41
  gemcode/prompt_suggestions.py,sha256=RNEclxtoorRqu-wUlzuyUJ7OLFVOOryGOZBpbaCducI,2544
41
42
  gemcode/refine.py,sha256=BijEZ4Z32wGa9aK_WottyAhZF-j0xEqRg5UpjedNv2A,7653
42
- gemcode/repl_commands.py,sha256=iGF68JdJiwryoZ69EvLSNAg7BN-ivopctJqNWe4UjWk,18467
43
- gemcode/repl_slash.py,sha256=zRBFJYnaNfO3yAfHQ5S9X-OyiZSWDePoR62OV5LbG5M,83743
43
+ gemcode/repl_commands.py,sha256=XKGXLkA_KfQZubg9_oNjKdd2NXnxOMi8KiwVU8Lllvc,18892
44
+ gemcode/repl_slash.py,sha256=J3_5p02Tp95bm5aigujBZpbP48k68PBLu3a7Rh0FQlU,86082
44
45
  gemcode/review_agent.py,sha256=4t7_5-aE60b4-EheJ_eSB_H2eQYf9GppKoui6jw0TME,5264
45
46
  gemcode/rules.py,sha256=Itg02VpifOo6jqGj5xwna_ahaPPb0OVtaeR2cNI0pLE,3018
46
47
  gemcode/session_runtime.py,sha256=MQr34s6dw4bmk_VPSgBvkJTMwUBZQhvWDXl_iC5Pp6s,20867
@@ -76,7 +77,7 @@ gemcode/tools/__init__.py,sha256=PiVHym3WCkRCC3Y45y_bORnhplY91LaIOaloRYjHPRY,574
76
77
  gemcode/tools/bash.py,sha256=kedqaL2CU6O9WLPT83FToqJSaRBEGHesJSBH4pRLHlU,13649
77
78
  gemcode/tools/browser.py,sha256=StWRttiyGkR4qaG5urRviJgdoj2hiFB2OuHPItaVzJY,7250
78
79
  gemcode/tools/curated_memory.py,sha256=HgpZS81Ncvf0aoSiQ9zDdJdMTA_NWTL4bjLy34Z9a_A,1044
79
- gemcode/tools/edit.py,sha256=_n45v10YQyKKE2muLDIyeFImzFs0fRztG4xv-HOLnDw,7807
80
+ gemcode/tools/edit.py,sha256=XQbN9iZNiUmzieSDNMPiK-I3LWt5EkJdDPN9slyUJn0,7942
80
81
  gemcode/tools/filesystem.py,sha256=1GtLPOKzgjsGNoQA1UvjaUA2MDzm4RjhxUsolGm2ImA,8330
81
82
  gemcode/tools/notebook.py,sha256=SF7c-iBDz8heBRK2hERYq39tFeEixhP-1d0kIHBruW4,9208
82
83
  gemcode/tools/notes.py,sha256=y1UJOMKntDDB5e8bBPkVVMDfie3ZgKmaoO8r5Cc-owA,4448
@@ -92,7 +93,7 @@ gemcode/tools/todo.py,sha256=dlGfcNce1WsJ5Y9txrDL3SoF6Hv2rms9r1cvGPs6qIs,5798
92
93
  gemcode/tools/web.py,sha256=I-6-GgCVKblc9zVFfilWLHoJZfri7_pC2MpT52ZZarE,5078
93
94
  gemcode/tools/web_search.py,sha256=YqIvwAoOXK3TMqrrMVeSrg5Nyt7Ou5nRljrYQ4784_4,8816
94
95
  gemcode/tui/input_handler.py,sha256=Az8SbPaPHksIoibjph8gevMnfjagR1b-34_wpKbEhgQ,8259
95
- gemcode/tui/scrollback.py,sha256=BA_hjqdEggnpbq5TZtJbojB7sRRgZ7evQMEQEfeCKoU,33137
96
+ gemcode/tui/scrollback.py,sha256=1LcH39ZRiO2BFHWBFEcWZdWpe6-XFhUYWAePCyx91PI,33534
96
97
  gemcode/tui/spinner.py,sha256=dExs_enBPOWjkmRtodDzRw3E-MYh-xgtqDo54Q82sco,4892
97
98
  gemcode/tui/welcome_banner.py,sha256=aocl1lnoyLIM6RN4f65g3i0wRA71RqUlgPrGsXeVLW4,4387
98
99
  gemcode/tui/welcome_rich.py,sha256=VERTr2PB8hCdxNftJ827OdilFnnJMMDnqkaqzLne_lQ,4059
@@ -100,9 +101,9 @@ gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
100
101
  gemcode/web/sse_adapter.py,sha256=fXhKxn_bdJJUGqlmvkxLNSYL-ZiIZDaLHtQCF_BheRc,7108
101
102
  gemcode/web/terminal_repl.py,sha256=fQt895g0qcr6VBhXfv_5b_bsC5zHT5-MO0ysBdgi2Fg,3886
102
103
  gemcode/web/web_sse_compat.py,sha256=9A2s-GI7El7AotJqhO263FrLwppCXXkdydZ5EiOQbao,504
103
- gemcode-0.3.78.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
104
- gemcode-0.3.78.dist-info/METADATA,sha256=LEGVRi0Lfgz12djaXYe11PAzIEpce21QAIzpPQx3tvM,40109
105
- gemcode-0.3.78.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
106
- gemcode-0.3.78.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
107
- gemcode-0.3.78.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
108
- gemcode-0.3.78.dist-info/RECORD,,
104
+ gemcode-0.3.81.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
105
+ gemcode-0.3.81.dist-info/METADATA,sha256=eZVjHhg63IeTdAmmONikMGyM4X9xR4RbIF3vaMUfaF4,41198
106
+ gemcode-0.3.81.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
107
+ gemcode-0.3.81.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
108
+ gemcode-0.3.81.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
109
+ gemcode-0.3.81.dist-info/RECORD,,