vet

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit e8ebbf617a2eb527b0a22d8407b094adb15c6610
parent 7de2e6efeb8bcebc732558ff89a30a8eb90fa0af
Author: Andrew Laack <andrew@laack.co>
Date:   Wed,  8 Apr 2026 13:38:37 -0500

Merge pull request #189 from imbue-ai/danielmewes/gemini-skill

Add conversation export for Gemini CLI
Diffstat:
Minstall-skill.sh | 1+
Mskills/vet/SKILL.md | 11+++++++++++
Askills/vet/scripts/export_gemini_cli_session.py | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 115 insertions(+), 0 deletions(-)

diff --git a/install-skill.sh b/install-skill.sh @@ -9,6 +9,7 @@ FILES=( "scripts/export_opencode_session.py" "scripts/export_codex_session.py" "scripts/export_claude_code_session.py" + "scripts/export_gemini_cli_session.py" ) echo "" diff --git a/skills/vet/SKILL.md b/skills/vet/SKILL.md @@ -52,6 +52,11 @@ vet "goal" --history-loader "python3 ~/.codex/skills/vet/scripts/export_codex_se vet "goal" --history-loader "python3 ~/.claude/skills/vet/scripts/export_claude_code_session.py --session-file <path-to-session.jsonl>" ``` +**Gemini CLI:** +```bash +vet "goal" --history-loader "python3 ~/.gemini/skills/vet/scripts/export_gemini_cli_session.py --session-file <path-to-session.json>" +``` + **Without Conversation History** ```bash vet "goal" @@ -79,6 +84,12 @@ You should only search for sessions from your coding harness. If a user requests - IMPORTANT: Verify the conversation you found matches the current conversation and that it is not another conversation with the same search string. 3. Pass the matched file path as `--session-file`. +**Gemini CLI:** Session files are stored in `~/.gemini/tmp/<project-name>/chats/`. To find the correct session file: +1. Find the most unique sentence / question / string in the current conversation. +2. Run: `grep -rl "UNIQUE_MESSAGE" ~/.gemini/tmp/` to find the matching session file. + - IMPORTANT: Verify the conversation you found matches the current conversation and that it is not another conversation with the same search string. +3. Pass the matched file path as `--session-file`. + NOTE: The examples in the standard usage section assume the user installed the vet skill at the user level, not the project level. Prior to trying to run vet, check if it was installed at the project level which should take precedence over the user level. If it is installed at the project level, ensure the history-loader option points to the correct location. ## Interpreting Results diff --git a/skills/vet/scripts/export_gemini_cli_session.py b/skills/vet/scripts/export_gemini_cli_session.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +import argparse +import json +import sys +from pathlib import Path + +parser = argparse.ArgumentParser(description="Export Gemini CLI session history for vet") +parser.add_argument("--session-file", required=True, help="Path to Gemini CLI session .json file") +args = parser.parse_args() + +SESSION_FILE = Path(args.session_file) +if not SESSION_FILE.exists(): + print(f"ERROR: Session file {SESSION_FILE} does not exist", file=sys.stderr) + sys.exit(1) + +try: + data = json.loads(SESSION_FILE.read_text()) +except json.JSONDecodeError as e: + print(f"ERROR: Failed to parse JSON from {SESSION_FILE}: {e}", file=sys.stderr) + sys.exit(1) + +messages = data.get("messages", []) + +for msg in messages: + msg_type = msg.get("type") + content = msg.get("content") + + if msg_type == "user": + if isinstance(content, list): + text = " ".join(c.get("text", "") for c in content if isinstance(c, dict) and "text" in c) + else: + text = str(content) + + if text: + print(json.dumps({"object_type": "ChatInputUserMessage", "text": text})) + + elif msg_type == "gemini": + blocks = [] + + # Add the text content as a TextBlock if it exists + if content and isinstance(content, str): + blocks.append({"object_type": "TextBlock", "type": "text", "text": content}) + + # Process tool calls + tool_calls = msg.get("toolCalls", []) + for tc in tool_calls: + call_id = tc.get("id") + name = tc.get("name") + tool_args = tc.get("args") + + blocks.append( + { + "object_type": "ToolUseBlock", + "type": "tool_use", + "id": call_id, + "name": name, + "input": tool_args, + } + ) + + # Process tool results + results = tc.get("result", []) + for res in results: + # Results can be complex; extract the response content + # Based on the example, it's often in functionResponse.response.output + output = "" + if "functionResponse" in res: + fr = res["functionResponse"] + response = fr.get("response", {}) + if isinstance(response, dict): + output = response.get("output", "") + else: + output = str(response) + else: + output = str(res) + + blocks.append( + { + "object_type": "ToolResultBlock", + "type": "tool_result", + "tool_use_id": call_id, + "tool_name": name, + "invocation_string": f"{name}({json.dumps(tool_args)})", + "content": {"content_type": "generic", "text": str(output)}, + } + ) + + if blocks: + print( + json.dumps( + { + "object_type": "ResponseBlockAgentMessage", + "role": "assistant", + "assistant_message_id": msg.get("id"), + "content": blocks, + } + ) + ) + + elif msg_type == "info": + # Info messages are usually system notifications, maybe skip or map to something else? + # For now, let's just skip them as they don't typically represent agent-user dialogue. + pass