gemcode 0.3.82__tar.gz → 0.3.85__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. {gemcode-0.3.82/src/gemcode.egg-info → gemcode-0.3.85}/PKG-INFO +8 -2
  2. {gemcode-0.3.82 → gemcode-0.3.85}/README.md +7 -1
  3. {gemcode-0.3.82 → gemcode-0.3.85}/pyproject.toml +1 -1
  4. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/agent.py +15 -1
  5. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/callbacks.py +20 -0
  6. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/cli.py +101 -0
  7. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/config.py +3 -1
  8. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/ide_stdio.py +7 -1
  9. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/plugins/terminal_hooks_plugin.py +31 -0
  10. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/repl_commands.py +1 -0
  11. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/repl_slash.py +70 -12
  12. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/session_runtime.py +15 -0
  13. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/skills.py +51 -6
  14. gemcode-0.3.85/src/gemcode/veomem_bridge.py +105 -0
  15. {gemcode-0.3.82 → gemcode-0.3.85/src/gemcode.egg-info}/PKG-INFO +8 -2
  16. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode.egg-info/SOURCES.txt +1 -0
  17. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_model_routing.py +4 -4
  18. {gemcode-0.3.82 → gemcode-0.3.85}/LICENSE +0 -0
  19. {gemcode-0.3.82 → gemcode-0.3.85}/MANIFEST.in +0 -0
  20. {gemcode-0.3.82 → gemcode-0.3.85}/setup.cfg +0 -0
  21. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/__init__.py +0 -0
  22. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/__main__.py +0 -0
  23. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/audit.py +0 -0
  24. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/autocompact.py +0 -0
  25. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/autotune.py +0 -0
  26. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/capability_routing.py +0 -0
  27. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/checkpoints.py +0 -0
  28. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/compaction.py +0 -0
  29. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/computer_use/__init__.py +0 -0
  30. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/computer_use/browser_computer.py +0 -0
  31. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/context_budget.py +0 -0
  32. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/context_warning.py +0 -0
  33. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/credentials.py +0 -0
  34. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/curated_memory.py +0 -0
  35. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/dynamic_policy.py +0 -0
  36. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/evals/harness.py +0 -0
  37. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/hitl_session.py +0 -0
  38. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/hooks.py +0 -0
  39. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/ide_protocol.py +0 -0
  40. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/intent_classifier.py +0 -0
  41. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/interactions.py +0 -0
  42. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/invoke.py +0 -0
  43. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/kaira_daemon.py +0 -0
  44. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/learning.py +0 -0
  45. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/limits.py +0 -0
  46. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/live_audio_engine.py +0 -0
  47. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/logging_config.py +0 -0
  48. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/mcp_loader.py +0 -0
  49. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/memory/__init__.py +0 -0
  50. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/memory/embedding_memory_service.py +0 -0
  51. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/memory/file_memory_service.py +0 -0
  52. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/modality_tools.py +0 -0
  53. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/model_errors.py +0 -0
  54. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/model_routing.py +0 -0
  55. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/multimodal_input.py +0 -0
  56. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/openapi_loader.py +0 -0
  57. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/output_styles.py +0 -0
  58. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/paths.py +0 -0
  59. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/permissions.py +0 -0
  60. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/plugins/__init__.py +0 -0
  61. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  62. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/policy_profile.py +0 -0
  63. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/pricing.py +0 -0
  64. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/prompt_suggestions.py +0 -0
  65. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/query/__init__.py +0 -0
  66. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/query/config.py +0 -0
  67. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/query/deps.py +0 -0
  68. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/query/engine.py +0 -0
  69. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/query/stop_hooks.py +0 -0
  70. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/query/token_budget.py +0 -0
  71. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/query/transitions.py +0 -0
  72. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/query_sanitizer.py +0 -0
  73. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/refine.py +0 -0
  74. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/review_agent.py +0 -0
  75. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/rules.py +0 -0
  76. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/session_store.py +0 -0
  77. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/slash_commands.py +0 -0
  78. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/thinking.py +0 -0
  79. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tool_prompt_manifest.py +0 -0
  80. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tool_registry.py +0 -0
  81. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tool_result_store.py +0 -0
  82. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/__init__.py +0 -0
  83. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/bash.py +0 -0
  84. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/browser.py +0 -0
  85. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/compress_memory.py +0 -0
  86. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/curated_memory.py +0 -0
  87. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/edit.py +0 -0
  88. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/filesystem.py +0 -0
  89. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/notebook.py +0 -0
  90. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/notes.py +0 -0
  91. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/repo_map.py +0 -0
  92. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/search.py +0 -0
  93. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/shell.py +0 -0
  94. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/shell_gate.py +0 -0
  95. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/skills.py +0 -0
  96. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/subtask.py +0 -0
  97. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/tasks.py +0 -0
  98. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/think.py +0 -0
  99. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/todo.py +0 -0
  100. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/web.py +0 -0
  101. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools/web_search.py +0 -0
  102. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tools_inspector.py +0 -0
  103. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/trust.py +0 -0
  104. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tui/input_handler.py +0 -0
  105. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tui/scrollback.py +0 -0
  106. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tui/spinner.py +0 -0
  107. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tui/welcome_banner.py +0 -0
  108. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/tui/welcome_rich.py +0 -0
  109. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/version.py +0 -0
  110. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/vertex.py +0 -0
  111. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/wal.py +0 -0
  112. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/web/__init__.py +0 -0
  113. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/web/sse_adapter.py +0 -0
  114. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/web/terminal_repl.py +0 -0
  115. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/web/web_sse_compat.py +0 -0
  116. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode/workspace_hints.py +0 -0
  117. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode.egg-info/dependency_links.txt +0 -0
  118. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode.egg-info/entry_points.txt +0 -0
  119. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode.egg-info/requires.txt +0 -0
  120. {gemcode-0.3.82 → gemcode-0.3.85}/src/gemcode.egg-info/top_level.txt +0 -0
  121. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_add_dir.py +0 -0
  122. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_agent_instruction.py +0 -0
  123. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_autocompact.py +0 -0
  124. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_capability_routing.py +0 -0
  125. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_checkpoint_diff_command.py +0 -0
  126. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_cli_init.py +0 -0
  127. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_compress_memory_tool.py +0 -0
  128. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_computer_use_permissions.py +0 -0
  129. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_context_budget.py +0 -0
  130. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_context_warning.py +0 -0
  131. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_credentials.py +0 -0
  132. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_eval_harness_layout.py +0 -0
  133. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_ide_stdio_attachments.py +0 -0
  134. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_interactive_permission_ask.py +0 -0
  135. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_kaira_scheduler.py +0 -0
  136. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_modality_tools.py +0 -0
  137. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_model_error_retry.py +0 -0
  138. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_model_errors.py +0 -0
  139. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_multimodal_input.py +0 -0
  140. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_output_styles_and_rules.py +0 -0
  141. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_paths.py +0 -0
  142. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_permissions.py +0 -0
  143. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_prompt_suggestions.py +0 -0
  144. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_repl_commands.py +0 -0
  145. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_repl_slash.py +0 -0
  146. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_skills.py +0 -0
  147. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_slash_commands.py +0 -0
  148. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_slash_completion_registry.py +0 -0
  149. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_thinking_config.py +0 -0
  150. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_token_budget.py +0 -0
  151. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_tool_context_circulation.py +0 -0
  152. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_tools.py +0 -0
  153. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_tools_inspector.py +0 -0
  154. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_web_sse_adapter.py +0 -0
  155. {gemcode-0.3.82 → gemcode-0.3.85}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.82
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
@@ -385,10 +385,16 @@ The TUI (when `GEMCODE_TUI=1` and terminal supports it) provides **slash complet
385
385
  - **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`.
386
386
  - **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`).
