lokuma-cli 1.4.9 → 1.4.11

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 (2) hide show
  1. package/assets/design.py +125 -2
  2. package/package.json +1 -1
package/assets/design.py CHANGED
@@ -95,7 +95,7 @@ def _post(endpoint: str, payload: dict) -> dict:
95
95
  )
96
96
 
97
97
  try:
98
- with urllib.request.urlopen(req, timeout=60) as resp:
98
+ with urllib.request.urlopen(req, timeout=120) as resp:
99
99
  body = resp.read().decode("utf-8")
100
100
  return json.loads(body)
101
101
  except urllib.error.HTTPError as e:
@@ -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 = _post("design", payload)
383
+ result = _post_stream("design/stream", payload)
261
384
  print(_format_output(result, args.format))
262
385
 
263
386
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lokuma-cli",
3
- "version": "1.4.9",
3
+ "version": "1.4.11",
4
4
  "description": "CLI to install Lokuma design intelligence skill for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {