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 +1 -1
- package/dist/index.js +15 -15
- package/package.json +1 -1
- package/assets/search.py +0 -229
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. **
|
|
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/
|
|
58
|
-
cursor: { root: ".cursor", skillMd: ".cursor/rules/lokuma.mdc", scriptPy: ".cursor/rules/lokuma/scripts/
|
|
59
|
-
windsurf: { root: ".windsurf", skillMd: ".windsurf/rules/lokuma.md", scriptPy: ".windsurf/rules/lokuma/scripts/
|
|
60
|
-
copilot: { root: ".github", skillMd: ".github/instructions/lokuma.instructions.md", scriptPy: ".github/instructions/lokuma/scripts/
|
|
61
|
-
kiro: { root: ".kiro", skillMd: ".kiro/steering/lokuma.md", scriptPy: ".kiro/steering/lokuma/scripts/
|
|
62
|
-
roocode: { root: ".roo", skillMd: ".roo/rules/lokuma.md", scriptPy: ".roo/rules/lokuma/scripts/
|
|
63
|
-
codex: { root: ".codex", skillMd: ".codex/instructions.md", scriptPy: ".codex/lokuma/scripts/
|
|
64
|
-
qoder: { root: ".qoder", skillMd: ".qoder/rules/lokuma.md", scriptPy: ".qoder/rules/lokuma/scripts/
|
|
65
|
-
gemini: { root: ".gemini", skillMd: ".gemini/GEMINI.md", scriptPy: ".gemini/lokuma/scripts/
|
|
66
|
-
trae: { root: ".trae", skillMd: ".trae/rules/lokuma.md", scriptPy: ".trae/rules/lokuma/scripts/
|
|
67
|
-
opencode: { root: ".opencode", skillMd: ".opencode/OPENCODE.md", scriptPy: ".opencode/lokuma/scripts/
|
|
68
|
-
continue: { root: ".continue", skillMd: ".continue/lokuma/SKILL.md", scriptPy: ".continue/lokuma/scripts/
|
|
69
|
-
codebuddy: { root: ".codebuddy", skillMd: ".codebuddy/lokuma/SKILL.md", scriptPy: ".codebuddy/lokuma/scripts/
|
|
70
|
-
droid: { root: ".factory", skillMd: ".factory/skills/lokuma/SKILL.md", scriptPy: ".factory/skills/lokuma/scripts/
|
|
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/
|
|
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
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()
|