GIMP — Python Manipulation Guide
SkillMediaHow to programmatically create, modify, and verify images using Python Pillow, NumPy, OpenCV, and scikit-image. For setup-gen and reward-gen agents.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the GIMP — Python Manipulation Guide skill
What this skill tells your AI
The instructions your AI receives, as published by xlang-ai/cua-gym in .claude/skills/gimp/SKILL.md and read by ahel’s review.
This skill teaches setup-gen (create/modify images) and reward-gen (read/verify image properties) how to work with image files using pure Python code.
- Libraries:
Pillow,numpy,opencv-python,scikit-image - Install:
pip3 install Pillow numpy opencv-python scikit-image - File formats:
.png,.jpg,.bmp,.xcf(GIMP native, read-only via special tools)
0. GUI Startup on VM (for setup-gen)
After generating image assets, setup-gen should open GIMP with the target initial file for the GUI agent.
CRITICAL VM LIMIT: GUI launches must set DISPLAY=:0.
import os
import shlex
import subprocess
import time
def launch_gui(command: str, delay_sec: float = 1.0):
env = os.environ.copy()
env["DISPLAY"] = ":0"
subprocess.Popen(
shlex.split(command),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=env,
)
time.sleep(delay_sec)
# Open target image in GIMP
launch_gui('gimp "/home/user/<task_id>_initial.png"', delay_sec=2.0)
Guidelines:
- Open initial artifact(s), never golden artifact(s).
- Use non-blocking launch (
Popen) and short delays.
0.5. Media Asset Library (setup-gen)
Instead of generating synthetic images (color blocks, testsrc patterns), use the real photo library at assets/media/gimp/. These are high-resolution photos from Pexels (free commercial use).
Picking Assets from Manifest
import json, os, random
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # project root
MANIFEST = os.path.join(ROOT, "assets", "media", "manifest.json")
def pick_asset(domain: str, category: str = None, tags: list = None) -> dict:
"""Pick a random asset matching domain/category/tags from manifest."""
with open(MANIFEST) as f:
manifest = json.load(f)
candidates = [a for a in manifest["assets"] if a["domain"] == domain]
if category:
candidates = [a for a in candidates if a["category"] == category]
if tags:
candidates = [a for a in candidates if any(t in a.get("tags", []) for t in tags)]
return random.choice(candidates) if candidates else None
# Pick a landscape photo for a brightness adjustment task
asset = pick_asset("gimp", "photos", tags=["landscape", "nature"])
local_path = os.path.join(ROOT, asset["path"]) # e.g., assets/media/gimp/photos/landscape_mountain_001.jpg
Uploading to VM
# Upload a photo to the VM before running initial_setup.py
python3 scripts/env_cli.py -c "<workdir>/env_config_initial.json" upload \
"assets/media/gimp/photos/landscape_mountain_001.jpg" "/home/user/<task_id>.jpg"
python3 scripts/env_cli.py -c "<workdir>/env_config_golden.json" upload \
"assets/media/gimp/photos/landscape_mountain_001.jpg" "/home/user/<task_id>.jpg"
Available Categories
| Category | Count | Description | Good for |
|---|---|---|---|
photos | ~50 | Real high-res photos (landscape, portrait, food, architecture, etc.) | Brightness, contrast, crop, resize, rotate, color grading |
graphics | ~10 | Geometric patterns, gradients, abstract art | Color mode conversion, palette, effects |
icons | ~10 | Logo/icon style images | Transparency, compositing, background removal |
Pattern: Upload + Reference Copy
# Setup-gen should upload the asset to BOTH VMs, then initial_setup.py saves the reference copy
python3 scripts/env_cli.py -c "<workdir>/env_config_initial.json" upload "assets/media/gimp/photos/xxx.jpg" "/home/user/<task_id>.jpg"
python3 scripts/env_cli.py -c "<workdir>/env_config_golden.json" upload "assets/media/gimp/photos/xxx.jpg" "/home/user/<task_id>.jpg"
Then in initial_setup.py:
import shutil
shutil.copy(f'{WORKDIR}/{TASK_ID}.jpg', f'{WORKDIR}/{TASK_ID}_initial_reference.jpg')
# ... launch GIMP with the file
1. Creating & Modifying Images (setup-gen)
Basic Image Creation
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageChops, ImageStat, ImageEnhance
from PIL.Image import Resampling
import numpy as np
import shutil, os
# Create blank image
img = Image.new("RGB", (800, 600), color=(255, 255, 255))
img.save("/home/user/Desktop/blank.png")
# Create RGBA image with transparency
img = Image.new("RGBA", (800, 600), color=(0, 0, 0, 0))
img.save("/home/user/Desktop/transparent.png")
Drawing Shapes and Text
draw = ImageDraw.Draw(img)
# Rectangle
draw.rectangle([100, 100, 300, 200], fill="red", outline="black", width=2)
# Circle / ellipse
draw.ellipse([400, 100, 550, 250], fill="blue")
# Triangle (polygon)
draw.polygon([(400, 300), (300, 500), (500, 500)], fill="yellow", outline="black")
# Line
draw.line([(0, 0), (800, 600)], fill="green", width=3)
# Text
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 36)
draw.text((100, 50), "Hello World", fill="black", font=font)
img.save("/home/user/Desktop/shapes.png")
Image Transformations
img = Image.open("/home/user/Desktop/photo.png")
# Resize (maintain aspect ratio)
target_height = 512
ratio = target_height / img.height
new_size = (int(img.width * ratio), target_height)
img_resized = img.resize(new_size, Resampling.LANCZOS)
# Crop
img_cropped = img.crop((left, top, right, bottom)) # box = (left, upper, right, lower)
# Rotate
img_rotated = img.rotate(90, expand=True) # expand=True to fit full rotated image
# Mirror / flip
img_mirror = img.transpose(Image.FLIP_LEFT_RIGHT) # horizontal mirror
img_flip = img.transpose(Image.FLIP_TOP_BOTTOM) # vertical flip
# Convert color mode
img_gray = img.convert("L") # grayscale
img_rgb = img.convert("RGB") # RGB (drops alpha)
img_rgba = img.convert("RGBA") # add alpha channel
img_palette = img.convert("P") # indexed/palette mode
img_hsv = img.convert("HSV") # hue-saturation-value
Brightness, Contrast, Saturation Adjustments
# Brightness
enhancer = ImageEnhance.Brightness(img)
img_darker = enhancer.enhance(0.7) # < 1 = darker, > 1 = brighter
# Contrast
enhancer = ImageEnhance.Contrast(img)
img_contrast = enhancer.enhance(1.5) # > 1 = more contrast
# Saturation / Color
enhancer = ImageEnhance.Color(img)
img_vivid = enhancer.enhance(1.5) # > 1 = more saturated
# Sharpness
enhancer = ImageEnhance.Sharpness(img)
img_sharp = enhancer.enhance(2.0) # > 1 = sharper
Transparency & Background Removal
# Make white background transparent
img = img.convert("RGBA")
data = np.array(img)
# Replace white pixels with transparent
white_mask = (data[:, :, 0] > 240) & (data[:, :, 1] > 240) & (data[:, :, 2] > 240)
data[white_mask, 3] = 0
img = Image.fromarray(data)
# Fill background layer with a color (keeping object intact)
bg = Image.new("RGB", img.size, (0, 255, 0)) # green background
if img.mode == "RGBA":
bg.paste(img, mask=img.split()[3]) # paste using alpha as mask
Filters
img_blur = img.filter(ImageFilter.GaussianBlur(radius=5))
img_sharp = img.filter(ImageFilter.SHARPEN)
img_edges = img.filter(ImageFilter.FIND_EDGES)
img_emboss = img.filter(ImageFilter.EMBOSS)
Golden File Pattern
# Always copy-then-modify for golden files
shutil.copy("/home/user/Desktop/original.png", "/home/user/Desktop/golden.png")
img = Image.open("/home/user/Desktop/golden.png")
img = img.transpose(Image.FLIP_LEFT_RIGHT) # example transformation
img.save("/home/user/Desktop/golden.png")
Compositing Multiple Images
bg = Image.open("/home/user/Desktop/background.png").convert("RGBA")
fg = Image.open("/home/user/Desktop/foreground.png").convert("RGBA")
# Paste foreground onto background at position
bg.paste(fg, (100, 100), mask=fg) # use alpha as mask
bg.save("/home/user/Desktop/composite.png")
2. Reading & Verifying Images (reward-gen)
Structure Similarity (SSIM)
from skimage.metrics import structural_similarity as ssim
def check_ssim(src_path: str, tgt_path: str, threshold: float = 0.9) -> bool:
"""Compare two same-sized images by SSIM. Returns True if similar."""
img1 = Image.open(src_path).convert("RGB")
img2 = Image.open(tgt_path).convert("RGB")
if img1.size != img2.size:
return False
arr1, arr2 = np.array(img1), np.array(img2)
min_dim = min(arr1.shape[0], arr1.shape[1])
win_size = min(7, min_dim if min_dim % 2 == 1 else min_dim - 1)
if win_size < 1:
return False
try:
score = ssim(arr1, arr2, win_size=win_size, channel_axis=2)
except TypeError:
score = ssim(arr1, arr2, win_size=win_size, multichannel=True)
return score >= threshold
def check_ssim_resized(src_path: str, tgt_path: str, threshold: float = 0.9) -> bool:
"""SSIM with auto-resize and transparency handling."""
img1 = Image.open(src_path)
img2 = Image.open(tgt_path)
# Handle transparency: crop to content bounding box
if img1.mode in ("RGBA", "LA"):
alpha = img1.split()[-1]
bbox = alpha.getbbox()
if bbox:
img1 = img1.crop(bbox)
img1 = img1.convert("RGB").resize(img2.size if img2.mode == "RGB" else img2.convert("RGB").size, Resampling.LANCZOS)
img2 = img2.convert("RGB")
arr1, arr2 = np.array(img1), np.array(img2)
try:
score = ssim(arr1, arr2, win_size=7, channel_axis=2)
except TypeError:
score = ssim(arr1, arr2, win_size=7, multichannel=True)
return score >= threshold
Pixel-Perfect Comparison
def compare_exact(src_path: str, tgt_path: str) -> bool:
"""Binary comparison: True only if all pixels match."""
img1 = Image.open(src_path)
img2 = Image.open(tgt_path)
if img1.size != img2.size:
img1 = img1.resize(img2.size, Resampling.LANCZOS)
if img1.mode != img2.mode:
img1 = img1.convert(img2.mode)
diff = ImageChops.difference(img1, img2)
return diff.getbbox() is None
Brightness Verification
def calculate_brightness(img: Image.Image) -> float:
gray = img.convert("L")
return ImageStat.Stat(gray).mean[0]
def verify_brightness_decreased(src_path: str, tgt_path: str) -> bool:
"""Verify src is darker than tgt AND structure preserved."""
img_src = Image.open(src_path)
img_tgt = Image.open(tgt_path)
if calculate_brightness(img_src) >= calculate_brightness(img_tgt):
return False
# Normalize both to same brightness, then compare structure
def normalize(img, target=128):
factor = target / max(calculate_brightness(img), 1)
return img.point(lambda x: min(255, max(0, int(x * factor))))
mse = np.mean((np.array(normalize(img_src), dtype=np.float32) / 255 -
np.array(normalize(img_tgt), dtype=np.float32) / 255) ** 2)
return mse < 0.03
Contrast Verification
def calculate_contrast(img: Image.Image) -> float:
return float(np.std(np.asarray(img, dtype=np.float32)))
def verify_contrast_increased(src_path: str, tgt_path: str) -> bool:
src, tgt = Image.open(src_path), Image.open(tgt_path)
return calculate_contrast(src) > calculate_contrast(tgt) and check_ssim(src_path, tgt_path, threshold=0.65)
Saturation Verification
def verify_saturation_increased(src_path: str, tgt_path: str) -> bool:
src_hsv = Image.open(src_path).convert("HSV")
tgt_hsv = Image.open(tgt_path).convert("HSV")
src_sat = np.mean(np.array(src_hsv.split()[1]))
tgt_sat = np.mean(np.array(tgt_hsv.split()[1]))
if src_sat <= tgt_sat:
return False # saturation did not increase
# H and V channels must match
h1, _, v1 = src_hsv.split()
h2, _, v2 = tgt_hsv.split()
return check_ssim_channel(h1, h2) and check_ssim_channel(v1, v2)
def check_ssim_channel(ch1, ch2, threshold=0.9):
arr1, arr2 = np.array(ch1.convert("RGB")), np.array(ch2.convert("RGB"))
if arr1.shape != arr2.shape:
return False
try:
return ssim(arr1, arr2, win_size=7, channel_axis=2) >= threshold
except TypeError:
return ssim(arr1, arr2, win_size=7, multichannel=True) >= threshold
Image Size Verification
def verify_image_size(src_path: str, width: int = None, height: int = None,
ignore_transparent: bool = False) -> bool:
img = Image.open(src_path)
if ignore_transparent and img.mode in ("RGBA", "LA"):
alpha = img.split()[-1]
bbox = alpha.getbbox()
if bbox is None:
return False
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
else:
w, h = img.size
if width is not None and w != width:
return False
if height is not None and h != height:
return False
return True
Mirror Verification
def verify_mirror(src_path: str, tgt_path: str) -> bool:
src = Image.open(src_path)
tgt = Image.open(tgt_path)
flipped = src.transpose(Image.FLIP_LEFT_RIGHT)
arr1, arr2 = np.array(flipped.convert("RGB")), np.array(tgt.convert("RGB"))
if arr1.shape != arr2.shape:
return False
try:
return ssim(arr1, arr2, win_size=7, channel_axis=2) >= 0.99
except TypeError:
return ssim(arr1, arr2, win_size=7, multichannel=True) >= 0.99
Palette Mode Verification
def verify_palette_mode(src_path: str, tgt_path: str) -> bool:
"""Verify image is palette-based and structure matches."""
img = Image.open(src_path)
if img.mode != "P":
return False
return check_ssim(src_path, tgt_path, threshold=0.9) # compare after RGB conversion
Green Background Verification
def verify_green_background(src_path: str, tgt_path: str) -> bool:
"""All non-black target pixels must have green source pixels (g > r and g > b)."""
src = np.array(Image.open(src_path))
tgt = np.array(Image.open(tgt_path))
# Vectorized: find non-black pixels in target
mask = np.any(tgt[:, :, :3] != 0, axis=2)
if not np.any(mask):
return True
r, g, b = src[mask, 0], src[mask, 1], src[mask, 2]
return bool(np.all(g > r) and np.all(g > b))
Shape Position Verification
def verify_centered(tgt_path: str, tolerance: float = 0.05) -> bool:
"""Verify a colored shape is centered within tolerance of image center."""
img = np.array(Image.open(tgt_path))
unique_colors, counts = np.unique(img.reshape(-1, img.shape[2]), axis=0, return_counts=True)
sorted_colors = unique_colors[np.argsort(counts)]
shape_color = sorted_colors[1] # second most common = shape (not background)
mask = np.all(img == shape_color, axis=2)
coords = np.argwhere(mask)
centroid = coords.mean(axis=0)
center = np.array(img.shape[:2]) / 2
return bool(np.all(np.abs(centroid - center) < tolerance * np.array(img.shape[:2])))
Text Position Verification
def verify_text_on_left(src_path: str, width_threshold: float = 0.05) -> bool:
"""Verify dark text starts within the left edge of the image."""
gray = np.array(Image.open(src_path).convert("L"))
h, w = gray.shape
dark_mask = gray < 128
if not np.any(dark_mask):
return False
# Find the leftmost dark pixel across all rows
cols = np.where(dark_mask.any(axis=0))[0]
left_most = cols[0] if len(cols) > 0 else w
return left_most < w * width_threshold
Sharpness Verification
import cv2
def verify_sharper(src_path: str, tgt_path: str) -> bool:
src = cv2.imread(src_path, cv2.IMREAD_GRAYSCALE)
tgt = cv2.imread(tgt_path, cv2.IMREAD_GRAYSCALE)
return float(np.var(cv2.Laplacian(src, cv2.CV_64F))) > float(np.var(cv2.Laplacian(tgt, cv2.CV_64F)))
File Existence & Size Verification
def verify_file_exists_and_similar(src_path: str, tgt_path: str) -> bool:
if not os.path.isfile(src_path):
return False
return check_ssim(src_path, tgt_path)
def verify_file_size(src_path: str, max_bytes: int) -> bool:
return os.path.isfile(src_path) and os.path.getsize(src_path) < max_bytes
GIMP Config Verification
def verify_gimp_config(config_path: str, key, value: str) -> bool:
"""Check GIMP gimprc config file for key-value pair.
key can be str or list of str (for multi-word keys)."""
with open(config_path, "r") as f:
for line in f:
if line.startswith("#") or line.strip() == "":
continue
items = line.strip().lstrip("(").rstrip(")").split()
if isinstance(key, str):
if items[0] == key and items[-1] == value:
return True
elif isinstance(key, list) and len(key) == 2:
if items[0] == key[0] and items[1] == key[1] and items[-1] == value:
return True
return False
3. XCF (GIMP Native Format) Reading
GIMP's native .xcf format stores layers, channels, and paths. Pillow cannot read XCF. Use these approaches:
Reading XCF with python-xcftools
import subprocess, json, re
def get_xcf_info(xcf_path: str) -> dict:
"""Extract XCF metadata: dimensions, layers, color mode."""
result = subprocess.run(["xcfinfo", xcf_path], capture_output=True, text=True)
info = {"layers": [], "width": 0, "height": 0}
for line in result.stdout.strip().split("\n"):
# First line: "Version X, WxH RGB color, N layers, ..."
if line.startswith("Version"):
m = re.search(r'(\d+)x(\d+)\s+(\w+)', line)
if m:
info["width"], info["height"] = int(m.group(1)), int(m.group(2))
info["color_mode"] = m.group(3)
# Layer lines: "+ WxH+X+Y RGB-alpha Normal name"
elif line.startswith("+") or line.startswith("-"):
parts = line.strip().split()
visible = parts[0] == "+"
# Layer name is last quoted or unquoted token
name = " ".join(parts[4:]) if len(parts) > 4 else ""
info["layers"].append({"name": name, "visible": visible})
return info
def get_xcf_layer_count(xcf_path: str) -> int:
info = get_xcf_info(xcf_path)
return len(info["layers"])
def verify_xcf_has_layer(xcf_path: str, layer_name: str) -> bool:
info = get_xcf_info(xcf_path)
return any(layer_name.lower() in l["name"].lower() for l in info["layers"])
Flattening XCF to PNG for Comparison
def xcf_to_png(xcf_path: str, png_path: str) -> bool:
"""Flatten XCF to PNG using xcf2png (from xcftools package)."""
result = subprocess.run(["xcf2png", xcf_path, "-o", png_path], capture_output=True)
return result.returncode == 0
def xcf_extract_layer(xcf_path: str, layer_name: str, png_path: str) -> bool:
"""Extract a single layer from XCF to PNG."""
result = subprocess.run(
["xcf2png", xcf_path, layer_name, "-o", png_path],
capture_output=True
)
return result.returncode == 0
# Install on VM: sudo apt install xcftools
4. GIMP Script-Fu Batch Mode
For tasks that require GIMP-specific operations (filters, layer manipulation), use headless batch mode.
def gimp_batch(script_fu: str, timeout: int = 30) -> str:
"""Run a Script-Fu command in GIMP batch mode (headless)."""
result = subprocess.run(
["gimp", "-i", "-b", script_fu, "-b", "(gimp-quit 0)"],
capture_output=True, text=True, timeout=timeout,
env={**os.environ, "DISPLAY": ":0"}
)
return result.stdout + result.stderr
# Flatten an XCF and export as PNG
gimp_batch('(let* ((image (car (gimp-file-load RUN-NONINTERACTIVE "/home/user/file.xcf" "file.xcf"))) '
'(drawable (car (gimp-image-flatten image)))) '
'(file-png-save RUN-NONINTERACTIVE image drawable "/home/user/flat.png" "flat.png" 0 9 1 1 1 1 1))')
# Apply Gaussian blur and export
gimp_batch('(let* ((image (car (gimp-file-load RUN-NONINTERACTIVE "/home/user/in.png" "in.png"))) '
'(drawable (car (gimp-image-flatten image)))) '
'(plug-in-gauss RUN-NONINTERACTIVE image drawable 10 10 0) '
'(gimp-file-overwrite RUN-NONINTERACTIVE image drawable "/home/user/out.png" "out.png"))')
# Get image properties (layer count, dimensions)
gimp_batch('(let* ((image (car (gimp-file-load RUN-NONINTERACTIVE "/home/user/file.xcf" "file.xcf")))) '
'(gimp-message (number->string (car (gimp-image-get-active-layer image)))) '
'(gimp-message (number->string (car (gimp-image-width image)))))')
Guidelines:
-i= no GUI,-b= batch command- Always end with
(gimp-quit 0)or GIMP hangs - Script-Fu uses Scheme syntax with
carto unwrap return lists - Set timeout — GIMP batch can hang on missing fonts or corrupt files
5. Color Space & Color Verification
Color Temperature / White Balance Verification
def calculate_color_temperature_shift(src_path: str, tgt_path: str) -> tuple:
"""Calculate average RGB shift between two images. Returns (dr, dg, db)."""
src = np.array(Image.open(src_path).convert("RGB"), dtype=np.float32)
tgt = np.array(Image.open(tgt_path).convert("RGB"), dtype=np.float32)
if src.shape != tgt.shape:
tgt = np.array(Image.open(tgt_path).convert("RGB").resize(
Image.open(src_path).size, Resampling.LANCZOS), dtype=np.float32)
diff = np.mean(src - tgt, axis=(0, 1))
return tuple(diff)
def verify_warmer_tone(src_path: str, tgt_path: str) -> bool:
"""Verify src has warmer tone than tgt (more red/yellow, less blue)."""
dr, dg, db = calculate_color_temperature_shift(src_path, tgt_path)
return dr > 2 and db < -2 # red increased, blue decreased
def verify_cooler_tone(src_path: str, tgt_path: str) -> bool:
"""Verify src has cooler tone than tgt (more blue, less red)."""
dr, dg, db = calculate_color_temperature_shift(src_path, tgt_path)
return dr < -2 and db > 2
Histogram-Based Color Distribution
def get_color_histogram(img_path: str, bins: int = 64) -> np.ndarray:
"""Get normalized RGB histogram."""
img = np.array(Image.open(img_path).convert("RGB"))
hist_r = np.histogram(img[:,:,0], bins=bins, range=(0,256))[0]
hist_g = np.histogram(img[:,:,1], bins=bins, range=(0,256))[0]
hist_b = np.histogram(img[:,:,2], bins=bins, range=(0,256))[0]
hist = np.concatenate([hist_r, hist_g, hist_b]).astype(np.float32)
return hist / hist.sum()
def verify_histogram_similar(src_path: str, tgt_path: str, threshold: float = 0.85) -> bool:
"""Compare color distributions via histogram correlation."""
h1 = get_color_histogram(src_path)
h2 = get_color_histogram(tgt_path)
correlation = float(np.corrcoef(h1, h2)[0, 1])
return correlation >= threshold
def verify_grayscale(img_path: str) -> bool:
"""Verify image is grayscale (R == G == B for all pixels, or mode is L)."""
img = Image.open(img_path)
if img.mode == "L":
return True
if img.mode in ("RGB", "RGBA"):
arr = np.array(img.convert("RGB"))
return bool(np.all(arr[:,:,0] == arr[:,:,1]) and np.all(arr[:,:,1] == arr[:,:,2]))
return False
def verify_inverted(src_path: str, tgt_path: str, threshold: float = 0.95) -> bool:
"""Verify src is the color-inverted version of tgt."""
src = np.array(Image.open(src_path).convert("RGB"), dtype=np.float32)
tgt = np.array(Image.open(tgt_path).convert("RGB"), dtype=np.float32)
if src.shape != tgt.shape:
return False
inverted_tgt = 255.0 - tgt
mse = np.mean((src - inverted_tgt) ** 2)
return mse < (1.0 - threshold) * 255 * 255
6. Gradual Scoring Pattern (reward-gen)
def compute_reward(src_path: str, golden_path: str, task_type: str = "transform",
checks: list = None) -> float:
"""Compute a 0.0-1.0 reward score with multiple verification dimensions.
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 197
- Forks
- 18
- Last commit
- Aug 2026
ahel review
K1binfo
installs-packages
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Catalog kind
- skill
- Gateway key
gimp- Source
- github.com/xlang-ai/cua-gym