@agent-webui/ai-desk-harness-gimp 1.0.29-beta1

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 (48) hide show
  1. package/README.md +4 -0
  2. package/manifest.json +22 -0
  3. package/package.json +11 -0
  4. package/python/agent-harness/GIMP.md +301 -0
  5. package/python/agent-harness/build/lib/cli_anything/gimp/__init__.py +1 -0
  6. package/python/agent-harness/build/lib/cli_anything/gimp/__main__.py +3 -0
  7. package/python/agent-harness/build/lib/cli_anything/gimp/core/__init__.py +1 -0
  8. package/python/agent-harness/build/lib/cli_anything/gimp/core/canvas.py +193 -0
  9. package/python/agent-harness/build/lib/cli_anything/gimp/core/export.py +479 -0
  10. package/python/agent-harness/build/lib/cli_anything/gimp/core/filters.py +382 -0
  11. package/python/agent-harness/build/lib/cli_anything/gimp/core/layers.py +249 -0
  12. package/python/agent-harness/build/lib/cli_anything/gimp/core/media.py +174 -0
  13. package/python/agent-harness/build/lib/cli_anything/gimp/core/project.py +131 -0
  14. package/python/agent-harness/build/lib/cli_anything/gimp/core/session.py +130 -0
  15. package/python/agent-harness/build/lib/cli_anything/gimp/gimp_cli.py +788 -0
  16. package/python/agent-harness/build/lib/cli_anything/gimp/tests/__init__.py +1 -0
  17. package/python/agent-harness/build/lib/cli_anything/gimp/tests/test_core.py +478 -0
  18. package/python/agent-harness/build/lib/cli_anything/gimp/tests/test_full_e2e.py +578 -0
  19. package/python/agent-harness/build/lib/cli_anything/gimp/utils/__init__.py +1 -0
  20. package/python/agent-harness/build/lib/cli_anything/gimp/utils/gimp_backend.py +208 -0
  21. package/python/agent-harness/build/lib/cli_anything/gimp/utils/repl_skin.py +498 -0
  22. package/python/agent-harness/cli_anything/gimp/README.md +202 -0
  23. package/python/agent-harness/cli_anything/gimp/__init__.py +1 -0
  24. package/python/agent-harness/cli_anything/gimp/__main__.py +3 -0
  25. package/python/agent-harness/cli_anything/gimp/core/__init__.py +1 -0
  26. package/python/agent-harness/cli_anything/gimp/core/canvas.py +193 -0
  27. package/python/agent-harness/cli_anything/gimp/core/export.py +479 -0
  28. package/python/agent-harness/cli_anything/gimp/core/filters.py +382 -0
  29. package/python/agent-harness/cli_anything/gimp/core/layers.py +249 -0
  30. package/python/agent-harness/cli_anything/gimp/core/media.py +174 -0
  31. package/python/agent-harness/cli_anything/gimp/core/project.py +131 -0
  32. package/python/agent-harness/cli_anything/gimp/core/session.py +130 -0
  33. package/python/agent-harness/cli_anything/gimp/gimp_cli.py +788 -0
  34. package/python/agent-harness/cli_anything/gimp/tests/TEST.md +137 -0
  35. package/python/agent-harness/cli_anything/gimp/tests/__init__.py +1 -0
  36. package/python/agent-harness/cli_anything/gimp/tests/test_core.py +478 -0
  37. package/python/agent-harness/cli_anything/gimp/tests/test_full_e2e.py +578 -0
  38. package/python/agent-harness/cli_anything/gimp/utils/__init__.py +1 -0
  39. package/python/agent-harness/cli_anything/gimp/utils/gimp_backend.py +208 -0
  40. package/python/agent-harness/cli_anything/gimp/utils/repl_skin.py +498 -0
  41. package/python/agent-harness/cli_anything_gimp.egg-info/PKG-INFO +236 -0
  42. package/python/agent-harness/cli_anything_gimp.egg-info/SOURCES.txt +25 -0
  43. package/python/agent-harness/cli_anything_gimp.egg-info/dependency_links.txt +1 -0
  44. package/python/agent-harness/cli_anything_gimp.egg-info/entry_points.txt +2 -0
  45. package/python/agent-harness/cli_anything_gimp.egg-info/not-zip-safe +1 -0
  46. package/python/agent-harness/cli_anything_gimp.egg-info/requires.txt +7 -0
  47. package/python/agent-harness/cli_anything_gimp.egg-info/top_level.txt +1 -0
  48. package/python/agent-harness/setup.py +54 -0
