feat: clipboard image paste in CLI (Cmd+V / Ctrl+V)

Copy an image to clipboard (screenshot, browser, etc.) and paste into
the Hermes CLI. The image is saved to ~/.hermes/images/, shown as a
badge above the input ([📎 Image #1]), and sent to the model as a
base64-encoded OpenAI vision multimodal content block.

Implementation:
- hermes_cli/clipboard.py: clean module with platform-specific extraction
  - macOS: pngpaste (if installed) → osascript fallback (always available)
  - Linux: xclip (apt install xclip)
- cli.py: BracketedPaste key handler checks clipboard on every paste,
  image bar widget shows attached images, chat() converts to multimodal
  content format, Ctrl+C clears attachments

Inspired by @m0at's fork (https://github.com/m0at/hermes-agent) which
implemented image paste support for local vision models. Reimplemented
cleanly as a separate module with tests.
This commit is contained in:
teknium1 2026-03-05 17:53:58 -08:00
parent fec8a0da72
commit 399562a7d1
3 changed files with 341 additions and 15 deletions

View file

@ -0,0 +1,107 @@
"""Tests for hermes_cli/clipboard.py — clipboard image extraction."""
import subprocess
import sys
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest
from hermes_cli.clipboard import (
save_clipboard_image,
_linux_save,
_macos_pngpaste,
_macos_osascript,
)
class TestSaveClipboardImage:
"""Platform dispatch."""
def test_dispatches_to_macos_on_darwin(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.sys") as mock_sys:
mock_sys.platform = "darwin"
with patch("hermes_cli.clipboard._macos_save", return_value=False) as mock_mac:
save_clipboard_image(dest)
mock_mac.assert_called_once_with(dest)
def test_dispatches_to_linux_on_linux(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.sys") as mock_sys:
mock_sys.platform = "linux"
with patch("hermes_cli.clipboard._linux_save", return_value=False) as mock_linux:
save_clipboard_image(dest)
mock_linux.assert_called_once_with(dest)
def test_creates_parent_dirs(self, tmp_path):
dest = tmp_path / "deep" / "nested" / "out.png"
with patch("hermes_cli.clipboard.sys") as mock_sys:
mock_sys.platform = "linux"
with patch("hermes_cli.clipboard._linux_save", return_value=False):
save_clipboard_image(dest)
assert dest.parent.exists()
class TestMacosPngpaste:
def test_success(self, tmp_path):
dest = tmp_path / "out.png"
dest.write_bytes(b"fake png data") # simulate pngpaste writing
with patch("hermes_cli.clipboard.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
assert _macos_pngpaste(dest) is True
def test_not_installed(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.subprocess.run", side_effect=FileNotFoundError):
assert _macos_pngpaste(dest) is False
def test_no_image_in_clipboard(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=1)
assert _macos_pngpaste(dest) is False
class TestMacosOsascript:
def test_no_image_type_in_clipboard(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(
stdout="«class ut16», «class utf8»", returncode=0
)
assert _macos_osascript(dest) is False
def test_clipboard_info_check_fails(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.subprocess.run", side_effect=Exception("fail")):
assert _macos_osascript(dest) is False
class TestLinuxSave:
def test_no_xclip_installed(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.subprocess.run", side_effect=FileNotFoundError):
assert _linux_save(dest) is False
def test_no_image_in_clipboard(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout="text/plain\n", returncode=0)
assert _linux_save(dest) is False
def test_image_in_clipboard(self, tmp_path):
dest = tmp_path / "out.png"
def fake_run(cmd, **kwargs):
if "TARGETS" in cmd:
return MagicMock(stdout="image/png\ntext/plain\n", returncode=0)
# Extract call — write fake data
if "stdout" in kwargs and kwargs["stdout"]:
kwargs["stdout"].write(b"fake png")
return MagicMock(returncode=0)
with patch("hermes_cli.clipboard.subprocess.run", side_effect=fake_run):
# Create the file to simulate xclip writing
dest.write_bytes(b"fake png")
assert _linux_save(dest) is True