lokuma-cli 1.4.9 → 1.4.10
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.
- package/assets/design.py +124 -1
- package/package.json +1 -1
package/assets/design.py
CHANGED
|
@@ -235,6 +235,129 @@ def _format_output(result: dict, fmt: str) -> str:
|
|
|
235
235
|
return "\n".join(lines) + IMAGE_REMINDER
|
|
236
236
|
|
|
237
237
|
|
|
238
|
+
# ─────────────────────────────────────────────
|
|
239
|
+
# SSE streaming client
|
|
240
|
+
# ─────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
_DOMAIN_LABELS = {
|
|
243
|
+
"style": "Visual Style",
|
|
244
|
+
"color": "Color Palette",
|
|
245
|
+
"typography": "Typography",
|
|
246
|
+
"product": "Product Type",
|
|
247
|
+
"reasoning": "Design Reasoning",
|
|
248
|
+
"ux": "UX Guidelines",
|
|
249
|
+
"chart": "Charts",
|
|
250
|
+
"landing": "Landing Pattern",
|
|
251
|
+
"icons": "Icon Library",
|
|
252
|
+
"google-fonts": "Google Fonts",
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _post_stream(endpoint: str, payload: dict) -> dict:
|
|
257
|
+
"""
|
|
258
|
+
POST to a streaming SSE endpoint.
|
|
259
|
+
Prints progress as events arrive, returns the final 'done' payload.
|
|
260
|
+
Falls back to non-streaming if SSE is not supported.
|
|
261
|
+
"""
|
|
262
|
+
api_key = _get_api_key()
|
|
263
|
+
url = f"{_BASE}/{endpoint}"
|
|
264
|
+
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
265
|
+
|
|
266
|
+
req = urllib.request.Request(
|
|
267
|
+
url,
|
|
268
|
+
data=data,
|
|
269
|
+
headers={
|
|
270
|
+
"Content-Type": "application/json",
|
|
271
|
+
"X-API-Key": api_key,
|
|
272
|
+
"User-Agent": "lokuma-skill/2.0",
|
|
273
|
+
"Accept": "text/event-stream",
|
|
274
|
+
},
|
|
275
|
+
method="POST",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
280
|
+
content_type = resp.headers.get("Content-Type", "")
|
|
281
|
+
if "text/event-stream" not in content_type:
|
|
282
|
+
# Server returned regular JSON — parse and return
|
|
283
|
+
body = resp.read().decode("utf-8")
|
|
284
|
+
return json.loads(body)
|
|
285
|
+
|
|
286
|
+
# Parse SSE stream
|
|
287
|
+
result = {}
|
|
288
|
+
buffer = ""
|
|
289
|
+
total = 0
|
|
290
|
+
done_count = 0
|
|
291
|
+
|
|
292
|
+
print("", file=sys.stderr) # blank line before progress
|
|
293
|
+
|
|
294
|
+
for raw_line in resp:
|
|
295
|
+
line = raw_line.decode("utf-8", errors="replace").rstrip("\n\r")
|
|
296
|
+
|
|
297
|
+
if not line.startswith("data:"):
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
json_str = line[len("data:"):].strip()
|
|
301
|
+
if not json_str:
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
event = json.loads(json_str)
|
|
306
|
+
except json.JSONDecodeError:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
kind = event.get("event")
|
|
310
|
+
|
|
311
|
+
if kind == "start":
|
|
312
|
+
total = event.get("total", 10)
|
|
313
|
+
name = event.get("project_name", "")
|
|
314
|
+
print(f" ⟳ Analyzing: {name}", file=sys.stderr)
|
|
315
|
+
|
|
316
|
+
elif kind == "domain_done":
|
|
317
|
+
done_count += 1
|
|
318
|
+
domain = event.get("domain", "")
|
|
319
|
+
match = event.get("match", "")
|
|
320
|
+
label = _DOMAIN_LABELS.get(domain, domain.title())
|
|
321
|
+
bar = "█" * done_count + "░" * (total - done_count)
|
|
322
|
+
print(f" [{bar}] {label}: {match}", file=sys.stderr)
|
|
323
|
+
|
|
324
|
+
elif kind == "synthesizing":
|
|
325
|
+
print(f"\n ✦ Synthesizing design recommendation...", file=sys.stderr)
|
|
326
|
+
|
|
327
|
+
elif kind == "done":
|
|
328
|
+
result = event
|
|
329
|
+
warning = result.get("warning")
|
|
330
|
+
if warning:
|
|
331
|
+
print(f"\n⚠️ {warning}", file=sys.stderr)
|
|
332
|
+
print("", file=sys.stderr) # blank line after progress
|
|
333
|
+
|
|
334
|
+
elif kind == "error":
|
|
335
|
+
print(f"\n ✗ Error: {event.get('message', 'Unknown error')}", file=sys.stderr)
|
|
336
|
+
|
|
337
|
+
return result
|
|
338
|
+
|
|
339
|
+
except urllib.error.HTTPError as e:
|
|
340
|
+
body = e.read().decode("utf-8", errors="replace")
|
|
341
|
+
try:
|
|
342
|
+
err = json.loads(body)
|
|
343
|
+
msg = err.get("error", body)
|
|
344
|
+
except Exception:
|
|
345
|
+
msg = body
|
|
346
|
+
if e.code in (401, 403):
|
|
347
|
+
print(f"Error: Invalid or expired API key (HTTP {e.code})", file=sys.stderr)
|
|
348
|
+
elif e.code == 402:
|
|
349
|
+
print(f"Error: {msg}", file=sys.stderr)
|
|
350
|
+
else:
|
|
351
|
+
print(f"Error: API returned HTTP {e.code}: {msg}", file=sys.stderr)
|
|
352
|
+
sys.exit(1)
|
|
353
|
+
except urllib.error.URLError as e:
|
|
354
|
+
print(f"Error: Could not reach Lokuma API: {e.reason}", file=sys.stderr)
|
|
355
|
+
sys.exit(1)
|
|
356
|
+
except json.JSONDecodeError as e:
|
|
357
|
+
print(f"Error: Invalid JSON response: {e}", file=sys.stderr)
|
|
358
|
+
sys.exit(1)
|
|
359
|
+
|
|
360
|
+
|
|
238
361
|
# ─────────────────────────────────────────────
|
|
239
362
|
# CLI
|
|
240
363
|
# ─────────────────────────────────────────────
|
|
@@ -257,7 +380,7 @@ def main():
|
|
|
257
380
|
if args.project_name:
|
|
258
381
|
payload["project_name"] = args.project_name
|
|
259
382
|
|
|
260
|
-
result =
|
|
383
|
+
result = _post_stream("design/stream", payload)
|
|
261
384
|
print(_format_output(result, args.format))
|
|
262
385
|
|
|
263
386
|
|