lokuma-cli 1.3.4 → 1.3.6

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/README.md CHANGED
@@ -68,7 +68,7 @@ Options:
68
68
  `lokuma-cli` installs two files into your project:
69
69
 
70
70
  1. **SKILL.md** — tells your AI assistant how to use Lokuma
71
- 2. **search.py** — a lightweight cloud client that calls the Lokuma API
71
+ 2. **design.py** — a lightweight cloud client that calls the Lokuma API
72
72
 
73
73
  No local data files. No Python dependencies. Just set your API key and go.
74
74
 
package/dist/index.js CHANGED
@@ -54,22 +54,22 @@ var AI_DISPLAY_NAMES = {
54
54
  all: "All assistants"
55
55
  };
56
56
  var AI_SKILL_PATHS = {
57
- claude: { root: ".claude", skillMd: ".claude/skills/lokuma/SKILL.md", scriptPy: ".claude/skills/lokuma/scripts/search.py" },
58
- cursor: { root: ".cursor", skillMd: ".cursor/rules/lokuma.mdc", scriptPy: ".cursor/rules/lokuma/scripts/search.py" },
59
- windsurf: { root: ".windsurf", skillMd: ".windsurf/rules/lokuma.md", scriptPy: ".windsurf/rules/lokuma/scripts/search.py" },
60
- copilot: { root: ".github", skillMd: ".github/instructions/lokuma.instructions.md", scriptPy: ".github/instructions/lokuma/scripts/search.py" },
61
- kiro: { root: ".kiro", skillMd: ".kiro/steering/lokuma.md", scriptPy: ".kiro/steering/lokuma/scripts/search.py" },
62
- roocode: { root: ".roo", skillMd: ".roo/rules/lokuma.md", scriptPy: ".roo/rules/lokuma/scripts/search.py" },
63
- codex: { root: ".codex", skillMd: ".codex/instructions.md", scriptPy: ".codex/lokuma/scripts/search.py" },
64
- qoder: { root: ".qoder", skillMd: ".qoder/rules/lokuma.md", scriptPy: ".qoder/rules/lokuma/scripts/search.py" },
65
- gemini: { root: ".gemini", skillMd: ".gemini/GEMINI.md", scriptPy: ".gemini/lokuma/scripts/search.py" },
66
- trae: { root: ".trae", skillMd: ".trae/rules/lokuma.md", scriptPy: ".trae/rules/lokuma/scripts/search.py" },
67
- opencode: { root: ".opencode", skillMd: ".opencode/OPENCODE.md", scriptPy: ".opencode/lokuma/scripts/search.py" },
68
- continue: { root: ".continue", skillMd: ".continue/lokuma/SKILL.md", scriptPy: ".continue/lokuma/scripts/search.py" },
69
- codebuddy: { root: ".codebuddy", skillMd: ".codebuddy/lokuma/SKILL.md", scriptPy: ".codebuddy/lokuma/scripts/search.py" },
70
- droid: { root: ".factory", skillMd: ".factory/skills/lokuma/SKILL.md", scriptPy: ".factory/skills/lokuma/scripts/search.py" },
57
+ claude: { root: ".claude", skillMd: ".claude/skills/lokuma/SKILL.md", scriptPy: ".claude/skills/lokuma/scripts/design.py" },
58
+ cursor: { root: ".cursor", skillMd: ".cursor/rules/lokuma.mdc", scriptPy: ".cursor/rules/lokuma/scripts/design.py" },
59
+ windsurf: { root: ".windsurf", skillMd: ".windsurf/rules/lokuma.md", scriptPy: ".windsurf/rules/lokuma/scripts/design.py" },
60
+ copilot: { root: ".github", skillMd: ".github/instructions/lokuma.instructions.md", scriptPy: ".github/instructions/lokuma/scripts/design.py" },
61
+ kiro: { root: ".kiro", skillMd: ".kiro/steering/lokuma.md", scriptPy: ".kiro/steering/lokuma/scripts/design.py" },
62
+ roocode: { root: ".roo", skillMd: ".roo/rules/lokuma.md", scriptPy: ".roo/rules/lokuma/scripts/design.py" },
63
+ codex: { root: ".codex", skillMd: ".codex/instructions.md", scriptPy: ".codex/lokuma/scripts/design.py" },
64
+ qoder: { root: ".qoder", skillMd: ".qoder/rules/lokuma.md", scriptPy: ".qoder/rules/lokuma/scripts/design.py" },
65
+ gemini: { root: ".gemini", skillMd: ".gemini/GEMINI.md", scriptPy: ".gemini/lokuma/scripts/design.py" },
66
+ trae: { root: ".trae", skillMd: ".trae/rules/lokuma.md", scriptPy: ".trae/rules/lokuma/scripts/design.py" },
67
+ opencode: { root: ".opencode", skillMd: ".opencode/OPENCODE.md", scriptPy: ".opencode/lokuma/scripts/design.py" },
68
+ continue: { root: ".continue", skillMd: ".continue/lokuma/SKILL.md", scriptPy: ".continue/lokuma/scripts/design.py" },
69
+ codebuddy: { root: ".codebuddy", skillMd: ".codebuddy/lokuma/SKILL.md", scriptPy: ".codebuddy/lokuma/scripts/design.py" },
70
+ droid: { root: ".factory", skillMd: ".factory/skills/lokuma/SKILL.md", scriptPy: ".factory/skills/lokuma/scripts/design.py" },
71
71
  // OpenClaw installs to a fixed absolute path (not relative to cwd)
72
- openclaw: { root: "~/.openclaw/workspace/skills/lokuma", skillMd: "~/.openclaw/workspace/skills/lokuma/SKILL.md", scriptPy: "~/.openclaw/workspace/skills/lokuma/scripts/search.py" }
72
+ openclaw: { root: "~/.openclaw/workspace/skills/lokuma", skillMd: "~/.openclaw/workspace/skills/lokuma/SKILL.md", scriptPy: "~/.openclaw/workspace/skills/lokuma/scripts/design.py" }
73
73
  };
74
74
 
75
75
  // src/commands/init.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lokuma-cli",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "CLI to install Lokuma design intelligence skill for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/assets/search.py DELETED
@@ -1,229 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- Lokuma Search - Cloud Client
5
- Calls the Lokuma cloud API to generate design intelligence.
6
-
7
- Usage:
8
- python search.py "<natural language design request>" [--domain <domain>] [--max-results 3]
9
- python search.py "<natural language design request>" --stack <stack>
10
- python search.py "<natural language product/design brief>" --design-system [-p "Project Name"] [-f ascii|markdown|json]
11
-
12
- Examples:
13
- python search.py "A meditation app for stressed professionals. Calm, premium, organic." --design-system -p "Still"
14
- python search.py "Need a calm but premium color direction for a wellness app" --domain color
15
- python search.py "Best landing page structure for an AI productivity SaaS that needs signups" --domain landing
16
- python search.py "Navigation performance and gestures for a React Native app" --stack react-native
17
-
18
- Configuration:
19
- Set LOKUMA_API_KEY environment variable (required):
20
- export LOKUMA_API_KEY=lokuma_your_key_here
21
-
22
- Optional overrides:
23
- LOKUMA_API_URL=https://custom.endpoint.com (default: official API)
24
-
25
- Domains: style, color, chart, landing, product, reasoning, ux, typography,
26
- google-fonts, icons
27
- Stacks: react-native
28
- """
29
-
30
- import argparse
31
- import json
32
- import os
33
- import sys
34
- import io
35
- import urllib.request
36
- import urllib.error
37
- from typing import Optional
38
-
39
- # Force UTF-8 output on Windows
40
- if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
41
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
42
- if sys.stderr.encoding and sys.stderr.encoding.lower() != "utf-8":
43
- sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
44
-
45
-
46
- # ─────────────────────────────────────────────
47
- # Config
48
- # ─────────────────────────────────────────────
49
-
50
- API_BASE = os.environ.get("LOKUMA_API_URL", "https://api.lokuma.ai").rstrip("/")
51
- API_VERSION = "v1"
52
- _BASE = f"{API_BASE}/{API_VERSION}"
53
-
54
-
55
- def _get_api_key() -> str:
56
- key = os.environ.get("LOKUMA_API_KEY", "").strip()
57
- if not key:
58
- print(
59
- "Error: LOKUMA_API_KEY is not set.\n"
60
- "Set it with: export LOKUMA_API_KEY=lokuma_your_key_here",
61
- file=sys.stderr,
62
- )
63
- sys.exit(1)
64
- return key
65
-
66
-
67
- # ─────────────────────────────────────────────
68
- # HTTP client (stdlib only, zero deps)
69
- # ─────────────────────────────────────────────
70
-
71
- def _post(endpoint: str, payload: dict) -> dict:
72
- """POST JSON payload to API endpoint, return parsed response."""
73
- api_key = _get_api_key()
74
- url = f"{_BASE}/{endpoint}"
75
- data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
76
-
77
- req = urllib.request.Request(
78
- url,
79
- data=data,
80
- headers={
81
- "Content-Type": "application/json",
82
- "X-API-Key": api_key,
83
- "User-Agent": "lokuma-skill/1.0",
84
- },
85
- method="POST",
86
- )
87
-
88
- try:
89
- with urllib.request.urlopen(req, timeout=30) as resp:
90
- body = resp.read().decode("utf-8")
91
- return json.loads(body)
92
- except urllib.error.HTTPError as e:
93
- body = e.read().decode("utf-8", errors="replace")
94
- try:
95
- err = json.loads(body)
96
- msg = err.get("error", body)
97
- except Exception:
98
- msg = body
99
- if e.code == 401 or e.code == 403:
100
- print(f"Error: Invalid or expired API key (HTTP {e.code})", file=sys.stderr)
101
- else:
102
- print(f"Error: API returned HTTP {e.code}: {msg}", file=sys.stderr)
103
- sys.exit(1)
104
- except urllib.error.URLError as e:
105
- print(f"Error: Could not reach API at {url}: {e.reason}", file=sys.stderr)
106
- sys.exit(1)
107
- except json.JSONDecodeError as e:
108
- print(f"Error: Invalid JSON response from API: {e}", file=sys.stderr)
109
- sys.exit(1)
110
-
111
-
112
- # ─────────────────────────────────────────────
113
- # API wrappers
114
- # ─────────────────────────────────────────────
115
-
116
- def search(query: str, domain: Optional[str] = None, max_results: int = 3) -> dict:
117
- payload = {"query": query, "max_results": max_results}
118
- if domain:
119
- payload["domain"] = domain
120
- return _post("search", payload)
121
-
122
-
123
- def search_stack(query: str, stack: str, max_results: int = 3) -> dict:
124
- return _post("search/stack", {"query": query, "stack": stack, "max_results": max_results})
125
-
126
-
127
- def design_system(query: str, project_name: Optional[str] = None, fmt: str = "ascii") -> dict:
128
- payload = {"query": query, "format": fmt}
129
- if project_name:
130
- payload["project_name"] = project_name
131
- return _post("design-system", payload)
132
-
133
-
134
- # ─────────────────────────────────────────────
135
- # Output formatting (mirrors original search.py)
136
- # ─────────────────────────────────────────────
137
-
138
- def format_search_output(result: dict) -> str:
139
- if "error" in result:
140
- return f"Error: {result['error']}"
141
-
142
- output = []
143
- if result.get("stack"):
144
- output.append("## Lokuma Stack Guidelines")
145
- output.append(f"**Stack:** {result['stack']} | **Query:** {result['query']}")
146
- else:
147
- output.append("## Lokuma Search Results")
148
- output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}")
149
-
150
- source = result.get("file") or result.get("source") or "cloud API"
151
- output.append(f"**Source:** {source} | **Found:** {result.get('count', len(result.get('results', [])))} results\n")
152
-
153
- for i, row in enumerate(result.get("results", []), 1):
154
- output.append(f"### Result {i}")
155
- for key, value in row.items():
156
- value_str = str(value)
157
- if len(value_str) > 300:
158
- value_str = value_str[:300] + "..."
159
- output.append(f"- **{key}:** {value_str}")
160
- output.append("")
161
-
162
- return "\n".join(output)
163
-
164
-
165
- # ─────────────────────────────────────────────
166
- # CLI
167
- # ─────────────────────────────────────────────
168
-
169
- VALID_DOMAINS = [
170
- "style", "color", "chart", "landing", "product", "reasoning", "ux",
171
- "typography", "google-fonts", "icons",
172
- ]
173
- VALID_STACKS = ["react-native"]
174
-
175
-
176
- def main():
177
- parser = argparse.ArgumentParser(
178
- description="Lokuma Cloud Search",
179
- formatter_class=argparse.RawDescriptionHelpFormatter,
180
- epilog=__doc__,
181
- )
182
- parser.add_argument("query", help="Search query")
183
- parser.add_argument("--domain", "-d", choices=VALID_DOMAINS, help="Search domain")
184
- parser.add_argument("--stack", "-s", choices=VALID_STACKS, help="Stack-specific search")
185
- parser.add_argument("--max-results", "-n", type=int, default=3, help="Max results (default: 3)")
186
- parser.add_argument("--json", action="store_true", help="Output raw JSON")
187
- # Design system flags
188
- parser.add_argument("--design-system", "-ds", action="store_true",
189
- help="Generate complete design system")
190
- parser.add_argument("--project-name", "-p", type=str, default=None,
191
- help="Project name for design system header")
192
- parser.add_argument("--format", "-f", choices=["ascii", "markdown", "json"],
193
- default="ascii", help="Output format for design system")
194
-
195
- args = parser.parse_args()
196
-
197
- # ── Design system ──
198
- if args.design_system:
199
- fmt = args.format
200
- result = design_system(args.query, args.project_name, fmt)
201
- if args.json:
202
- print(json.dumps(result, indent=2, ensure_ascii=False))
203
- elif fmt == "json":
204
- # Pretty-print the nested design_system dict
205
- ds = result.get("design_system", result)
206
- print(json.dumps(ds, indent=2, ensure_ascii=False))
207
- else:
208
- print(result.get("output", ""))
209
- return
210
-
211
- # ── Stack search ──
212
- if args.stack:
213
- result = search_stack(args.query, args.stack, args.max_results)
214
- if args.json:
215
- print(json.dumps(result, indent=2, ensure_ascii=False))
216
- else:
217
- print(format_search_output(result))
218
- return
219
-
220
- # ── Domain search ──
221
- result = search(args.query, args.domain, args.max_results)
222
- if args.json:
223
- print(json.dumps(result, indent=2, ensure_ascii=False))
224
- else:
225
- print(format_search_output(result))
226
-
227
-
228
- if __name__ == "__main__":
229
- main()