devlyn-cli 0.5.2 → 0.5.4

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 (35) hide show
  1. package/bin/devlyn.js +1 -0
  2. package/config/commands/devlyn.team-resolve.md +31 -2
  3. package/optional-skills/dokkit/ANALYSIS.md +198 -0
  4. package/optional-skills/dokkit/COMMANDS.md +365 -0
  5. package/optional-skills/dokkit/DOCX-XML.md +76 -0
  6. package/optional-skills/dokkit/EXPORT.md +102 -0
  7. package/optional-skills/dokkit/FILLING.md +377 -0
  8. package/optional-skills/dokkit/HWPX-XML.md +73 -0
  9. package/optional-skills/dokkit/IMAGE-SOURCING.md +127 -0
  10. package/optional-skills/dokkit/INGESTION.md +65 -0
  11. package/optional-skills/dokkit/SKILL.md +153 -0
  12. package/optional-skills/dokkit/STATE.md +60 -0
  13. package/optional-skills/dokkit/references/docx-field-patterns.md +151 -0
  14. package/optional-skills/dokkit/references/docx-structure.md +58 -0
  15. package/optional-skills/dokkit/references/field-detection-patterns.md +130 -0
  16. package/optional-skills/dokkit/references/hwpx-field-patterns.md +461 -0
  17. package/optional-skills/dokkit/references/hwpx-structure.md +159 -0
  18. package/optional-skills/dokkit/references/image-opportunity-heuristics.md +121 -0
  19. package/optional-skills/dokkit/references/image-xml-patterns.md +338 -0
  20. package/optional-skills/dokkit/references/section-image-interleaving.md +346 -0
  21. package/optional-skills/dokkit/references/section-range-detection.md +118 -0
  22. package/optional-skills/dokkit/references/state-schema.md +143 -0
  23. package/optional-skills/dokkit/references/supported-formats.md +67 -0
  24. package/optional-skills/dokkit/scripts/compile_hwpx.py +134 -0
  25. package/optional-skills/dokkit/scripts/detect_fields.py +301 -0
  26. package/optional-skills/dokkit/scripts/detect_fields_hwpx.py +286 -0
  27. package/optional-skills/dokkit/scripts/export_pdf.py +99 -0
  28. package/optional-skills/dokkit/scripts/parse_hwpx.py +185 -0
  29. package/optional-skills/dokkit/scripts/parse_image_with_gemini.py +159 -0
  30. package/optional-skills/dokkit/scripts/parse_xlsx.py +98 -0
  31. package/optional-skills/dokkit/scripts/source_images.py +365 -0
  32. package/optional-skills/dokkit/scripts/validate_docx.py +142 -0
  33. package/optional-skills/dokkit/scripts/validate_hwpx.py +281 -0
  34. package/optional-skills/dokkit/scripts/validate_state.py +132 -0
  35. package/package.json +1 -1
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python3
2
+ """Parse image files using Google Gemini Vision API for OCR and content extraction.
3
+
4
+ Usage:
5
+ python parse_image_with_gemini.py <input-image> [--project-dir <dir>]
6
+
7
+ Output:
8
+ JSON to stdout with 'content_md' and 'metadata' fields.
9
+
10
+ Requires:
11
+ GEMINI_API_KEY in .env or environment variables.
12
+ """
13
+
14
+ import base64
15
+ import json
16
+ import os
17
+ import sys
18
+ import urllib.error
19
+ import urllib.request
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+
23
+
24
+ def load_api_key(project_dir: Path) -> str:
25
+ """Load Gemini API key from .env or environment."""
26
+ # Check environment first
27
+ key = os.environ.get("GEMINI_API_KEY", "")
28
+ if key:
29
+ return key
30
+
31
+ # Check .env file
32
+ env_path = project_dir / ".env"
33
+ if env_path.exists():
34
+ with open(env_path, encoding="utf-8") as f:
35
+ for line in f:
36
+ line = line.strip()
37
+ if line.startswith("GEMINI_API_KEY="):
38
+ return line.split("=", 1)[1].strip()
39
+
40
+ return ""
41
+
42
+
43
+ def parse_image(file_path: str, project_dir: str = ".") -> dict:
44
+ """Parse an image using Gemini Vision API."""
45
+ path = Path(file_path)
46
+ proj = Path(project_dir).resolve()
47
+
48
+ api_key = load_api_key(proj)
49
+ if not api_key:
50
+ return {"error": "GEMINI_API_KEY not configured. Set it in .env or environment."}
51
+
52
+ # Read and encode image
53
+ with open(path, "rb") as f:
54
+ image_data = base64.b64encode(f.read()).decode("utf-8")
55
+
56
+ # Determine MIME type
57
+ ext = path.suffix.lower()
58
+ mime_map = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
59
+ ".webp": "image/webp", ".gif": "image/gif"}
60
+ mime_type = mime_map.get(ext, "image/png")
61
+
62
+ # Call Gemini Vision
63
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"
64
+
65
+ payload = {
66
+ "contents": [{
67
+ "parts": [
68
+ {"text": (
69
+ "Extract ALL text from this image. Preserve the layout as much as possible. "
70
+ "If there are tables, convert them to markdown tables. "
71
+ "If there are form fields, identify labels and values. "
72
+ "Output the extracted content as clean markdown. "
73
+ "Also identify any key-value pairs (like Name: John) and list them at the end "
74
+ "in a section called '## Extracted Key-Value Pairs' as a markdown table."
75
+ )},
76
+ {"inlineData": {"mimeType": mime_type, "data": image_data}}
77
+ ]
78
+ }]
79
+ }
80
+
81
+ req = urllib.request.Request(
82
+ url,
83
+ data=json.dumps(payload).encode("utf-8"),
84
+ headers={"Content-Type": "application/json", "x-goog-api-key": api_key},
85
+ method="POST",
86
+ )
87
+
88
+ try:
89
+ with urllib.request.urlopen(req, timeout=60) as resp:
90
+ result = json.loads(resp.read().decode("utf-8"))
91
+ except urllib.error.HTTPError as e:
92
+ body = e.read().decode("utf-8", errors="replace")
93
+ return {"error": f"Gemini API error ({e.code}): {body}"}
94
+ except urllib.error.URLError as e:
95
+ return {"error": f"Gemini API connection error: {e}"}
96
+
97
+ # Extract text from response
98
+ candidates = result.get("candidates", [])
99
+ if not candidates:
100
+ return {"error": "Gemini returned no response"}
101
+
102
+ parts = candidates[0].get("content", {}).get("parts", [])
103
+ extracted_text = ""
104
+ for part in parts:
105
+ if "text" in part:
106
+ extracted_text += part["text"]
107
+
108
+ if not extracted_text.strip():
109
+ return {"error": "No text could be extracted from the image"}
110
+
111
+ # Parse key-value pairs from the extracted text
112
+ key_value_pairs = {}
113
+ lines = extracted_text.split("\n")
114
+ for line in lines:
115
+ if ":" in line and not line.startswith("#"):
116
+ parts_split = line.split(":", 1)
117
+ label = parts_split[0].strip().strip("|").strip()
118
+ value = parts_split[1].strip().strip("|").strip()
119
+ if label and value and len(label) < 50:
120
+ key_value_pairs[label] = value
121
+
122
+ content_md = f"# {path.stem}\n\n{extracted_text}"
123
+
124
+ return {
125
+ "content_md": content_md,
126
+ "metadata": {
127
+ "file_name": path.name,
128
+ "file_type": ext.lstrip("."),
129
+ "parse_date": datetime.now().isoformat(),
130
+ "key_value_pairs": key_value_pairs,
131
+ "sections": ["OCR Content"],
132
+ "parse_method": "gemini_vision",
133
+ }
134
+ }
135
+
136
+
137
+ def main():
138
+ if len(sys.argv) < 2:
139
+ print("Usage: python parse_image_with_gemini.py <image> [--project-dir <dir>]",
140
+ file=sys.stderr)
141
+ sys.exit(1)
142
+
143
+ file_path = sys.argv[1]
144
+ project_dir = "."
145
+ if "--project-dir" in sys.argv:
146
+ idx = sys.argv.index("--project-dir")
147
+ if idx + 1 < len(sys.argv):
148
+ project_dir = sys.argv[idx + 1]
149
+
150
+ if not Path(file_path).exists():
151
+ print(json.dumps({"error": f"File not found: {file_path}"}))
152
+ sys.exit(1)
153
+
154
+ result = parse_image(file_path, project_dir)
155
+ print(json.dumps(result, ensure_ascii=False, indent=2))
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ """Parse XLSX files into Dokkit's dual-file format (Markdown + JSON sidecar).
3
+
4
+ Usage:
5
+ python parse_xlsx.py <input.xlsx>
6
+
7
+ Output:
8
+ JSON to stdout with 'content_md' and 'metadata' fields.
9
+
10
+ Requires:
11
+ pip install openpyxl
12
+ """
13
+
14
+ import json
15
+ import sys
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+
19
+ try:
20
+ import openpyxl
21
+ except ImportError:
22
+ print(json.dumps({
23
+ "error": "openpyxl not installed. Run: pip install openpyxl"
24
+ }))
25
+ sys.exit(1)
26
+
27
+
28
+ def parse_xlsx(file_path: str) -> dict:
29
+ """Parse an XLSX file and return content + metadata."""
30
+ path = Path(file_path)
31
+ wb = openpyxl.load_workbook(path, data_only=True)
32
+
33
+ sections = []
34
+ all_content = []
35
+ key_value_pairs = {}
36
+
37
+ for sheet_name in wb.sheetnames:
38
+ ws = wb[sheet_name]
39
+ sections.append(sheet_name)
40
+ all_content.append(f"## {sheet_name}\n")
41
+
42
+ rows = list(ws.iter_rows(values_only=True))
43
+ if not rows:
44
+ all_content.append("*(empty sheet)*\n")
45
+ continue
46
+
47
+ # Detect if first row is a header
48
+ headers = [str(c) if c is not None else "" for c in rows[0]]
49
+
50
+ # Build markdown table
51
+ all_content.append("| " + " | ".join(headers) + " |")
52
+ all_content.append("| " + " | ".join(["---"] * len(headers)) + " |")
53
+
54
+ for row in rows[1:]:
55
+ cells = [str(c) if c is not None else "" for c in row]
56
+ all_content.append("| " + " | ".join(cells) + " |")
57
+
58
+ # Extract key-value pairs from 2-column patterns
59
+ if len(cells) >= 2 and cells[0] and cells[1]:
60
+ # If first column looks like a label (short text, no numbers)
61
+ label = cells[0].strip()
62
+ value = cells[1].strip()
63
+ if len(label) < 50 and not label.replace(" ", "").isdigit():
64
+ key_value_pairs[label] = value
65
+
66
+ all_content.append("")
67
+
68
+ content_md = f"# {path.stem}\n\n" + "\n".join(all_content)
69
+
70
+ return {
71
+ "content_md": content_md,
72
+ "metadata": {
73
+ "file_name": path.name,
74
+ "file_type": "xlsx",
75
+ "parse_date": datetime.now().isoformat(),
76
+ "key_value_pairs": key_value_pairs,
77
+ "sections": sections,
78
+ "sheet_count": len(wb.sheetnames),
79
+ }
80
+ }
81
+
82
+
83
+ def main():
84
+ if len(sys.argv) != 2:
85
+ print("Usage: python parse_xlsx.py <input.xlsx>", file=sys.stderr)
86
+ sys.exit(1)
87
+
88
+ file_path = sys.argv[1]
89
+ if not Path(file_path).exists():
90
+ print(json.dumps({"error": f"File not found: {file_path}"}))
91
+ sys.exit(1)
92
+
93
+ result = parse_xlsx(file_path)
94
+ print(json.dumps(result, ensure_ascii=False, indent=2))
95
+
96
+
97
+ if __name__ == "__main__":
98
+ main()
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env python3
2
+ """Generate or search images for dokkit template filling.
3
+
4
+ Usage:
5
+ python source_images.py generate \\
6
+ --prompt "인포그래픽 제목: AI 감정 케어 플랫폼" \\
7
+ --preset infographic \\
8
+ --output-dir .dokkit/images/ \\
9
+ --project-dir . \\
10
+ [--lang ko] \\
11
+ [--aspect-ratio 16:9] \\
12
+ [--no-enhance]
13
+
14
+ python source_images.py search \\
15
+ --query "company logo example" \\
16
+ --output-dir .dokkit/images/
17
+
18
+ Language options (--lang):
19
+ ko Korean only (default). All text in generated images will be Korean.
20
+ en English only. All text in generated images will be English.
21
+ ko+en Mixed Korean and English. Titles in Korean, technical terms in English.
22
+ ja Japanese only.
23
+ <code> Any ISO 639-1 language code.
24
+ <a>+<b> Mixed: primary language + secondary language.
25
+
26
+ Output:
27
+ Prints __RESULT__ JSON to stdout:
28
+ {"image_id": "...", "file_path": "...", "source_type": "generated"|"searched"}
29
+
30
+ Requires:
31
+ GEMINI_API_KEY in .env or environment variables.
32
+ """
33
+
34
+ import base64
35
+ import json
36
+ import os
37
+ import sys
38
+ import urllib.error
39
+ import urllib.request
40
+ import uuid
41
+ from pathlib import Path
42
+
43
+ # Model for image generation
44
+ IMAGE_MODEL = "gemini-3-pro-image-preview"
45
+
46
+ # Language display names for prompt injection
47
+ LANG_NAMES = {
48
+ "ko": "한국어",
49
+ "en": "English",
50
+ "ja": "日本語",
51
+ "zh": "中文",
52
+ "es": "español",
53
+ "fr": "français",
54
+ "de": "Deutsch",
55
+ "pt": "português",
56
+ }
57
+
58
+ # Preset-to-style mapping for prompt enhancement
59
+ PRESETS = {
60
+ "technical_illustration": {
61
+ "style": "깔끔한 기술 다이어그램 스타일. 선명한 선, 레이블이 있는 구성요소, 전문적인 색상.",
62
+ "aspect_ratio": "16:9",
63
+ },
64
+ "infographic": {
65
+ "style": "전문적인 인포그래픽 스타일. 아이콘 기반, 깔끔한 레이아웃, 기업용 색상 팔레트.",
66
+ "aspect_ratio": "16:9",
67
+ },
68
+ "photorealistic": {
69
+ "style": "사실적인 사진 스타일. 고품질, 자연스러운 조명.",
70
+ "aspect_ratio": "4:3",
71
+ },
72
+ "concept": {
73
+ "style": "개념적 일러스트레이션 스타일. 추상적/모던, 비즈니스 제안서에 적합.",
74
+ "aspect_ratio": "1:1",
75
+ },
76
+ "chart": {
77
+ "style": "깔끔한 차트/그래프 스타일. 정확한 데이터 시각화, 전문적 색상.",
78
+ "aspect_ratio": "16:9",
79
+ },
80
+ }
81
+
82
+
83
+ def load_api_key(project_dir: Path) -> str:
84
+ """Load Gemini API key from .env or environment."""
85
+ key = os.environ.get("GEMINI_API_KEY", "")
86
+ if key:
87
+ return key
88
+ env_path = project_dir / ".env"
89
+ if env_path.exists():
90
+ with open(env_path, encoding="utf-8") as f:
91
+ for line in f:
92
+ line = line.strip()
93
+ if line.startswith("GEMINI_API_KEY="):
94
+ return line.split("=", 1)[1].strip()
95
+ return ""
96
+
97
+
98
+ def build_lang_instruction(lang: str) -> str:
99
+ """Build language instruction to append to the prompt.
100
+
101
+ Args:
102
+ lang: Language code. 'ko', 'en', 'ko+en', etc.
103
+
104
+ Returns:
105
+ Instruction string to append to the prompt.
106
+ """
107
+ if "+" in lang:
108
+ parts = lang.split("+", 1)
109
+ primary = parts[0].strip()
110
+ secondary = parts[1].strip()
111
+ primary_name = LANG_NAMES.get(primary, primary)
112
+ secondary_name = LANG_NAMES.get(secondary, secondary)
113
+ return (
114
+ f"\n\n[언어 규칙] 이미지의 텍스트는 {primary_name}를 기본으로 하되, "
115
+ f"기술 용어나 고유명사는 {secondary_name}를 사용할 수 있습니다. "
116
+ f"제목과 설명은 반드시 {primary_name}로 작성하세요."
117
+ )
118
+ else:
119
+ lang_name = LANG_NAMES.get(lang, lang)
120
+ if lang == "ko":
121
+ return (
122
+ "\n\n[언어 규칙] 이미지의 모든 텍스트는 반드시 한국어로만 작성해야 합니다. "
123
+ "영어 텍스트를 절대 사용하지 마세요. 제목, 라벨, 설명, 주석 등 "
124
+ "모든 텍스트 요소를 한국어로 작성하세요."
125
+ )
126
+ elif lang == "en":
127
+ return (
128
+ "\n\n[Language Rule] All text in the image must be written in English only. "
129
+ "Do not use any other language. Titles, labels, descriptions, and annotations "
130
+ "must all be in English."
131
+ )
132
+ else:
133
+ return (
134
+ f"\n\n[Language Rule] All text in the image must be written in {lang_name} only. "
135
+ f"Do not use any other language."
136
+ )
137
+
138
+
139
+ def enhance_prompt(prompt: str, preset: str, lang: str, no_enhance: bool) -> str:
140
+ """Enhance the prompt with preset style and language instructions.
141
+
142
+ Args:
143
+ prompt: User-provided prompt.
144
+ preset: Preset name (e.g., 'infographic', 'technical_illustration').
145
+ lang: Language code.
146
+ no_enhance: If True, skip preset style enhancement (still apply lang).
147
+
148
+ Returns:
149
+ Enhanced prompt string.
150
+ """
151
+ parts = []
152
+
153
+ if not no_enhance and preset in PRESETS:
154
+ parts.append(f"[스타일] {PRESETS[preset]['style']}")
155
+
156
+ parts.append(prompt)
157
+ parts.append(build_lang_instruction(lang))
158
+
159
+ return "\n\n".join(parts)
160
+
161
+
162
+ def generate_image(
163
+ prompt: str,
164
+ preset: str,
165
+ output_dir: str,
166
+ project_dir: str = ".",
167
+ lang: str = "ko",
168
+ aspect_ratio: str = "",
169
+ no_enhance: bool = False,
170
+ ) -> dict:
171
+ """Generate an image using Gemini image generation model.
172
+
173
+ Args:
174
+ prompt: Image generation prompt.
175
+ preset: Style preset name.
176
+ output_dir: Directory to save the generated image.
177
+ project_dir: Project root (for .env lookup).
178
+ lang: Language code for text in images.
179
+ aspect_ratio: Override aspect ratio (e.g., '16:9', '4:3').
180
+ no_enhance: Skip preset style enhancement.
181
+
182
+ Returns:
183
+ Result dict with image_id, file_path, source_type.
184
+ """
185
+ proj = Path(project_dir).resolve()
186
+ api_key = load_api_key(proj)
187
+ if not api_key:
188
+ return {"error": "GEMINI_API_KEY not configured. Set it in .env or environment."}
189
+
190
+ # Enhance prompt
191
+ full_prompt = enhance_prompt(prompt, preset, lang, no_enhance)
192
+
193
+ # Resolve aspect ratio
194
+ if not aspect_ratio and preset in PRESETS:
195
+ aspect_ratio = PRESETS[preset]["aspect_ratio"]
196
+ if not aspect_ratio:
197
+ aspect_ratio = "16:9"
198
+
199
+ # Build request
200
+ url = (
201
+ f"https://generativelanguage.googleapis.com/v1beta/models/"
202
+ f"{IMAGE_MODEL}:generateContent?key={api_key}"
203
+ )
204
+
205
+ # Add aspect ratio hint to prompt
206
+ ratio_hint = f"\n\n[이미지 비율] {aspect_ratio} 비율로 생성해주세요."
207
+ full_prompt += ratio_hint
208
+
209
+ payload = {
210
+ "contents": [{"parts": [{"text": full_prompt}]}],
211
+ "generationConfig": {"responseModalities": ["image", "text"]},
212
+ }
213
+
214
+ data = json.dumps(payload).encode("utf-8")
215
+ req = urllib.request.Request(
216
+ url, data=data, headers={"Content-Type": "application/json"}
217
+ )
218
+
219
+ try:
220
+ with urllib.request.urlopen(req, timeout=180) as resp:
221
+ result = json.loads(resp.read().decode("utf-8"))
222
+ except urllib.error.HTTPError as e:
223
+ body = e.read().decode("utf-8", errors="replace")[:500]
224
+ return {"error": f"Gemini API error ({e.code}): {body}"}
225
+ except urllib.error.URLError as e:
226
+ return {"error": f"Gemini API connection error: {e}"}
227
+
228
+ # Extract image from response
229
+ candidates = result.get("candidates", [])
230
+ if not candidates:
231
+ return {"error": "Gemini returned no candidates"}
232
+
233
+ parts = candidates[0].get("content", {}).get("parts", [])
234
+ for part in parts:
235
+ if "inlineData" in part:
236
+ img_b64 = part["inlineData"].get("data", "")
237
+ mime = part["inlineData"].get("mimeType", "image/png")
238
+ if img_b64:
239
+ img_bytes = base64.b64decode(img_b64)
240
+ # Determine extension
241
+ ext = ".png" if "png" in mime else ".jpg"
242
+ image_id = f"gen_{uuid.uuid4().hex[:8]}"
243
+ filename = f"{image_id}{ext}"
244
+
245
+ out_path = Path(output_dir)
246
+ out_path.mkdir(parents=True, exist_ok=True)
247
+ file_path = out_path / filename
248
+
249
+ with open(file_path, "wb") as f:
250
+ f.write(img_bytes)
251
+
252
+ return {
253
+ "image_id": image_id,
254
+ "file_path": str(file_path),
255
+ "source_type": "generated",
256
+ "file_size": len(img_bytes),
257
+ "lang": lang,
258
+ "preset": preset,
259
+ "model": IMAGE_MODEL,
260
+ }
261
+
262
+ return {"error": "No image data in Gemini response"}
263
+
264
+
265
+ def search_image(query: str, output_dir: str) -> dict:
266
+ """Search for an image (placeholder — not yet implemented).
267
+
268
+ Image search requires additional API setup. For now, returns an error
269
+ directing the user to provide images manually.
270
+ """
271
+ return {
272
+ "error": (
273
+ "Image search is not yet implemented. "
274
+ "Please provide images manually via '/dokkit modify \"use <file>\"'."
275
+ )
276
+ }
277
+
278
+
279
+ def parse_args(argv: list) -> dict:
280
+ """Parse command-line arguments."""
281
+ if len(argv) < 2:
282
+ return {"error": "Usage: source_images.py <generate|search> [options]"}
283
+
284
+ command = argv[1]
285
+ args = {"command": command}
286
+
287
+ i = 2
288
+ while i < len(argv):
289
+ arg = argv[i]
290
+ if arg == "--prompt" and i + 1 < len(argv):
291
+ args["prompt"] = argv[i + 1]
292
+ i += 2
293
+ elif arg == "--preset" and i + 1 < len(argv):
294
+ args["preset"] = argv[i + 1]
295
+ i += 2
296
+ elif arg == "--output-dir" and i + 1 < len(argv):
297
+ args["output_dir"] = argv[i + 1]
298
+ i += 2
299
+ elif arg == "--project-dir" and i + 1 < len(argv):
300
+ args["project_dir"] = argv[i + 1]
301
+ i += 2
302
+ elif arg == "--lang" and i + 1 < len(argv):
303
+ args["lang"] = argv[i + 1]
304
+ i += 2
305
+ elif arg == "--aspect-ratio" and i + 1 < len(argv):
306
+ args["aspect_ratio"] = argv[i + 1]
307
+ i += 2
308
+ elif arg == "--query" and i + 1 < len(argv):
309
+ args["query"] = argv[i + 1]
310
+ i += 2
311
+ elif arg == "--no-enhance":
312
+ args["no_enhance"] = True
313
+ i += 1
314
+ else:
315
+ i += 1
316
+
317
+ return args
318
+
319
+
320
+ def main():
321
+ args = parse_args(sys.argv)
322
+
323
+ if "error" in args:
324
+ print(json.dumps(args), file=sys.stderr)
325
+ sys.exit(1)
326
+
327
+ command = args.get("command")
328
+
329
+ if command == "generate":
330
+ prompt = args.get("prompt")
331
+ if not prompt:
332
+ print(json.dumps({"error": "Missing --prompt"}), file=sys.stderr)
333
+ sys.exit(1)
334
+
335
+ result = generate_image(
336
+ prompt=prompt,
337
+ preset=args.get("preset", "infographic"),
338
+ output_dir=args.get("output_dir", ".dokkit/images/"),
339
+ project_dir=args.get("project_dir", "."),
340
+ lang=args.get("lang", "ko"),
341
+ aspect_ratio=args.get("aspect_ratio", ""),
342
+ no_enhance=args.get("no_enhance", False),
343
+ )
344
+
345
+ elif command == "search":
346
+ query = args.get("query")
347
+ if not query:
348
+ print(json.dumps({"error": "Missing --query"}), file=sys.stderr)
349
+ sys.exit(1)
350
+ result = search_image(
351
+ query=query,
352
+ output_dir=args.get("output_dir", ".dokkit/images/"),
353
+ )
354
+ else:
355
+ result = {"error": f"Unknown command: {command}. Use 'generate' or 'search'."}
356
+
357
+ # Output result with __RESULT__ marker for agent parsing
358
+ print(f"__RESULT__{json.dumps(result, ensure_ascii=False)}")
359
+
360
+ if "error" in result:
361
+ sys.exit(1)
362
+
363
+
364
+ if __name__ == "__main__":
365
+ main()