vet

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

commit d7619686b535d69cae1f6deb0b2772edada65018
parent e8961663b76edc72818737dc0216d2c2b638dbfb
Author: andrewlaack-collab <andrew.laack@imbue.com>
Date:   Tue, 24 Feb 2026 18:39:48 +0000

Update opencode sqlite support (#137)

* Update opencode sqlite support

* Different approach

* Stupid message streaming clis, absolutely no taste.

* Fixed skill messaging for loading conversations

* Formatters

* Error handling

* Format

* Instructions about python usage

---------

Co-authored-by: Andrew Laack <andrew@laack.co>
Diffstat:
Mskills/vet/SKILL.md | 35++++++++++++++++++++++++-----------
Mskills/vet/scripts/export_opencode_session.py | 75++++++++++++++++++++++++++++++++++++++++++---------------------------------
Muv.lock | 2+-
3 files changed, 67 insertions(+), 45 deletions(-)

diff --git a/skills/vet/SKILL.md b/skills/vet/SKILL.md @@ -28,19 +28,25 @@ vet --help ### Standard Usage +Before running vet, determine the correct Python binary: +```bash +$(command -v python3 || command -v python) +``` +Use whichever resolves (prefer `python3`). The examples below use `python3` — substitute `python` if that is what your system provides. + **OpenCode:** ```bash -vet "goal" --history-loader "python ~/.agents/skills/vet/scripts/export_opencode_session.py --session-id <ses_ID>" +vet "goal" --history-loader "$(command -v python3 || command -v python) ~/.agents/skills/vet/scripts/export_opencode_session.py --session-id <ses_ID>" ``` **Codex:** ```bash -vet "goal" --history-loader "python ~/.codex/skills/vet/scripts/export_codex_session.py --session-file <path-to-session.jsonl>" +vet "goal" --history-loader "$(command -v python3 || command -v python) ~/.codex/skills/vet/scripts/export_codex_session.py --session-file <path-to-session.jsonl>" ``` **Claude Code:** ```bash -vet "goal" --history-loader "python ~/.claude/skills/vet/scripts/export_claude_code_session.py --session-file <path-to-session.jsonl>" +vet "goal" --history-loader "$(command -v python3 || command -v python) ~/.claude/skills/vet/scripts/export_claude_code_session.py --session-file <path-to-session.jsonl>" ``` **Without Conversation History** @@ -50,16 +56,23 @@ vet "goal" ### Finding Your Session -**OpenCode:** The `--session-id` argument requires a `ses_...` session ID. To find the current session ID, search for the first user message from this conversation in the part files: -1. Find the most unique sentence / question / string in the current conversation. -2. Run: `grep -rl "UNIQUE_MESSAGE" ~/.local/share/opencode/storage/part/` to find the matching part file. - - IMPORTANT: Verify the conversation you found matches the current conversation and that it is not another conversation with the same search string. This happens frequently so it is paramount you verify this. Repeat steps 1 and 2 until you have verified the session you found is the current conversation. -3. Read the `sessionID` field from that part JSON file. -4. Pass that value as `--session-id`. +**OpenCode:** The `--session-id` argument requires a `ses_...` session ID. To find the current session ID: +1. Run: `opencode session list --format json` to list recent sessions with their IDs and titles. +2. Identify the current session from the list by matching the title or timestamp. + - IMPORTANT: Verify the session you found matches the current conversation. If the title is ambiguous, compare timestamps or check multiple candidates. +3. Pass the session ID as `--session-id`. -**Codex:** Session files are stored in `~/.codex/sessions/YYYY/MM/DD/`. Find the correct conversation using the approach described above for opencode that uses textual search. +**Codex:** Session files are stored in `~/.codex/sessions/YYYY/MM/DD/`. To find the correct session file: +1. Find the most unique sentence / question / string in the current conversation. +2. Run: `grep -rl "UNIQUE_MESSAGE" ~/.codex/sessions/` 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`. -**Claude Code:** Session files are stored in `~/.claude/projects/<encoded-path>/`. The encoded path replaces `/` with `-` (e.g. `/home/user/myproject` becomes `-home-user-myproject`). Find the correct conversation using the approach described above for opencode that uses textual search. +**Claude Code:** Session files are stored in `~/.claude/projects/<encoded-path>/`. The encoded path replaces `/` with `-` (e.g. `/home/user/myproject` becomes `-home-user-myproject`). To find the correct session file: +1. Find the most unique sentence / question / string in the current conversation. +2. Run: `grep -rl "UNIQUE_MESSAGE" ~/.claude/projects/` 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. diff --git a/skills/vet/scripts/export_opencode_session.py b/skills/vet/scripts/export_opencode_session.py @@ -1,52 +1,61 @@ #!/usr/bin/env python3 import argparse import json +import os +import subprocess import sys -from pathlib import Path +import tempfile parser = argparse.ArgumentParser(description="Export OpenCode session history for vet") parser.add_argument("--session-id", required=True, help="OpenCode session ID (ses_...)") args = parser.parse_args() -STORAGE = Path.home() / ".local/share/opencode/storage" -MSG_DIR = STORAGE / "message" / args.session_id -PART_DIR = STORAGE / "part" +fd, tmppath = tempfile.mkstemp(suffix=".json") +try: + with os.fdopen(fd, "w+b") as f: + try: + result = subprocess.run( + ["opencode", "export", args.session_id], + stdout=f, + stderr=subprocess.PIPE, + ) + except (FileNotFoundError, OSError) as e: + print( + f"WARNING: Could not run 'opencode' command: {e}", + file=sys.stderr, + ) + sys.exit(0) + + if result.returncode != 0: + print( + f"WARNING: opencode export failed for session {args.session_id}: {result.stderr.decode().strip()}", + file=sys.stderr, + ) + sys.exit(0) + + with open(tmppath, "r") as f: + raw = f.read() +finally: + os.unlink(tmppath) -if not MSG_DIR.exists(): +if not raw.strip(): print( - f"WARNING: Message directory not found for session {args.session_id}", + f"WARNING: opencode export returned empty output for session {args.session_id}", file=sys.stderr, ) sys.exit(0) -messages = [] -for msg_file in sorted(MSG_DIR.glob("*.json")): - try: - msg = json.loads(msg_file.read_text()) - except json.JSONDecodeError as e: - print(f"WARNING: Skipping malformed message file {msg_file}: {e}", file=sys.stderr) - continue - messages.append((msg.get("time", {}).get("created", 0), msg)) - -for _, msg in sorted(messages, key=lambda x: x[0]): - msg_id = msg["id"] - role = msg.get("role", "user") - part_dir = PART_DIR / msg_id - - if not part_dir.exists(): - continue +try: + data = json.loads(raw) +except json.JSONDecodeError as e: + print(f"WARNING: Failed to parse opencode export output: {e}", file=sys.stderr) + sys.exit(0) - parts = [] - for part_file in part_dir.glob("*.json"): - try: - part = json.loads(part_file.read_text()) - except json.JSONDecodeError as e: - print( - f"WARNING: Skipping malformed part file {part_file}: {e}", - file=sys.stderr, - ) - continue - parts.append(part) +for msg in data.get("messages", []): + info = msg.get("info", {}) + parts = msg.get("parts", []) + role = info.get("role", "user") + msg_id = info.get("id", "") if role == "user": text = " ".join(p.get("text", "") for p in parts if p.get("type") == "text") diff --git a/uv.lock b/uv.lock @@ -1511,7 +1511,7 @@ wheels = [ [[package]] name = "verify-everything" -version = "0.1.12" +version = "0.1.14" source = { editable = "." } dependencies = [ { name = "anthropic" },