gemcode 0.3.82__py3-none-any.whl → 0.3.85__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 +15 -1
- gemcode/callbacks.py +20 -0
- gemcode/cli.py +101 -0
- 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 +70 -12
- gemcode/session_runtime.py +15 -0
- gemcode/skills.py +51 -6
- gemcode/veomem_bridge.py +105 -0
- {gemcode-0.3.82.dist-info → gemcode-0.3.85.dist-info}/METADATA +8 -2
- {gemcode-0.3.82.dist-info → gemcode-0.3.85.dist-info}/RECORD +17 -16
- {gemcode-0.3.82.dist-info → gemcode-0.3.85.dist-info}/WHEEL +0 -0
- {gemcode-0.3.82.dist-info → gemcode-0.3.85.dist-info}/entry_points.txt +0 -0
- {gemcode-0.3.82.dist-info → gemcode-0.3.85.dist-info}/licenses/LICENSE +0 -0
- {gemcode-0.3.82.dist-info → gemcode-0.3.85.dist-info}/top_level.txt +0 -0
gemcode/agent.py
CHANGED
|
@@ -303,6 +303,20 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
303
303
|
except Exception:
|
|
304
304
|
curated_section = ""
|
|
305
305
|
|
|
306
|
+
# ── VeoMem recall (optional) ─────────────────────────────────────────────
|
|
307
|
+
veomem_section = ""
|
|
308
|
+
try:
|
|
309
|
+
t = getattr(cfg, "_veomem_wakeup_text", None)
|
|
310
|
+
if isinstance(t, str) and t.strip():
|
|
311
|
+
veomem_section = (
|
|
312
|
+
"\n\n## VeoMem recall (auto-captured, progressive)\n"
|
|
313
|
+
"This section is automatically generated from prior tool usage and summaries. "
|
|
314
|
+
"Treat it as helpful context; do not restate it verbatim to the user.\n"
|
|
315
|
+
f"{t.strip()}\n"
|
|
316
|
+
)
|
|
317
|
+
except Exception:
|
|
318
|
+
veomem_section = ""
|
|
319
|
+
|
|
306
320
|
return f"""## Runtime facts (authoritative for this session)
|
|
307
321
|
- **Today's date:** {today}
|
|
308
322
|
- **Project root** — every filesystem tool path is relative to: `{root}`
|
|
@@ -316,7 +330,7 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
316
330
|
{kaira_section}
|
|
317
331
|
- **UI banner** phrases like "GemCode Pro" are terminal marketing, not a separate API tier.
|
|
318
332
|
- **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}"""
|
|
333
|
+
- **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
334
|
|
|
321
335
|
|
|
322
336
|
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
|
|
@@ -262,6 +263,100 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
262
263
|
"GemCode CLI is running. Type your prompt and press Enter. (Ctrl+D to exit)",
|
|
263
264
|
file=sys.stderr,
|
|
264
265
|
)
|
|
266
|
+
|
|
267
|
+
def _looks_like_new_skill_request(s: str) -> bool:
|
|
268
|
+
t = (s or "").strip().lower()
|
|
269
|
+
if not t or t.startswith("/"):
|
|
270
|
+
return False
|
|
271
|
+
# Natural language trigger: "make/create/build a new skill/gemskill"
|
|
272
|
+
return bool(
|
|
273
|
+
re.search(r"\b(new|create|make|build)\b", t)
|
|
274
|
+
and re.search(r"\b(gem\s*skill|gemskill|skill)\b", t)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def _prompt_nonempty(label: str, default: str | None = None) -> str:
|
|
278
|
+
while True:
|
|
279
|
+
try:
|
|
280
|
+
v = input(label).strip()
|
|
281
|
+
except EOFError:
|
|
282
|
+
return default or ""
|
|
283
|
+
if v:
|
|
284
|
+
return v
|
|
285
|
+
if default is not None:
|
|
286
|
+
return default
|
|
287
|
+
|
|
288
|
+
def _wizard_create_gemskill() -> str | None:
|
|
289
|
+
"""
|
|
290
|
+
Interactive CLI wizard that collects a GemSkill spec, then returns a single
|
|
291
|
+
model prompt instructing the agent to generate the skill folder/files.
|
|
292
|
+
"""
|
|
293
|
+
if not (hasattr(sys.stdin, "isatty") and sys.stdin.isatty()):
|
|
294
|
+
return None
|
|
295
|
+
print("\n── GemSkill wizard ──", file=sys.stderr)
|
|
296
|
+
print("We'll create a new skill under `.gemcode/skills/<name>/`.\n", file=sys.stderr)
|
|
297
|
+
name = _prompt_nonempty("skill name (kebab-case, e.g. api-review): ")
|
|
298
|
+
name = (name or "").strip().lower()
|
|
299
|
+
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", name):
|
|
300
|
+
print("Invalid name. Use lowercase letters/numbers/hyphens (max 64 chars).", file=sys.stderr)
|
|
301
|
+
return None
|
|
302
|
+
desc = _prompt_nonempty("one-line description: ")
|
|
303
|
+
when = _prompt_nonempty("when should it be used (1-2 sentences)? ")
|
|
304
|
+
inputs = _prompt_nonempty("inputs it should accept (bullets or short text): ", default="User request + $ARGUMENTS")
|
|
305
|
+
outputs = _prompt_nonempty("expected output format (short): ", default="Concise checklist + actionable steps")
|
|
306
|
+
tools_pref = _prompt_nonempty(
|
|
307
|
+
"tools: (a)uto, (r)ead-only, (w)eb-research-heavy? [a]: ",
|
|
308
|
+
default="a",
|
|
309
|
+
).strip().lower()
|
|
310
|
+
use_web = tools_pref.startswith("w")
|
|
311
|
+
read_only = tools_pref.startswith("r")
|
|
312
|
+
examples = _prompt_nonempty("example command(s) user will type (optional): ", default="")
|
|
313
|
+
|
|
314
|
+
root = cfg.project_root.resolve()
|
|
315
|
+
skill_dir = root / ".gemcode" / "skills" / name
|
|
316
|
+
skill_md = skill_dir / "SKILL.md"
|
|
317
|
+
|
|
318
|
+
prompt_parts: list[str] = [
|
|
319
|
+
"You are creating a **GemCode GemSkill**. Generate a new skill folder and files.\n\n",
|
|
320
|
+
"## Target\n",
|
|
321
|
+
f"- Project root: {root}\n",
|
|
322
|
+
f"- Skill directory: {skill_dir}\n",
|
|
323
|
+
f"- Primary file: {skill_md}\n\n",
|
|
324
|
+
"## Requirements\n",
|
|
325
|
+
f"- Skill name: {name}\n",
|
|
326
|
+
f"- Description: {desc}\n",
|
|
327
|
+
f"- When to use: {when}\n",
|
|
328
|
+
f"- Inputs: {inputs}\n",
|
|
329
|
+
f"- Output expectations: {outputs}\n",
|
|
330
|
+
]
|
|
331
|
+
if examples:
|
|
332
|
+
prompt_parts.append(f"- Example invocations: {examples}\n")
|
|
333
|
+
prompt_parts.extend(
|
|
334
|
+
[
|
|
335
|
+
"\n",
|
|
336
|
+
"- Write `SKILL.md` with YAML frontmatter using **multiline-friendly** fields when needed.\n",
|
|
337
|
+
"- Include: Purpose, When to use, When NOT to use (guardrails), Inputs, Output format, Workflow, Examples.\n",
|
|
338
|
+
"- Make it **token-efficient**: prefer short checklists and explicit decision gates.\n",
|
|
339
|
+
"- Avoid vague 'ALWAYS trigger' language; provide precise triggers.\n",
|
|
340
|
+
"- If you need templates/checklists, create supporting files in a `references/` subfolder and keep them small.\n",
|
|
341
|
+
"- Do not create CLAUDE.md/AGENTS.md or other vendor-specific files.\n\n",
|
|
342
|
+
"## Tooling / research policy\n",
|
|
343
|
+
(
|
|
344
|
+
"- You MAY use web research to find best practices, but only if it materially improves the skill.\n"
|
|
345
|
+
if use_web
|
|
346
|
+
else "- Avoid web research unless strictly necessary.\n"
|
|
347
|
+
),
|
|
348
|
+
("- Operate in read-only mode: do not write files.\n" if read_only else "- You are allowed to write the skill files.\n"),
|
|
349
|
+
"\n",
|
|
350
|
+
"## Execution steps\n",
|
|
351
|
+
"1. Create the skill directory if missing.\n",
|
|
352
|
+
"2. Write `SKILL.md` (and any supporting files).\n",
|
|
353
|
+
"3. Validate the YAML frontmatter parses and the skill is usable.\n",
|
|
354
|
+
f"4. Print a short confirmation: created files + how to invoke (e.g. `/skills list`, `/{name} ...`, `/gemskill {name}`).\n",
|
|
355
|
+
]
|
|
356
|
+
)
|
|
357
|
+
prompt = "".join(prompt_parts)
|
|
358
|
+
return prompt
|
|
359
|
+
|
|
265
360
|
while True:
|
|
266
361
|
try:
|
|
267
362
|
raw = input("> ")
|
|
@@ -274,6 +369,12 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
274
369
|
if prompt_text in (":q", "quit", "exit", "/exit"):
|
|
275
370
|
break
|
|
276
371
|
|
|
372
|
+
# Natural language shortcut: "I want to make a new skill" => wizard.
|
|
373
|
+
if _looks_like_new_skill_request(prompt_text):
|
|
374
|
+
wizard_prompt = _wizard_create_gemskill()
|
|
375
|
+
if wizard_prompt:
|
|
376
|
+
prompt_text = wizard_prompt
|
|
377
|
+
|
|
277
378
|
cfg.session_skill_expand_session_id = session_id
|
|
278
379
|
slash = await process_repl_slash(
|
|
279
380
|
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) ────────────────
|
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:
|
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
|
|
gemcode/veomem_bridge.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _enabled() -> bool:
|
|
10
|
+
return os.environ.get("GEMCODE_VEOMEM", "").strip().lower() in ("1", "true", "yes", "on")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _try_import():
|
|
14
|
+
try:
|
|
15
|
+
from veomem.store import add_observation # type: ignore[import-not-found]
|
|
16
|
+
return add_observation
|
|
17
|
+
except Exception:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _summarize_tool_result(result: dict[str, Any]) -> str:
|
|
22
|
+
if not isinstance(result, dict):
|
|
23
|
+
return ""
|
|
24
|
+
if result.get("error"):
|
|
25
|
+
e = str(result.get("error"))
|
|
26
|
+
return f"error: {e[:800]}"
|
|
27
|
+
parts: list[str] = []
|
|
28
|
+
for k in ("exit_code", "path", "backup_path", "count", "chars_before", "chars_after"):
|
|
29
|
+
if k in result:
|
|
30
|
+
parts.append(f"{k}={result.get(k)!r}")
|
|
31
|
+
for k in ("stdout", "stderr"):
|
|
32
|
+
v = result.get(k)
|
|
33
|
+
if isinstance(v, str) and v.strip():
|
|
34
|
+
parts.append(f"{k}={v.strip()[:200]}{'…' if len(v) > 200 else ''}")
|
|
35
|
+
return " ".join(parts).strip()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def record_tool_use(
|
|
39
|
+
project_root: Path,
|
|
40
|
+
*,
|
|
41
|
+
session_id: str | None,
|
|
42
|
+
tool_name: str,
|
|
43
|
+
args: dict[str, Any],
|
|
44
|
+
result: dict[str, Any],
|
|
45
|
+
paths: list[str] | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
if not _enabled():
|
|
48
|
+
return
|
|
49
|
+
add_observation = _try_import()
|
|
50
|
+
if add_observation is None:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
touched = list(paths or [])
|
|
54
|
+
# Heuristic: record read_file path if present.
|
|
55
|
+
try:
|
|
56
|
+
p = (args or {}).get("path")
|
|
57
|
+
if isinstance(p, str) and p.strip():
|
|
58
|
+
touched.append(p.strip())
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
touched = list(dict.fromkeys([p for p in touched if isinstance(p, str) and p.strip()]))[:50]
|
|
62
|
+
|
|
63
|
+
text = _summarize_tool_result(result)
|
|
64
|
+
if not text:
|
|
65
|
+
# Keep small but non-empty to be searchable.
|
|
66
|
+
text = json.dumps({"ok": not bool(result.get("error")), "tool": tool_name}, ensure_ascii=False)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
add_observation(
|
|
70
|
+
project_root,
|
|
71
|
+
kind="tool",
|
|
72
|
+
title=tool_name,
|
|
73
|
+
text=text,
|
|
74
|
+
session_id=session_id,
|
|
75
|
+
tool_name=tool_name,
|
|
76
|
+
paths=touched,
|
|
77
|
+
extra={"args_keys": sorted(list((args or {}).keys()))[:40]},
|
|
78
|
+
)
|
|
79
|
+
except Exception:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def record_turn_summary(project_root: Path, *, session_id: str | None, text: str) -> None:
|
|
84
|
+
if not _enabled():
|
|
85
|
+
return
|
|
86
|
+
add_observation = _try_import()
|
|
87
|
+
if add_observation is None:
|
|
88
|
+
return
|
|
89
|
+
t = (text or "").strip()
|
|
90
|
+
if not t:
|
|
91
|
+
return
|
|
92
|
+
try:
|
|
93
|
+
add_observation(
|
|
94
|
+
project_root,
|
|
95
|
+
kind="summary",
|
|
96
|
+
title="turn_summary",
|
|
97
|
+
text=t[:8000],
|
|
98
|
+
session_id=session_id,
|
|
99
|
+
tool_name=None,
|
|
100
|
+
paths=[],
|
|
101
|
+
extra={},
|
|
102
|
+
)
|
|
103
|
+
except Exception:
|
|
104
|
+
return
|
|
105
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemcode
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.85
|
|
4
4
|
Summary: Local-first coding agent on Google Gemini + ADK
|
|
5
5
|
Author: GemCode Contributors
|
|
6
6
|
License: Apache License
|
|
@@ -574,10 +574,16 @@ The TUI (when `GEMCODE_TUI=1` and terminal supports it) provides **slash complet
|
|
|
574
574
|
- **Discovery:** Only **metadata** (name + description) is preloaded into context for token efficiency. Full body loads **on demand** via `/skill <name>`, `/<name>`, or tools `load_skill` / `list_skills`.
|
|
575
575
|
- **Built-in:** **`batch`** — parallel large-change workflow (map → units → `run_subtask` → verify). Exposed as `/batch <goal>`; not auto-invoked by the model (`disable_model_invocation`).
|
|
576
576
|
|
|
577
|
-
- **`/create gemskill <name>`** — create a new skill directory.
|
|
577
|
+
- **`/create gemskill <name>`** — create a new skill directory (scaffold).
|
|
578
|
+
- **Interactive wizard (CLI)**: if you type something like “I want to make a new skill” in the REPL, GemCode will switch into a short **GemSkill wizard** (name/description/spec) and then generate the full skill folder for you.
|
|
578
579
|
- **`/gemskill <name>`** — pin the full skill body into the system prompt for this session.
|
|
579
580
|
- **`/append gemskill <name> <text>`** — one-shot turn for the model to revise that skill on disk.
|
|
580
581
|
|
|
582
|
+
### Frontmatter notes (compatibility)
|
|
583
|
+
GemCode supports common YAML frontmatter styles including multi-line scalars like:
|
|
584
|
+
- `description: >` (folded)
|
|
585
|
+
- `description: |` (literal)
|
|
586
|
+
|
|
581
587
|
---
|
|
582
588
|
|
|
583
589
|
## Curated memory
|
|
@@ -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=
|
|
3
|
+
gemcode/agent.py,sha256=Q6sm16a6nFg6tG4fF_IvhxmR-WXawuIWKCciXHDGPmc,57904
|
|
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
|
-
gemcode/callbacks.py,sha256=
|
|
7
|
+
gemcode/callbacks.py,sha256=aTS7HAIsWNI1TJawscU_bMLZtHOKgsfjzx1sJHXcXsA,31801
|
|
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=
|
|
10
|
+
gemcode/cli.py,sha256=qkw5tT6lrUiH9aIrlb0zMPSXvOvluPixnmMiRPN8mq8,33924
|
|
11
11
|
gemcode/compaction.py,sha256=9YtA_qa23_8dHWVHx7AJwUduuI7jJQtq-m6sT8jgPWI,1186
|
|
12
|
-
gemcode/config.py,sha256=
|
|
12
|
+
gemcode/config.py,sha256=JKOtOlQ2TGqVo_4HfemWJc3_TKa3nkjl-dJjeNdS6gc,18342
|
|
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
|
|
@@ -18,7 +18,7 @@ gemcode/dynamic_policy.py,sha256=nWgBN6ffSn1Te4aDI1MURxRaQjTclzIuSf4KaAskE9U,466
|
|
|
18
18
|
gemcode/hitl_session.py,sha256=oNiI7odFJGUcmqPavjKLJOEumZKrgklLvwjjrIG9GPg,281
|
|
19
19
|
gemcode/hooks.py,sha256=tAzzZgfALC-nqSGoUdnEvHyGJNM2iTqKN5Yrfm8wFo8,6350
|
|
20
20
|
gemcode/ide_protocol.py,sha256=WJO4KdwyxjQcH1O_vTn7SPuy1ZZMm0eC8_xRLA9RYQo,2108
|
|
21
|
-
gemcode/ide_stdio.py,sha256=
|
|
21
|
+
gemcode/ide_stdio.py,sha256=qDZ8qCR0kWipvyxLJ3tbZfAXChZtosi46dLeNuMejFk,11066
|
|
22
22
|
gemcode/intent_classifier.py,sha256=YfRVEe8gHeKlRkjuSWef1bZ0MPBwyYMp5jymP5Vig5U,8507
|
|
23
23
|
gemcode/interactions.py,sha256=B0b3QNE_I2i5_HtiebX4ehhjlc4Nbqjf_XbvcTLyJT0,641
|
|
24
24
|
gemcode/invoke.py,sha256=wyX39MHj_R_ttGVQGG7ORlcsTehUlpAJ6VMsDZ0qSD8,10856
|
|
@@ -41,13 +41,13 @@ gemcode/pricing.py,sha256=lftp0SwyDqOzHqC2-6XzgZZhjif5PLdCe1Q3wY-p6kQ,3558
|
|
|
41
41
|
gemcode/prompt_suggestions.py,sha256=RNEclxtoorRqu-wUlzuyUJ7OLFVOOryGOZBpbaCducI,2544
|
|
42
42
|
gemcode/query_sanitizer.py,sha256=KqXo5U7igNSgOAH4YpyANlgS--1WnZEdpO4AU4SNQUs,2746
|
|
43
43
|
gemcode/refine.py,sha256=BijEZ4Z32wGa9aK_WottyAhZF-j0xEqRg5UpjedNv2A,7653
|
|
44
|
-
gemcode/repl_commands.py,sha256=
|
|
45
|
-
gemcode/repl_slash.py,sha256=
|
|
44
|
+
gemcode/repl_commands.py,sha256=x9LQVthWPrbwB3QhUakmIg4puHEnS1Vuax4yKtOLU2Y,19315
|
|
45
|
+
gemcode/repl_slash.py,sha256=Hbc72fPms3MxmfLcWc44tdQqZiQHxVm11ulDuS9fi_A,91654
|
|
46
46
|
gemcode/review_agent.py,sha256=4t7_5-aE60b4-EheJ_eSB_H2eQYf9GppKoui6jw0TME,5264
|
|
47
47
|
gemcode/rules.py,sha256=Itg02VpifOo6jqGj5xwna_ahaPPb0OVtaeR2cNI0pLE,3018
|
|
48
|
-
gemcode/session_runtime.py,sha256=
|
|
48
|
+
gemcode/session_runtime.py,sha256=Spv2j1MQVYcb3nQKXFVHlB3vLtyujfjP6Fy7g62Qqi0,21614
|
|
49
49
|
gemcode/session_store.py,sha256=POUT_QQf715c74jbXj0s5vCd4dlAgJz_CLsIWuEUoO0,6051
|
|
50
|
-
gemcode/skills.py,sha256=
|
|
50
|
+
gemcode/skills.py,sha256=nnrzYUCiuEkU_i57p_jJpPHRfw1t2t2EA3pJHNqvpzw,12554
|
|
51
51
|
gemcode/slash_commands.py,sha256=bcD-S_H7p7AlTli6g2dLPPG46HejPje0Imb3ScDTCaQ,798
|
|
52
52
|
gemcode/thinking.py,sha256=-1TVkOMG-7CSQN0Mc18EqINkUxWOMBgeTlF7CX9zYL4,4641
|
|
53
53
|
gemcode/tool_prompt_manifest.py,sha256=BbM88yj6hN5CTDBzyHa7DZ1S6gpzIZQe9G7dFqkMmtA,8681
|
|
@@ -55,6 +55,7 @@ gemcode/tool_registry.py,sha256=DqHshoOG_8U5Ijey407DFxp3xYq3bVBLwUVxFgGgELs,1799
|
|
|
55
55
|
gemcode/tool_result_store.py,sha256=50ufSP03F1D8mWFX7HFimTVh-fMWpshOtw4AP-qf-cU,6351
|
|
56
56
|
gemcode/tools_inspector.py,sha256=ZAuD1oVIsZvyD_vrzaWBws2ezpFT6cIsU_w4yc_1PPA,4086
|
|
57
57
|
gemcode/trust.py,sha256=kZi1dll1T8RUZtSY42WbsEh-ZIkMRup_IZPfYlJMbw4,1457
|
|
58
|
+
gemcode/veomem_bridge.py,sha256=jqGeej3itMKgWQr5CZN_frq3gxGu9sQIb0e0CRK5qVU,2574
|
|
58
59
|
gemcode/version.py,sha256=uwynYS-RmK8CDoqGtt8976kFkJv0zELkEAlwebnp_io,380
|
|
59
60
|
gemcode/vertex.py,sha256=Fy8zxuU8jWkObt0WDRI0XmgnjNznILXVLVwKjImNz9Q,643
|
|
60
61
|
gemcode/wal.py,sha256=moUldC__j0YmuNhVuawIIDYSoIgZ9_HpwKFvU9vieq8,1645
|
|
@@ -66,7 +67,7 @@ gemcode/memory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
66
67
|
gemcode/memory/embedding_memory_service.py,sha256=4iMZUw80GY8SPrJcuT4CwOsTmZ600SOuYkm-nv2c634,9422
|
|
67
68
|
gemcode/memory/file_memory_service.py,sha256=yAXCspfSPBfQXDrwPX8ZZsqaprnC_CKvgmN_GiQVzuo,6092
|
|
68
69
|
gemcode/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
69
|
-
gemcode/plugins/terminal_hooks_plugin.py,sha256=
|
|
70
|
+
gemcode/plugins/terminal_hooks_plugin.py,sha256=gA8UzB7AZ4WxrDMa4i0hN9Ds3pggScfkWWCN62vO_60,7310
|
|
70
71
|
gemcode/plugins/tool_recovery_plugin.py,sha256=mJR5G_O8vXgXFlC3RRGyxLAPDXDoCcakm-nVf-Tdj5o,3866
|
|
71
72
|
gemcode/query/__init__.py,sha256=As7rCwRMOYk-03fLPodM7XDZ25sUP-F-v8StYMiONnE,1247
|
|
72
73
|
gemcode/query/config.py,sha256=TP4vffDR-gP-4tYHAKXxSD8QjOYb8ctUfFGnpQ2oem0,932
|
|
@@ -104,9 +105,9 @@ gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
|
|
|
104
105
|
gemcode/web/sse_adapter.py,sha256=fXhKxn_bdJJUGqlmvkxLNSYL-ZiIZDaLHtQCF_BheRc,7108
|
|
105
106
|
gemcode/web/terminal_repl.py,sha256=fQt895g0qcr6VBhXfv_5b_bsC5zHT5-MO0ysBdgi2Fg,3886
|
|
106
107
|
gemcode/web/web_sse_compat.py,sha256=9A2s-GI7El7AotJqhO263FrLwppCXXkdydZ5EiOQbao,504
|
|
107
|
-
gemcode-0.3.
|
|
108
|
-
gemcode-0.3.
|
|
109
|
-
gemcode-0.3.
|
|
110
|
-
gemcode-0.3.
|
|
111
|
-
gemcode-0.3.
|
|
112
|
-
gemcode-0.3.
|
|
108
|
+
gemcode-0.3.85.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
|
|
109
|
+
gemcode-0.3.85.dist-info/METADATA,sha256=h4YenRMKe4flImBOdRIoTgn3yiS_bFDCw_if4D1_mSs,41998
|
|
110
|
+
gemcode-0.3.85.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
111
|
+
gemcode-0.3.85.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
|
|
112
|
+
gemcode-0.3.85.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
|
|
113
|
+
gemcode-0.3.85.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|