lokuma-cli 1.0.0

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 ADDED
@@ -0,0 +1,64 @@
1
+ # lokuma-cli
2
+
3
+ Install [Lokuma](https://lokuma.io) design intelligence skill for AI coding assistants.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx lokuma-cli init --ai claude
9
+ ```
10
+
11
+ ## Supported AI Assistants
12
+
13
+ | Flag | Assistant |
14
+ |------|-----------|
15
+ | `--ai claude` | Claude Code |
16
+ | `--ai cursor` | Cursor |
17
+ | `--ai windsurf` | Windsurf |
18
+ | `--ai copilot` | GitHub Copilot |
19
+ | `--ai kiro` | Kiro |
20
+ | `--ai roocode` | Roo Code |
21
+ | `--ai codex` | Codex CLI |
22
+ | `--ai qoder` | Qoder |
23
+ | `--ai gemini` | Gemini CLI |
24
+ | `--ai trae` | Trae |
25
+ | `--ai opencode` | OpenCode |
26
+ | `--ai continue` | Continue |
27
+ | `--ai codebuddy` | CodeBuddy |
28
+ | `--ai droid` | Droid (Factory) |
29
+ | `--ai all` | All assistants |
30
+
31
+ ## Configuration
32
+
33
+ After installing, set your API key:
34
+
35
+ ```bash
36
+ export LOKUMA_API_KEY=lokuma_your_key_here
37
+ ```
38
+
39
+ Get your API key at [lokuma.io](https://lokuma.io).
40
+
41
+ ## Options
42
+
43
+ ```
44
+ lokuma init [options]
45
+
46
+ Options:
47
+ -a, --ai <type> AI assistant type
48
+ -f, --force Overwrite existing files
49
+ -h, --help Show help
50
+ -V, --version Show version
51
+ ```
52
+
53
+ ## How It Works
54
+
55
+ `lokuma-cli` installs two files into your project:
56
+
57
+ 1. **SKILL.md** — tells your AI assistant how to use Lokuma
58
+ 2. **search.py** — a lightweight cloud client that calls the Lokuma API
59
+
60
+ No local data files. No Python dependencies. Just set your API key and go.
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,221 @@
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 "<query>" [--domain <domain>] [--max-results 3]
9
+ python search.py "<query>" --stack <stack>
10
+ python search.py "<query>" --design-system [-p "Project Name"] [-f ascii|markdown|json]
11
+
12
+ Configuration:
13
+ Set LOKUMA_API_KEY environment variable (required):
14
+ export LOKUMA_API_KEY=lokuma_your_key_here
15
+
16
+ Optional overrides:
17
+ LOKUMA_API_URL=https://custom.endpoint.com (default: official API)
18
+
19
+ Domains: style, color, chart, landing, product, ux, typography, google-fonts,
20
+ icons, react, web
21
+ Stacks: react-native
22
+ """
23
+
24
+ import argparse
25
+ import json
26
+ import os
27
+ import sys
28
+ import io
29
+ import urllib.request
30
+ import urllib.error
31
+ from typing import Optional
32
+
33
+ # Force UTF-8 output on Windows
34
+ if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
35
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
36
+ if sys.stderr.encoding and sys.stderr.encoding.lower() != "utf-8":
37
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
38
+
39
+
40
+ # ─────────────────────────────────────────────
41
+ # Config
42
+ # ─────────────────────────────────────────────
43
+
44
+ API_BASE = os.environ.get("LOKUMA_API_URL", "https://api.lokuma.io").rstrip("/")
45
+ API_VERSION = "v1"
46
+ _BASE = f"{API_BASE}/{API_VERSION}"
47
+
48
+
49
+ def _get_api_key() -> str:
50
+ key = os.environ.get("LOKUMA_API_KEY", "").strip()
51
+ if not key:
52
+ print(
53
+ "Error: LOKUMA_API_KEY is not set.\n"
54
+ "Set it with: export LOKUMA_API_KEY=lokuma_your_key_here",
55
+ file=sys.stderr,
56
+ )
57
+ sys.exit(1)
58
+ return key
59
+
60
+
61
+ # ─────────────────────────────────────────────
62
+ # HTTP client (stdlib only, zero deps)
63
+ # ─────────────────────────────────────────────
64
+
65
+ def _post(endpoint: str, payload: dict) -> dict:
66
+ """POST JSON payload to API endpoint, return parsed response."""
67
+ api_key = _get_api_key()
68
+ url = f"{_BASE}/{endpoint}"
69
+ data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
70
+
71
+ req = urllib.request.Request(
72
+ url,
73
+ data=data,
74
+ headers={
75
+ "Content-Type": "application/json",
76
+ "X-API-Key": api_key,
77
+ "User-Agent": "lokuma-skill/1.0",
78
+ },
79
+ method="POST",
80
+ )
81
+
82
+ try:
83
+ with urllib.request.urlopen(req, timeout=30) as resp:
84
+ body = resp.read().decode("utf-8")
85
+ return json.loads(body)
86
+ except urllib.error.HTTPError as e:
87
+ body = e.read().decode("utf-8", errors="replace")
88
+ try:
89
+ err = json.loads(body)
90
+ msg = err.get("error", body)
91
+ except Exception:
92
+ msg = body
93
+ if e.code == 401 or e.code == 403:
94
+ print(f"Error: Invalid or expired API key (HTTP {e.code})", file=sys.stderr)
95
+ else:
96
+ print(f"Error: API returned HTTP {e.code}: {msg}", file=sys.stderr)
97
+ sys.exit(1)
98
+ except urllib.error.URLError as e:
99
+ print(f"Error: Could not reach API at {url}: {e.reason}", file=sys.stderr)
100
+ sys.exit(1)
101
+ except json.JSONDecodeError as e:
102
+ print(f"Error: Invalid JSON response from API: {e}", file=sys.stderr)
103
+ sys.exit(1)
104
+
105
+
106
+ # ─────────────────────────────────────────────
107
+ # API wrappers
108
+ # ─────────────────────────────────────────────
109
+
110
+ def search(query: str, domain: Optional[str] = None, max_results: int = 3) -> dict:
111
+ payload = {"query": query, "max_results": max_results}
112
+ if domain:
113
+ payload["domain"] = domain
114
+ return _post("search", payload)
115
+
116
+
117
+ def search_stack(query: str, stack: str, max_results: int = 3) -> dict:
118
+ return _post("search/stack", {"query": query, "stack": stack, "max_results": max_results})
119
+
120
+
121
+ def design_system(query: str, project_name: Optional[str] = None, fmt: str = "ascii") -> dict:
122
+ payload = {"query": query, "format": fmt}
123
+ if project_name:
124
+ payload["project_name"] = project_name
125
+ return _post("design-system", payload)
126
+
127
+
128
+ # ─────────────────────────────────────────────
129
+ # Output formatting (mirrors original search.py)
130
+ # ─────────────────────────────────────────────
131
+
132
+ def format_search_output(result: dict) -> str:
133
+ if "error" in result:
134
+ return f"Error: {result['error']}"
135
+
136
+ output = []
137
+ if result.get("stack"):
138
+ output.append("## Lokuma Stack Guidelines")
139
+ output.append(f"**Stack:** {result['stack']} | **Query:** {result['query']}")
140
+ else:
141
+ output.append("## Lokuma Search Results")
142
+ output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}")
143
+ output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n")
144
+
145
+ for i, row in enumerate(result.get("results", []), 1):
146
+ output.append(f"### Result {i}")
147
+ for key, value in row.items():
148
+ value_str = str(value)
149
+ if len(value_str) > 300:
150
+ value_str = value_str[:300] + "..."
151
+ output.append(f"- **{key}:** {value_str}")
152
+ output.append("")
153
+
154
+ return "\n".join(output)
155
+
156
+
157
+ # ─────────────────────────────────────────────
158
+ # CLI
159
+ # ─────────────────────────────────────────────
160
+
161
+ VALID_DOMAINS = [
162
+ "style", "color", "chart", "landing", "product", "ux",
163
+ "typography", "google-fonts", "icons", "react", "web",
164
+ ]
165
+ VALID_STACKS = ["react-native"]
166
+
167
+
168
+ def main():
169
+ parser = argparse.ArgumentParser(
170
+ description="Lokuma Cloud Search",
171
+ formatter_class=argparse.RawDescriptionHelpFormatter,
172
+ epilog=__doc__,
173
+ )
174
+ parser.add_argument("query", help="Search query")
175
+ parser.add_argument("--domain", "-d", choices=VALID_DOMAINS, help="Search domain")
176
+ parser.add_argument("--stack", "-s", choices=VALID_STACKS, help="Stack-specific search")
177
+ parser.add_argument("--max-results", "-n", type=int, default=3, help="Max results (default: 3)")
178
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
179
+ # Design system flags
180
+ parser.add_argument("--design-system", "-ds", action="store_true",
181
+ help="Generate complete design system")
182
+ parser.add_argument("--project-name", "-p", type=str, default=None,
183
+ help="Project name for design system header")
184
+ parser.add_argument("--format", "-f", choices=["ascii", "markdown", "json"],
185
+ default="ascii", help="Output format for design system")
186
+
187
+ args = parser.parse_args()
188
+
189
+ # ── Design system ──
190
+ if args.design_system:
191
+ fmt = args.format
192
+ result = design_system(args.query, args.project_name, fmt)
193
+ if args.json:
194
+ print(json.dumps(result, indent=2, ensure_ascii=False))
195
+ elif fmt == "json":
196
+ # Pretty-print the nested design_system dict
197
+ ds = result.get("design_system", result)
198
+ print(json.dumps(ds, indent=2, ensure_ascii=False))
199
+ else:
200
+ print(result.get("output", ""))
201
+ return
202
+
203
+ # ── Stack search ──
204
+ if args.stack:
205
+ result = search_stack(args.query, args.stack, args.max_results)
206
+ if args.json:
207
+ print(json.dumps(result, indent=2, ensure_ascii=False))
208
+ else:
209
+ print(format_search_output(result))
210
+ return
211
+
212
+ # ── Domain search ──
213
+ result = search(args.query, args.domain, 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
+
219
+
220
+ if __name__ == "__main__":
221
+ main()
package/dist/index.js ADDED
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import { readFileSync as readFileSync2 } from "fs";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
7
+ import { dirname as dirname2, join as join2 } from "path";
8
+
9
+ // src/commands/init.ts
10
+ import { mkdir, writeFile, access } from "node:fs/promises";
11
+ import { dirname, join } from "node:path";
12
+ import { readFileSync } from "node:fs";
13
+ import { fileURLToPath } from "node:url";
14
+ import chalk from "chalk";
15
+ import ora from "ora";
16
+ import prompts from "prompts";
17
+
18
+ // src/types/index.ts
19
+ var AI_TYPES = [
20
+ "claude",
21
+ "cursor",
22
+ "windsurf",
23
+ "copilot",
24
+ "kiro",
25
+ "roocode",
26
+ "codex",
27
+ "qoder",
28
+ "gemini",
29
+ "trae",
30
+ "opencode",
31
+ "continue",
32
+ "codebuddy",
33
+ "droid",
34
+ "all"
35
+ ];
36
+ var AI_DISPLAY_NAMES = {
37
+ claude: "Claude Code",
38
+ cursor: "Cursor",
39
+ windsurf: "Windsurf",
40
+ copilot: "GitHub Copilot",
41
+ kiro: "Kiro",
42
+ roocode: "Roo Code",
43
+ codex: "Codex CLI",
44
+ qoder: "Qoder",
45
+ gemini: "Gemini CLI",
46
+ trae: "Trae",
47
+ opencode: "OpenCode",
48
+ continue: "Continue",
49
+ codebuddy: "CodeBuddy",
50
+ droid: "Droid (Factory)",
51
+ all: "All assistants"
52
+ };
53
+ var AI_SKILL_PATHS = {
54
+ claude: { root: ".claude", skillMd: ".claude/skills/lokuma/SKILL.md", scriptPy: ".claude/skills/lokuma/scripts/search.py" },
55
+ cursor: { root: ".cursor", skillMd: ".cursor/rules/lokuma.mdc", scriptPy: ".cursor/rules/lokuma/scripts/search.py" },
56
+ windsurf: { root: ".windsurf", skillMd: ".windsurf/rules/lokuma.md", scriptPy: ".windsurf/rules/lokuma/scripts/search.py" },
57
+ copilot: { root: ".github", skillMd: ".github/instructions/lokuma.instructions.md", scriptPy: ".github/instructions/lokuma/scripts/search.py" },
58
+ kiro: { root: ".kiro", skillMd: ".kiro/steering/lokuma.md", scriptPy: ".kiro/steering/lokuma/scripts/search.py" },
59
+ roocode: { root: ".roo", skillMd: ".roo/rules/lokuma.md", scriptPy: ".roo/rules/lokuma/scripts/search.py" },
60
+ codex: { root: ".codex", skillMd: ".codex/instructions.md", scriptPy: ".codex/lokuma/scripts/search.py" },
61
+ qoder: { root: ".qoder", skillMd: ".qoder/rules/lokuma.md", scriptPy: ".qoder/rules/lokuma/scripts/search.py" },
62
+ gemini: { root: ".gemini", skillMd: ".gemini/GEMINI.md", scriptPy: ".gemini/lokuma/scripts/search.py" },
63
+ trae: { root: ".trae", skillMd: ".trae/rules/lokuma.md", scriptPy: ".trae/rules/lokuma/scripts/search.py" },
64
+ opencode: { root: ".opencode", skillMd: ".opencode/OPENCODE.md", scriptPy: ".opencode/lokuma/scripts/search.py" },
65
+ continue: { root: ".continue", skillMd: ".continue/lokuma/SKILL.md", scriptPy: ".continue/lokuma/scripts/search.py" },
66
+ codebuddy: { root: ".codebuddy", skillMd: ".codebuddy/lokuma/SKILL.md", scriptPy: ".codebuddy/lokuma/scripts/search.py" },
67
+ droid: { root: ".factory", skillMd: ".factory/skills/lokuma/SKILL.md", scriptPy: ".factory/skills/lokuma/scripts/search.py" }
68
+ };
69
+
70
+ // src/commands/init.ts
71
+ import { createRequire } from "node:module";
72
+ var _require = createRequire(import.meta.url);
73
+ var _pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
74
+ var SEARCH_PY = readFileSync(join(_pkgDir, "assets", "search.py"), "utf-8");
75
+ function buildSkillMd(scriptPath) {
76
+ return `---
77
+ name: lokuma
78
+ description: Lokuma design intelligence. 67 UI styles, 161 color palettes, 57 font pairings, 25 chart types, 99 UX guidelines. Triggers: build, design, create, implement UI/UX. Projects: landing page, dashboard, SaaS, e-commerce, portfolio, mobile app. Styles: glassmorphism, minimalism, brutalism, neumorphism, bento grid, dark mode.
79
+ ---
80
+
81
+ # Lokuma - Design Intelligence
82
+
83
+ ## Setup
84
+
85
+ Set your API key (required):
86
+ \`\`\`bash
87
+ export LOKUMA_API_KEY=lokuma_your_key_here
88
+ \`\`\`
89
+
90
+ Get your API key at https://lokuma.io
91
+
92
+ ## Usage
93
+
94
+ ### Generate Design System (start here)
95
+ \`\`\`bash
96
+ python3 ${scriptPath} "<product description>" --design-system [-p "Project Name"]
97
+ \`\`\`
98
+
99
+ Example:
100
+ \`\`\`bash
101
+ python3 ${scriptPath} "beauty spa wellness" --design-system -p "Serenity Spa"
102
+ \`\`\`
103
+
104
+ ### Domain Search
105
+ \`\`\`bash
106
+ python3 ${scriptPath} "<keyword>" --domain <domain>
107
+ \`\`\`
108
+
109
+ Domains: \`style\` \`color\` \`typography\` \`chart\` \`landing\` \`product\` \`ux\` \`icons\`
110
+
111
+ ### Stack Guidelines
112
+ \`\`\`bash
113
+ python3 ${scriptPath} "<keyword>" --stack react-native
114
+ \`\`\`
115
+
116
+ ## Workflow
117
+
118
+ 1. **New project** \u2192 \`--design-system\` first to get full design system
119
+ 2. **Component** \u2192 \`--domain style\` or \`--domain ux\`
120
+ 3. **Colors/fonts** \u2192 \`--domain color\` or \`--domain typography\`
121
+ 4. **Review** \u2192 \`--domain ux\` for best practices
122
+ `;
123
+ }
124
+ async function exists(path) {
125
+ try {
126
+ await access(path);
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+ async function initCommand(options) {
133
+ console.log(chalk.bold("\n Lokuma Design Intelligence Installer\n"));
134
+ let aiType = options.ai;
135
+ if (!aiType) {
136
+ const res = await prompts({
137
+ type: "select",
138
+ name: "aiType",
139
+ message: "Select your AI coding assistant:",
140
+ choices: AI_TYPES.map((t) => ({ title: AI_DISPLAY_NAMES[t], value: t }))
141
+ });
142
+ if (!res.aiType) {
143
+ console.log(chalk.yellow(" Cancelled."));
144
+ return;
145
+ }
146
+ aiType = res.aiType;
147
+ }
148
+ const targets = aiType === "all" ? AI_TYPES.filter((t) => t !== "all") : [aiType];
149
+ const spinner = ora("Installing\u2026").start();
150
+ const installed = [];
151
+ try {
152
+ for (const t of targets) {
153
+ const paths = AI_SKILL_PATHS[t];
154
+ if (!options.force && await exists(paths.skillMd)) {
155
+ spinner.warn(`${AI_DISPLAY_NAMES[t]}: already installed (use --force to overwrite)`);
156
+ spinner.start();
157
+ continue;
158
+ }
159
+ await mkdir(dirname(paths.skillMd), { recursive: true });
160
+ await mkdir(dirname(paths.scriptPy), { recursive: true });
161
+ await writeFile(paths.skillMd, buildSkillMd(paths.scriptPy), "utf-8");
162
+ await writeFile(paths.scriptPy, SEARCH_PY, "utf-8");
163
+ installed.push(AI_DISPLAY_NAMES[t]);
164
+ }
165
+ spinner.succeed("Done!");
166
+ if (installed.length > 0) {
167
+ console.log();
168
+ installed.forEach((name) => console.log(chalk.green(` \u2713 ${name}`)));
169
+ }
170
+ console.log();
171
+ console.log(chalk.bold(" Next steps:"));
172
+ console.log(chalk.dim(" 1. Set your API key:"));
173
+ console.log(chalk.cyan(" export LOKUMA_API_KEY=lokuma_your_key_here"));
174
+ console.log(chalk.dim(" 2. Get a key \u2192 https://lokuma.io"));
175
+ console.log(chalk.dim(" 3. Restart your AI assistant"));
176
+ console.log(chalk.dim(' 4. Try: "Build a landing page for a SaaS product"'));
177
+ console.log();
178
+ } catch (err) {
179
+ spinner.fail("Installation failed");
180
+ if (err instanceof Error) console.error(chalk.red(` ${err.message}`));
181
+ process.exit(1);
182
+ }
183
+ }
184
+
185
+ // src/index.ts
186
+ var __filename = fileURLToPath2(import.meta.url);
187
+ var __dirname = dirname2(__filename);
188
+ var pkg = JSON.parse(readFileSync2(join2(__dirname, "../package.json"), "utf-8"));
189
+ var program = new Command();
190
+ program.name("lokuma").description("Install Lokuma design intelligence skill for AI coding assistants").version(pkg.version);
191
+ program.command("init").description("Install Lokuma skill to current project").option("-a, --ai <type>", `AI assistant type (${AI_TYPES.join(", ")})`).option("-f, --force", "Overwrite existing files").action(async (options) => {
192
+ if (options.ai && !AI_TYPES.includes(options.ai)) {
193
+ console.error(`Invalid AI type: ${options.ai}`);
194
+ console.error(`Valid types: ${AI_TYPES.join(", ")}`);
195
+ process.exit(1);
196
+ }
197
+ await initCommand({
198
+ ai: options.ai,
199
+ force: options.force
200
+ });
201
+ });
202
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "lokuma-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI to install Lokuma design intelligence skill for AI coding assistants",
5
+ "type": "module",
6
+ "bin": {
7
+ "lokuma": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "assets"
12
+ ],
13
+ "scripts": {
14
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=esm --external:commander --external:chalk --external:ora --external:prompts",
15
+ "dev": "node --loader ts-node/esm src/index.ts",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "lokuma",
20
+ "ui",
21
+ "ux",
22
+ "design",
23
+ "claude",
24
+ "cursor",
25
+ "windsurf",
26
+ "copilot",
27
+ "kiro",
28
+ "codex",
29
+ "ai",
30
+ "skill"
31
+ ],
32
+ "author": "s1non",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "chalk": "^5.3.0",
36
+ "commander": "^12.1.0",
37
+ "ora": "^8.1.1",
38
+ "prompts": "^2.4.2"
39
+ },
40
+ "devDependencies": {
41
+ "@types/bun": "^1.1.14",
42
+ "@types/node": "^22.10.1",
43
+ "@types/prompts": "^2.4.9",
44
+ "esbuild": "^0.27.4",
45
+ "typescript": "^5.7.2"
46
+ }
47
+ }