387
387
 
388
- - **`/create gemskill <name>`** — create a new skill directory.
388
+ - **`/create gemskill <name>`** — create a new skill directory (scaffold).
389
+ - **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.
389
390
  - **`/gemskill <name>`** — pin the full skill body into the system prompt for this session.
390
391
  - **`/append gemskill <name> <text>`** — one-shot turn for the model to revise that skill on disk.
391
392
 
393
+ ### Frontmatter notes (compatibility)
394
+ GemCode supports common YAML frontmatter styles including multi-line scalars like:
395
+ - `description: >` (folded)
396
+ - `description: |` (literal)
397
+
392
398
  ---
393
399
 
394
400
  ## Curated memory
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.82"
7
+ version = "0.3.85"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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:
@@ -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",
@@ -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,
@@ -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
- "GEMCODE_MODEL_DEEP_RESEARCH", "travel_explore"
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
 
@@ -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
- emitter.send(make_event(event="text", id=req_id, text=out_text))
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
 
@@ -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",
@@ -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
- low_i = raw_i.lower()
105
- if low_i == "list":
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 low_i == "clear":
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
- resolved = resolve_attachment_path(raw_i, project_root=cfg.project_root)
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: {raw_i}")
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
- expanded = expand_skill_text(s, arguments=sk_args, session_id=session_id)
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
- prompt = (
166
- f"Apply GemSkill `/{s.meta.name}`.\n\n"
167
- f"## Skill instructions\n{expanded}\n\n"
168
- + (f"## Skill supporting files\n{', '.join(files)}\n\n" if files else "")
169
- + "Now carry out the user's request using the skill instructions."
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) ────────────────
@@ -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:
@@ -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
- for line in raw.splitlines():
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.strip()
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(v) >= 2 and ((v[0] == v[-1] == '"') or (v[0] == v[-1] == "'")):
161
- v = v[1:-1]
162
- d[k.lower()] = v
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
 
@@ -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
+