@@ -0,0 +1,498 @@
1
+ """cli-anything REPL Skin — Unified terminal interface for all CLI harnesses.
2
+
3
+ Copy this file into your CLI package at:
4
+ cli_anything/<software>/utils/repl_skin.py
5
+
6
+ Usage:
7
+ from cli_anything.<software>.utils.repl_skin import ReplSkin
8
+
9
+ skin = ReplSkin("shotcut", version="1.0.0")
10
+ skin.print_banner()
11
+ prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
12
+ skin.success("Project saved")
13
+ skin.error("File not found")
14
+ skin.warning("Unsaved changes")
15
+ skin.info("Processing 24 clips...")
16
+ skin.status("Track 1", "3 clips, 00:02:30")
17
+ skin.table(headers, rows)
18
+ skin.print_goodbye()
19
+ """
20
+
21
+ import os
22
+ import sys
23
+
24
+ # ── ANSI color codes (no external deps for core styling) ──────────────
25
+
26
+ _RESET = "\033[0m"
27
+ _BOLD = "\033[1m"
28
+ _DIM = "\033[2m"
29
+ _ITALIC = "\033[3m"
30
+ _UNDERLINE = "\033[4m"
31
+
32
+ # Brand colors
33
+ _CYAN = "\033[38;5;80m" # cli-anything brand cyan
34
+ _CYAN_BG = "\033[48;5;80m"
35
+ _WHITE = "\033[97m"
36
+ _GRAY = "\033[38;5;245m"
37
+ _DARK_GRAY = "\033[38;5;240m"
38
+ _LIGHT_GRAY = "\033[38;5;250m"
39
+
40
+ # Software accent colors — each software gets a unique accent
41
+ _ACCENT_COLORS = {
42
+ "gimp": "\033[38;5;214m", # warm orange
43
+ "blender": "\033[38;5;208m", # deep orange
44
+ "inkscape": "\033[38;5;39m", # bright blue
45
+ "audacity": "\033[38;5;33m", # navy blue
46
+ "libreoffice": "\033[38;5;40m", # green
47
+ "obs_studio": "\033[38;5;55m", # purple
48
+ "kdenlive": "\033[38;5;69m", # slate blue
49
+ "shotcut": "\033[38;5;35m", # teal green
50
+ }
51
+ _DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
52
+
53
+ # Status colors
54
+ _GREEN = "\033[38;5;78m"
55
+ _YELLOW = "\033[38;5;220m"
56
+ _RED = "\033[38;5;196m"
57
+ _BLUE = "\033[38;5;75m"
58
+ _MAGENTA = "\033[38;5;176m"
59
+
60
+ # ── Brand icon ────────────────────────────────────────────────────────
61
+
62
+ # The cli-anything icon: a small colored diamond/chevron mark
63
+ _ICON = f"{_CYAN}{_BOLD}◆{_RESET}"
64
+ _ICON_SMALL = f"{_CYAN}▸{_RESET}"
65
+
66
+ # ── Box drawing characters ────────────────────────────────────────────
67
+
68
+ _H_LINE = "─"
69
+ _V_LINE = "│"
70
+ _TL = "╭"
71
+ _TR = "╮"
72
+ _BL = "╰"
73
+ _BR = "╯"
74
+ _T_DOWN = "┬"
75
+ _T_UP = "┴"
76
+ _T_RIGHT = "├"
77
+ _T_LEFT = "┤"
78
+ _CROSS = "┼"
79
+
80
+
81
+ def _strip_ansi(text: str) -> str:
82
+ """Remove ANSI escape codes for length calculation."""
83
+ import re
84
+ return re.sub(r"\033\[[^m]*m", "", text)
85
+
86
+
87
+ def _visible_len(text: str) -> int:
88
+ """Get visible length of text (excluding ANSI codes)."""
89
+ return len(_strip_ansi(text))
90
+
91
+
92
+ class ReplSkin:
93
+ """Unified REPL skin for cli-anything CLIs.
94
+
95
+ Provides consistent branding, prompts, and message formatting
96
+ across all CLI harnesses built with the cli-anything methodology.
97
+ """
98
+
99
+ def __init__(self, software: str, version: str = "1.0.0",
100
+ history_file: str | None = None):
101
+ """Initialize the REPL skin.
102
+
103
+ Args:
104
+ software: Software name (e.g., "gimp", "shotcut", "blender").
105
+ version: CLI version string.
106
+ history_file: Path for persistent command history.
107
+ Defaults to ~/.cli-anything-<software>/history
108
+ """
109
+ self.software = software.lower().replace("-", "_")
110
+ self.display_name = software.replace("_", " ").title()
111
+ self.version = version
112
+ self.accent = _ACCENT_COLORS.get(self.software, _DEFAULT_ACCENT)
113
+
114
+ # History file
115
+ if history_file is None:
116
+ from pathlib import Path
117
+ hist_dir = Path.home() / f".cli-anything-{self.software}"
118
+ hist_dir.mkdir(parents=True, exist_ok=True)
119
+ self.history_file = str(hist_dir / "history")
120
+ else:
121
+ self.history_file = history_file
122
+
123
+ # Detect terminal capabilities
124
+ self._color = self._detect_color_support()
125
+
126
+ def _detect_color_support(self) -> bool:
127
+ """Check if terminal supports color."""
128
+ if os.environ.get("NO_COLOR"):
129
+ return False
130
+ if os.environ.get("CLI_ANYTHING_NO_COLOR"):
131
+ return False
132
+ if not hasattr(sys.stdout, "isatty"):
133
+ return False
134
+ return sys.stdout.isatty()
135
+
136
+ def _c(self, code: str, text: str) -> str:
137
+ """Apply color code if colors are supported."""
138
+ if not self._color:
139
+ return text
140
+ return f"{code}{text}{_RESET}"
141
+
142
+ # ── Banner ────────────────────────────────────────────────────────
143
+
144
+ def print_banner(self):
145
+ """Print the startup banner with branding."""
146
+ inner = 54
147
+
148
+ def _box_line(content: str) -> str:
149
+ """Wrap content in box drawing, padding to inner width."""
150
+ pad = inner - _visible_len(content)
151
+ vl = self._c(_DARK_GRAY, _V_LINE)
152
+ return f"{vl}{content}{' ' * max(0, pad)}{vl}"
153
+
154
+ top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
155
+ bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
156
+
157
+ # Title: ◆ cli-anything · Shotcut
158
+ icon = self._c(_CYAN + _BOLD, "◆")
159
+ brand = self._c(_CYAN + _BOLD, "cli-anything")
160
+ dot = self._c(_DARK_GRAY, "·")
161
+ name = self._c(self.accent + _BOLD, self.display_name)
162
+ title = f" {icon} {brand} {dot} {name}"
163
+
164
+ ver = f" {self._c(_DARK_GRAY, f' v{self.version}')}"
165
+ tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
166
+ empty = ""
167
+
168
+ print(top)
169
+ print(_box_line(title))
170
+ print(_box_line(ver))
171
+ print(_box_line(empty))
172
+ print(_box_line(tip))
173
+ print(bot)
174
+ print()
175
+
176
+ # ── Prompt ────────────────────────────────────────────────────────
177
+
178
+ def prompt(self, project_name: str = "", modified: bool = False,
179
+ context: str = "") -> str:
180
+ """Build a styled prompt string for prompt_toolkit or input().
181
+
182
+ Args:
183
+ project_name: Current project name (empty if none open).
184
+ modified: Whether the project has unsaved changes.
185
+ context: Optional extra context to show in prompt.
186
+
187
+ Returns:
188
+ Formatted prompt string.
189
+ """
190
+ parts = []
191
+
192
+ # Icon
193
+ if self._color:
194
+ parts.append(f"{_CYAN}◆{_RESET} ")
195
+ else:
196
+ parts.append("> ")
197
+
198
+ # Software name
199
+ parts.append(self._c(self.accent + _BOLD, self.software))
200
+
201
+ # Project context
202
+ if project_name or context:
203
+ ctx = context or project_name
204
+ mod = "*" if modified else ""
205
+ parts.append(f" {self._c(_DARK_GRAY, '[')}")
206
+ parts.append(self._c(_LIGHT_GRAY, f"{ctx}{mod}"))
207
+ parts.append(self._c(_DARK_GRAY, ']'))
208
+
209
+ parts.append(self._c(_GRAY, " ❯ "))
210
+
211
+ return "".join(parts)
212
+
213
+ def prompt_tokens(self, project_name: str = "", modified: bool = False,
214
+ context: str = ""):
215
+ """Build prompt_toolkit formatted text tokens for the prompt.
216
+
217
+ Use with prompt_toolkit's FormattedText for proper ANSI handling.
218
+
219
+ Returns:
220
+ list of (style, text) tuples for prompt_toolkit.
221
+ """
222
+ accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff")
223
+ tokens = []
224
+
225
+ tokens.append(("class:icon", "◆ "))
226
+ tokens.append(("class:software", self.software))
227
+
228
+ if project_name or context:
229
+ ctx = context or project_name
230
+ mod = "*" if modified else ""
231
+ tokens.append(("class:bracket", " ["))
232
+ tokens.append(("class:context", f"{ctx}{mod}"))
233
+ tokens.append(("class:bracket", "]"))
234
+
235
+ tokens.append(("class:arrow", " ❯ "))
236
+
237
+ return tokens
238
+
239
+ def get_prompt_style(self):
240
+ """Get a prompt_toolkit Style object matching the skin.
241
+
242
+ Returns:
243
+ prompt_toolkit.styles.Style
244
+ """
245
+ try:
246
+ from prompt_toolkit.styles import Style
247
+ except ImportError:
248
+ return None
249
+
250
+ accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff")
251
+
252
+ return Style.from_dict({
253
+ "icon": "#5fdfdf bold", # cyan brand color
254
+ "software": f"{accent_hex} bold",
255
+ "bracket": "#585858",
256
+ "context": "#bcbcbc",
257
+ "arrow": "#808080",
258
+ # Completion menu
259
+ "completion-menu.completion": "bg:#303030 #bcbcbc",
260
+ "completion-menu.completion.current": f"bg:{accent_hex} #000000",
261
+ "completion-menu.meta.completion": "bg:#303030 #808080",
262
+ "completion-menu.meta.completion.current": f"bg:{accent_hex} #000000",
263
+ # Auto-suggest
264
+ "auto-suggest": "#585858",
265
+ # Bottom toolbar
266
+ "bottom-toolbar": "bg:#1c1c1c #808080",
267
+ "bottom-toolbar.text": "#808080",
268
+ })
269
+
270
+ # ── Messages ──────────────────────────────────────────────────────
271
+
272
+ def success(self, message: str):
273
+ """Print a success message with green checkmark."""
274
+ icon = self._c(_GREEN + _BOLD, "✓")
275
+ print(f" {icon} {self._c(_GREEN, message)}")
276
+
277
+ def error(self, message: str):
278
+ """Print an error message with red cross."""
279
+ icon = self._c(_RED + _BOLD, "✗")
280
+ print(f" {icon} {self._c(_RED, message)}", file=sys.stderr)
281
+
282
+ def warning(self, message: str):
283
+ """Print a warning message with yellow triangle."""
284
+ icon = self._c(_YELLOW + _BOLD, "⚠")
285
+ print(f" {icon} {self._c(_YELLOW, message)}")
286
+
287
+ def info(self, message: str):
288
+ """Print an info message with blue dot."""
289
+ icon = self._c(_BLUE, "●")
290
+ print(f" {icon} {self._c(_LIGHT_GRAY, message)}")
291
+
292
+ def hint(self, message: str):
293
+ """Print a subtle hint message."""
294
+ print(f" {self._c(_DARK_GRAY, message)}")
295
+
296
+ def section(self, title: str):
297
+ """Print a section header."""
298
+ print()
299
+ print(f" {self._c(self.accent + _BOLD, title)}")
300
+ print(f" {self._c(_DARK_GRAY, _H_LINE * len(title))}")
301
+
302
+ # ── Status display ────────────────────────────────────────────────
303
+
304
+ def status(self, label: str, value: str):
305
+ """Print a key-value status line."""
306
+ lbl = self._c(_GRAY, f" {label}:")
307
+ val = self._c(_WHITE, f" {value}")
308
+ print(f"{lbl}{val}")
309
+
310
+ def status_block(self, items: dict[str, str], title: str = ""):
311
+ """Print a block of status key-value pairs.
312
+
313
+ Args:
314
+ items: Dict of label -> value pairs.
315
+ title: Optional title for the block.
316
+ """
317
+ if title:
318
+ self.section(title)
319
+
320
+ max_key = max(len(k) for k in items) if items else 0
321
+ for label, value in items.items():
322
+ lbl = self._c(_GRAY, f" {label:<{max_key}}")
323
+ val = self._c(_WHITE, f" {value}")
324
+ print(f"{lbl}{val}")
325
+
326
+ def progress(self, current: int, total: int, label: str = ""):
327
+ """Print a simple progress indicator.
328
+
329
+ Args:
330
+ current: Current step number.
331
+ total: Total number of steps.
332
+ label: Optional label for the progress.
333
+ """
334
+ pct = int(current / total * 100) if total > 0 else 0
335
+ bar_width = 20
336
+ filled = int(bar_width * current / total) if total > 0 else 0
337
+ bar = "█" * filled + "░" * (bar_width - filled)
338
+ text = f" {self._c(_CYAN, bar)} {self._c(_GRAY, f'{pct:3d}%')}"
339
+ if label:
340
+ text += f" {self._c(_LIGHT_GRAY, label)}"
341
+ print(text)
342
+
343
+ # ── Table display ─────────────────────────────────────────────────
344
+
345
+ def table(self, headers: list[str], rows: list[list[str]],
346
+ max_col_width: int = 40):
347
+ """Print a formatted table with box-drawing characters.
348
+
349
+ Args:
350
+ headers: Column header strings.
351
+ rows: List of rows, each a list of cell strings.
352
+ max_col_width: Maximum column width before truncation.
353
+ """
354
+ if not headers:
355
+ return
356
+
357
+ # Calculate column widths
358
+ col_widths = [min(len(h), max_col_width) for h in headers]
359
+ for row in rows:
360
+ for i, cell in enumerate(row):
361
+ if i < len(col_widths):
362
+ col_widths[i] = min(
363
+ max(col_widths[i], len(str(cell))), max_col_width
364
+ )
365
+
366
+ def pad(text: str, width: int) -> str:
367
+ t = str(text)[:width]
368
+ return t + " " * (width - len(t))
369
+
370
+ # Header
371
+ header_cells = [
372
+ self._c(_CYAN + _BOLD, pad(h, col_widths[i]))
373
+ for i, h in enumerate(headers)
374
+ ]
375
+ sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
376
+ header_line = f" {sep.join(header_cells)}"
377
+ print(header_line)
378
+
379
+ # Separator
380
+ sep_parts = [self._c(_DARK_GRAY, _H_LINE * w) for w in col_widths]
381
+ sep_line = self._c(_DARK_GRAY, f" {'───'.join([_H_LINE * w for w in col_widths])}")
382
+ print(sep_line)
383
+
384
+ # Rows
385
+ for row in rows:
386
+ cells = []
387
+ for i, cell in enumerate(row):
388
+ if i < len(col_widths):
389
+ cells.append(self._c(_LIGHT_GRAY, pad(str(cell), col_widths[i])))
390
+ row_sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
391
+ print(f" {row_sep.join(cells)}")
392
+
393
+ # ── Help display ──────────────────────────────────────────────────
394
+
395
+ def help(self, commands: dict[str, str]):
396
+ """Print a formatted help listing.
397
+
398
+ Args:
399
+ commands: Dict of command -> description pairs.
400
+ """
401
+ self.section("Commands")
402
+ max_cmd = max(len(c) for c in commands) if commands else 0
403
+ for cmd, desc in commands.items():
404
+ cmd_styled = self._c(self.accent, f" {cmd:<{max_cmd}}")
405
+ desc_styled = self._c(_GRAY, f" {desc}")
406
+ print(f"{cmd_styled}{desc_styled}")
407
+ print()
408
+
409
+ # ── Goodbye ───────────────────────────────────────────────────────
410
+
411
+ def print_goodbye(self):
412
+ """Print a styled goodbye message."""
413
+ print(f"\n {_ICON_SMALL} {self._c(_GRAY, 'Goodbye!')}\n")
414
+
415
+ # ── Prompt toolkit session factory ────────────────────────────────
416
+
417
+ def create_prompt_session(self):
418
+ """Create a prompt_toolkit PromptSession with skin styling.
419
+
420
+ Returns:
421
+ A configured PromptSession, or None if prompt_toolkit unavailable.
422
+ """
423
+ try:
424
+ from prompt_toolkit import PromptSession
425
+ from prompt_toolkit.history import FileHistory
426
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
427
+ from prompt_toolkit.formatted_text import FormattedText
428
+
429
+ style = self.get_prompt_style()
430
+
431
+ session = PromptSession(
432
+ history=FileHistory(self.history_file),
433
+ auto_suggest=AutoSuggestFromHistory(),
434
+ style=style,
435
+ enable_history_search=True,
436
+ )
437
+ return session
438
+ except ImportError:
439
+ return None
440
+
441
+ def get_input(self, pt_session, project_name: str = "",
442
+ modified: bool = False, context: str = "") -> str:
443
+ """Get input from user using prompt_toolkit or fallback.
444
+
445
+ Args:
446
+ pt_session: A prompt_toolkit PromptSession (or None).
447
+ project_name: Current project name.
448
+ modified: Whether project has unsaved changes.
449
+ context: Optional context string.
450
+
451
+ Returns:
452
+ User input string (stripped).
453
+ """
454
+ if pt_session is not None:
455
+ from prompt_toolkit.formatted_text import FormattedText
456
+ tokens = self.prompt_tokens(project_name, modified, context)
457
+ return pt_session.prompt(FormattedText(tokens)).strip()
458
+ else:
459
+ raw_prompt = self.prompt(project_name, modified, context)
460
+ return input(raw_prompt).strip()
461
+
462
+ # ── Toolbar builder ───────────────────────────────────────────────
463
+
464
+ def bottom_toolbar(self, items: dict[str, str]):
465
+ """Create a bottom toolbar callback for prompt_toolkit.
466
+
467
+ Args:
468
+ items: Dict of label -> value pairs to show in toolbar.
469
+
470
+ Returns:
471
+ A callable that returns FormattedText for the toolbar.
472
+ """
473
+ def toolbar():
474
+ from prompt_toolkit.formatted_text import FormattedText
475
+ parts = []
476
+ for i, (k, v) in enumerate(items.items()):
477
+ if i > 0:
478
+ parts.append(("class:bottom-toolbar.text", " │ "))
479
+ parts.append(("class:bottom-toolbar.text", f" {k}: "))
480
+ parts.append(("class:bottom-toolbar", v))
481
+ return FormattedText(parts)
482
+ return toolbar
483
+
484
+
485
+ # ── ANSI 256-color to hex mapping (for prompt_toolkit styles) ─────────
486
+
487
+ _ANSI_256_TO_HEX = {
488
+ "\033[38;5;33m": "#0087ff", # audacity navy blue
489
+ "\033[38;5;35m": "#00af5f", # shotcut teal
490
+ "\033[38;5;39m": "#00afff", # inkscape bright blue
491
+ "\033[38;5;40m": "#00d700", # libreoffice green
492
+ "\033[38;5;55m": "#5f00af", # obs purple
493
+ "\033[38;5;69m": "#5f87ff", # kdenlive slate blue
494
+ "\033[38;5;75m": "#5fafff", # default sky blue
495
+ "\033[38;5;80m": "#5fd7d7", # brand cyan
496
+ "\033[38;5;208m": "#ff8700", # blender deep orange
497
+ "\033[38;5;214m": "#ffaf00", # gimp warm orange
498
+ }
@@ -0,0 +1,202 @@
1
+ # GIMP CLI
2
+
3
+ A stateful command-line interface for image editing, built on Pillow.
4
+ Designed for AI agents and power users who need to create and manipulate
5
+ images without a GUI.
6
+
7
+ ## Prerequisites
8
+
9
+ - Python 3.10+
10
+ - `Pillow` (image processing)
11
+ - `click` (CLI framework)
12
+ - `numpy` (blend modes, pixel analysis)
13
+
14
+ Optional (for interactive REPL):
15
+ - `prompt_toolkit`
16
+
17
+ ## Install Dependencies
18
+
19
+ ```bash
20
+ pip install Pillow click numpy prompt_toolkit
21
+ ```
22
+
23
+ ## How to Run
24
+
25
+ All commands are run from the `agent-harness/` directory.
26
+
27
+ ### One-shot commands
28
+
29
+ ```bash
30
+ # Show help
31
+ python3 -m cli.gimp_cli --help
32
+
33
+ # Create a new project
34
+ python3 -m cli.gimp_cli project new --width 1920 --height 1080 -o my_project.json
35
+
36
+ # Create with a profile
37
+ python3 -m cli.gimp_cli project new --profile hd720p -o project.json
38
+
39
+ # Open a project and show info
40
+ python3 -m cli.gimp_cli --project project.json project info
41
+
42
+ # JSON output (for agent consumption)
43
+ python3 -m cli.gimp_cli --json --project project.json project info
44
+ ```
45
+
46
+ ### Interactive REPL
47
+
48
+ ```bash
49
+ python3 -m cli.gimp_cli repl
50
+ python3 -m cli.gimp_cli repl --project my_project.json
51
+ ```
52
+
53
+ Inside the REPL, type `help` for all available commands.
54
+
55
+ ## Command Reference
56
+
57
+ ### Project
58
+
59
+ ```bash
60
+ project new [--width W] [--height H] [--mode RGB|RGBA|L|LA] [--profile P] [-o path]
61
+ project open <path>
62
+ project save [path]
63
+ project info
64
+ project profiles
65
+ project json
66
+ ```
67
+
68
+ Available profiles: `hd1080p`, `hd720p`, `4k`, `square1080`, `a4_300dpi`, `a4_150dpi`,
69
+ `letter_300dpi`, `web_banner`, `instagram_post`, `instagram_story`, `twitter_header`,
70
+ `youtube_thumb`, `icon_256`, `icon_512`
71
+
72
+ ### Layer
73
+
74
+ ```bash
75
+ layer new [--name N] [--type image|text|solid] [--fill F] [--opacity O] [--mode M]
76
+ layer add-from-file <path> [--name N] [--position P] [--opacity O] [--mode M]
77
+ layer list
78
+ layer remove <index>
79
+ layer duplicate <index>
80
+ layer move <index> --to <position>
81
+ layer set <index> <property> <value>
82
+ layer flatten
83
+ layer merge-down <index>
84
+ ```
85
+
86
+ Layer properties: `name`, `opacity` (0.0-1.0), `visible` (true/false),
87
+ `mode` (blend mode), `offset_x`, `offset_y`
88
+
89
+ ### Canvas
90
+
91
+ ```bash
92
+ canvas info
93
+ canvas resize --width W --height H [--anchor center|top-left|...]
94
+ canvas scale --width W --height H [--resample lanczos|bicubic|bilinear|nearest]
95
+ canvas crop --left L --top T --right R --bottom B
96
+ canvas mode <RGB|RGBA|L|LA|CMYK|P>
97
+ canvas dpi <value>
98
+ ```
99
+
100
+ ### Filters
101
+
102
+ ```bash
103
+ filter list-available [--category adjustment|blur|stylize|transform]
104
+ filter info <name>
105
+ filter add <name> [--layer L] [--param key=value ...]
106
+ filter remove <index> [--layer L]
107
+ filter set <index> <param> <value> [--layer L]
108
+ filter list [--layer L]
109
+ ```
110
+
111
+ Available filters:
112
+ - **Adjustments**: brightness, contrast, saturation, sharpness, autocontrast,
113
+ equalize, invert, posterize, solarize, grayscale, sepia
114
+ - **Blur**: gaussian_blur, box_blur, unsharp_mask, smooth
115
+ - **Stylize**: find_edges, emboss, contour, detail
116
+ - **Transform**: rotate, flip_h, flip_v, resize, crop
117
+
118
+ ### Media
119
+
120
+ ```bash
121
+ media probe <file>
122
+ media list
123
+ media check
124
+ media histogram <file>
125
+ ```
126
+
127
+ ### Export
128
+
129
+ ```bash
130
+ export presets
131
+ export preset-info <name>
132
+ export render <output> [--preset name] [--overwrite] [--quality Q] [--format F]
133
+ ```
134
+
135
+ Available presets: `png`, `png-max`, `jpeg-high`, `jpeg-medium`, `jpeg-low`,
136
+ `webp`, `webp-lossless`, `tiff`, `tiff-none`, `bmp`, `gif`, `pdf`, `ico`
137
+
138
+ ### Draw
139
+
140
+ ```bash
141
+ draw text --layer L --text "Hello" [--x X] [--y Y] [--font F] [--size S] [--color C]
142
+ draw rect --layer L --x1 X --y1 Y --x2 X --y2 Y [--fill C] [--outline C]
143
+ ```
144
+
145
+ ### Session
146
+
147
+ ```bash
148
+ session status
149
+ session undo
150
+ session redo
151
+ session history
152
+ ```
153
+
154
+ ## JSON Mode
155
+
156
+ Add `--json` before the subcommand for machine-readable output:
157
+
158
+ ```bash
159
+ python3 -m cli.gimp_cli --json --project p.json layer list
160
+ ```
161
+
162
+ ## Running Tests
163
+
164
+ ```bash
165
+ cd agent-harness
166
+ python3 -m pytest cli/tests/test_core.py -v # Unit tests (no images needed)
167
+ python3 -m pytest cli/tests/test_full_e2e.py -v # E2E tests (creates test images)
168
+ python3 -m pytest cli/tests/ -v # All tests
169
+ ```
170
+
171
+ ## Example Workflow
172
+
173
+ ```bash
174
+ # Create a project
175
+ python3 -m cli.gimp_cli project new --width 1920 --height 1080 --profile hd1080p -o edit.json
176
+
177
+ # Add an image layer
178
+ python3 -m cli.gimp_cli --project edit.json layer add-from-file photo.jpg --name "Background"
179
+
180
+ # Apply filters
181
+ python3 -m cli.gimp_cli --project edit.json filter add brightness --layer 0 --param factor=1.2
182
+ python3 -m cli.gimp_cli --project edit.json filter add contrast --layer 0 --param factor=1.1
183
+ python3 -m cli.gimp_cli --project edit.json filter add saturation --layer 0 --param factor=1.3
184
+
185
+ # Add a text overlay
186
+ python3 -m cli.gimp_cli --project edit.json layer new --type text --name "Title"
187
+ python3 -m cli.gimp_cli --project edit.json draw text --layer 0 --text "My Photo" --size 48 --color "#ffffff"
188
+
189
+ # View the layer stack
190
+ python3 -m cli.gimp_cli --project edit.json layer list
191
+
192
+ # Save and render
193
+ python3 -m cli.gimp_cli --project edit.json project save
194
+ python3 -m cli.gimp_cli --project edit.json export render output.jpg --preset jpeg-high --overwrite
195
+ ```
196
+
197
+ ## Blend Modes
198
+
199
+ Supported blend modes for layer compositing:
200
+ `normal`, `multiply`, `screen`, `overlay`, `soft_light`, `hard_light`,
201
+ `difference`, `darken`, `lighten`, `color_dodge`, `color_burn`,
202
+ `addition`, `subtract`, `grain_merge`, `grain_extract`
@@ -0,0 +1 @@
1
+ """GIMP CLI - A stateful CLI for image editing."""