AWS Builder Center
I build a GUI to Migrate Skills into Kiro Powers

I build a GUI to Migrate Skills into Kiro Powers

Kiro use powers and not skills. Solve the manual work of migrating with simple few clicks.

Migrating Claude Skills to Kiro Powers

If you've been using Claude Code Skills and want to move to Kiro Powers, you know the pain: different file structures, different frontmatter formats, different naming conventions. I got tired of doing it manually, so I built a zero-dependency local GUI that handles the whole conversion in one click.
Here's how it works, and the full code you can copy-paste to run it yourself.

The Problem

Claude Skills and Kiro Powers are conceptually similar — both package reusable instructions for an AI coding assistant. But the file structures don't match:
Claude SkillKiro Power
SKILL.mdPOWER.md (with YAML frontmatter)
CLAUDE.mdsteering/contributing-guidelines.md
references/*.mdsteering/*.md
.claude-plugin/marketplace.jsonFrontmatter in POWER.md
README.mdNot needed
Beyond the file mapping, there's language cleanup needed — references to "this skill", "Claude Code", "SKILL.md" links, and marketplace-specific sections all need to be rewritten.

The Solution: A Local Python GUI

I wanted something that:
  • Runs entirely locally (no data leaves your machine)
  • Has zero dependencies (just Python 3, which you already have)
  • Provides a visual file browser so you can point-and-click
  • Auto-detects skill metadata from marketplace.json
  • Validates the output and flags any leftover Claude references
  • Auto-cleans Claude-specific language during conversion
The result is two files: server.py (the backend) and index.html (the frontend). Drop them in a folder, run python3 server.py, and open your browser.

How to Use It

Step 1: Create the files

Create a folder called skill-converter-gui/ and add the two files below.

Step 2: Run it

1
2
cd skill-converter-gui
python3 server.py --port 8888
Open http://localhost:8888 in your browser.

Step 3: Convert

  1. Browse to the folder containing your Claude Skill (it'll be highlighted green with a "SKILL" badge)
  2. Click it — the tool scans for SKILL.md, CLAUDE.md, marketplace.json, and reference files
  3. Review the pre-filled metadata (pulled from marketplace.json if available)
  4. Hit "Convert to Kiro Power"
  5. Review the log, generated files, and validation results
That's it. The output folder contains a ready-to-install Kiro Power.

What the Converter Does (6-Step Pipeline)

  1. Generates POWER.md with proper YAML frontmatter (name, displayName, description, keywords, author) from your SKILL.md content
  2. Copies references/ → steering/ preserving all your reference markdown files
  3. Converts CLAUDE.md → steering/contributing-guidelines.md, stripping Claude-specific sections like "Token Budget"
  4. Updates all internal links (references/ → steering/, ../SKILL.md → Main POWER.md) and auto-cleans Claude-specific language ("this skill" → "this power", "Claude Code" → "Kiro", etc.)
  5. Adds an "Available Steering Files" section to POWER.md listing all steering files
  6. Adds attribution crediting the original skill author and version

The Prompt to Give Your AI

If you want to build this yourself using Kiro (or any AI coding assistant), here's the prompt that captures the full requirements:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Build me a local GUI web application for converting Claude Code Skills into Kiro Powers.

Requirements:
- Zero dependencies — Python 3 standard library only (http.server, json, pathlib, shutil, re)
- Two files: server.py (backend) and index.html (frontend served from disk)
- Dark theme UI with a 4-step flow: folder browser → skill analysis → metadata config → conversion results

Backend API endpoints:
- GET / — serves index.html from the same directory
- GET /api/browse?path= — lists subdirectories, detects Claude Skills (folders containing SKILL.md)
- GET /api/scan?path= — analyzes a skill directory: checks for SKILL.md, CLAUDE.md, README.md, .claude-plugin/marketplace.json, references/*.md. Extracts metadata from marketplace.json if present.
- POST /api/convert — accepts {source, output, options} JSON. Runs a 6-step conversion pipeline:
1. Generate POWER.md with YAML frontmatter (name, displayName, description, keywords, author) + SKILL.md body content
2. Copy references/*.md → steering/*.md
3. Convert CLAUDE.md → steering/contributing-guidelines.md (strip "Token Budget" section, Claude marketplace lines)
4. Update all internal links (references/ → steering/, ../SKILL.md → Main POWER.md) AND auto-clean Claude-specific language using regex replacements:
- "This/this/The/the/a/A skill" → "power" equivalents
- "Claude Code skill/Skill" → "Kiro Power"
- "Claude Code" → "Kiro"
- "Claude marketplace" → "Kiro Powers panel"
- "SKILL.md" → "POWER.md"
5. Insert "## Available Steering Files" section listing all steering files
6. Add attribution section with author and source version
Returns: {success, log[], files[], validation[]}

Validation checks:
- Frontmatter has all required fields (name, displayName, description, keywords)
- No leftover references/ links, SKILL.md refs, or .claude-plugin refs
- No Claude-specific language remaining
- Steering files section present if steering/ directory exists

Frontend:
- File browser with directory navigation, "Up" button, skill auto-detection (green highlight + badge)
- Skill analysis grid showing which files are present
- Metadata form pre-filled from marketplace.json
- Conversion log with staggered animations, file tree output, color-coded validation results
- "Convert Another" button to reset

Run with: python3 server.py --port 8888

The Code

server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
#!/usr/bin/env python3
"""
Claude Skill to Kiro Power Converter GUI
Local web server with browser-based UI.

Usage:
python3 server.py [--port 8888]
Open http://localhost:8888
"""


import http.server
import json
import os
import re
import shutil
import sys
import urllib.parse
from pathlib import Path

PORT = int(sys.argv[sys.argv.index("--port") + 1]) if "--port" in sys.argv else 8888
SCRIPT_DIR = Path(__file__).parent

def scan_skill_dir(path: str) -> dict:
p = Path(path)
if not p.is_dir():
return {"error": f"Not a directory: {path}"}
result = {
"path": str(p.resolve()),
"has_skill_md": (p / "SKILL.md").is_file(),
"has_claude_md": (p / "CLAUDE.md").is_file(),
"has_readme": (p / "README.md").is_file(),
"has_marketplace": (p / ".claude-plugin" / "marketplace.json").is_file(),
"reference_files": [],
"metadata": None,
}
refs_dir = p / "references"
if refs_dir.is_dir():
result["reference_files"] = [f.name for f in refs_dir.iterdir() if f.suffix == ".md"]
if result["has_marketplace"]:
try:
with open(p / ".claude-plugin" / "marketplace.json") as f:
result["metadata"] = json.load(f)
except Exception:
pass
return result

def browse_directory(path: str) -> dict:
if not path:
path = str(Path.cwd())
p = Path(path).resolve()
if not p.is_dir():
return {"error": f"Not a directory: {path}"}

entries = []
try:
for item in sorted(p.iterdir()):
if item.name.startswith(".") and item.name != ".claude-plugin":
continue
if item.is_dir():
is_skill = (item / "SKILL.md").is_file()
entries.append({
"name": item.name,
"path": str(item),
"is_skill": is_skill,
})
except PermissionError:
return {"error": f"Permission denied: {path}"}

parent = str(p.parent) if p.parent != p else None
return {"current": str(p), "parent": parent, "entries": entries}

def validate_power(output_dir: str) -> list:
results = []
p = Path(output_dir)
power_md = p / "POWER.md"

if not power_md.is_file():
results.append({"level": "error", "msg": "POWER.md not found"})
return results

content = power_md.read_text()

# Check frontmatter
if content.startswith("---"):
fm_end = content.find("---", 3)
if fm_end > 0:
fm = content[3:fm_end]
for field in ["name", "displayName", "description", "keywords"]:
if field + ":" not in fm:
results.append({"level": "error", "msg": f"Missing frontmatter field: {field}"})
if not results:
results.append({"level": "success", "msg": "Frontmatter has all required fields"})
else:
results.append({"level": "error", "msg": "Malformed frontmatter (no closing ---)"})
else:
results.append({"level": "error", "msg": "No frontmatter found in POWER.md"})

# Check for leftover Claude references
if "references/" in content:
results.append({"level": "warning", "msg": "Found leftover 'references/' link in POWER.md"})
if "SKILL.md" in content:
results.append({"level": "warning", "msg": "Found leftover 'SKILL.md' reference in POWER.md"})
if ".claude-plugin" in content:
results.append({"level": "warning", "msg": "Found leftover '.claude-plugin' reference in POWER.md"})

# Check for Claude-specific language
claude_terms = ["Claude Code", "Claude marketplace", "this skill"]
for term in claude_terms:
if term.lower() in content.lower():
results.append({"level": "warning", "msg": f"Found Claude-specific language: '{term}'"})

# Check steering files section
if "## Available Steering Files" in content:
results.append({"level": "success", "msg": "Steering files section present"})
elif (p / "steering").is_dir():
results.append({"level": "warning", "msg": "Has steering/ dir but no steering files section in POWER.md"})

if not any(r["level"] == "error" for r in results):
results.insert(0, {"level": "success", "msg": "Power structure is valid"})

return results

def convert_skill(source: str, output: str, options: dict) -> dict:
log = []
src = Path(source)
out = Path(output)

if not src.is_dir():
return {"success": False, "error": f"Source not found: {source}"}
if not (src / "SKILL.md").is_file():
return {"success": False, "error": "No SKILL.md in source directory"}

# Clean and create output
if out.exists():
shutil.rmtree(out)
out.mkdir(parents=True, exist_ok=True)
log.append(f"📁 Created output: {out}")

name = options.get("name", src.name.lower().replace(" ", "-"))
display = options.get("displayName", name.replace("-", " ").title())
desc = options.get("description", "A Kiro Power converted from a Claude Code Skill.")
keywords = options.get("keywords", [name])
author = options.get("author", "Unknown")
include_claude = options.get("include_claude_md", True)

# Format keywords for frontmatter
kw_str = ", ".join(f'"{k}"' for k in keywords) if keywords else f'"{name}"'

# Step 1: Generate POWER.md
log.append("[1/6] Generating POWER.md with frontmatter...")
skill_content = (src / "SKILL.md").read_text()

# Strip existing frontmatter from source
if skill_content.startswith("---"):
end = skill_content.find("---", 3)
if end > 0:
skill_content = skill_content[end + 3:].lstrip("\n")

frontmatter = f"""---
name: "{name}"
displayName: "{display}"
description: "{desc}"
keywords: [{kw_str}]
author: "{author}"
---

"""

power_content = frontmatter + skill_content
log.append(" ✅ Frontmatter generated")

# Step 2: Copy references/ → steering/
log.append("[2/6] Copying reference files to steering/...")
steering_files = []
refs_dir = src / "references"
if refs_dir.is_dir():
steering_dir = out / "steering"
steering_dir.mkdir(exist_ok=True)
for ref_file in sorted(refs_dir.glob("*.md")):
shutil.copy2(ref_file, steering_dir / ref_file.name)
steering_files.append(ref_file.name)
log.append(f" 📄 references/{ref_file.name} → steering/{ref_file.name}")
else:
log.append(" ⏭️ No references/ directory found")

# Step 3: Process CLAUDE.md
log.append("[3/6] Processing CLAUDE.md...")
if (src / "CLAUDE.md").is_file() and include_claude:
claude_content = (src / "CLAUDE.md").read_text()
claude_content = claude_content.replace("This skill", "This power")
claude_content = claude_content.replace("this skill", "this power")
claude_content = claude_content.replace("SKILL.md", "POWER.md")
lines = claude_content.split("\n")
cleaned = []
skip = False
for line in lines:
if re.match(r"^## Token Budget", line):
skip = True
continue
if skip and re.match(r"^## ", line):
skip = False
if not skip:
if "claude code marketplace" not in line.lower() and "claude-specific" not in line.lower():
cleaned.append(line)
claude_content = "\n".join(cleaned)

steering_dir = out / "steering"
steering_dir.mkdir(exist_ok=True)
(steering_dir / "contributing-guidelines.md").write_text(claude_content)
steering_files.append("contributing-guidelines.md")
log.append(" ✅ Created steering/contributing-guidelines.md from CLAUDE.md")
else:
log.append(" ⏭️ No CLAUDE.md or skipped by config")

# Step 4: Update internal links + auto-clean Claude language
log.append("[4/6] Updating internal links and cleaning Claude language...")
power_content = power_content.replace("references/", "steering/")
power_content = power_content.replace("../SKILL.md", "Main POWER.md")
power_content = re.sub(
r"\[Main Skill File\]\(\.\./SKILL\.md\)", "Main POWER.md", power_content
)

# Auto-clean Claude-specific language from POWER.md body
replacements = [
(r"\bThis skill\b", "This power"),
(r"\bthis skill\b", "this power"),
(r"\bThe skill\b", "The power"),
(r"\bthe skill\b", "the power"),
(r"\ba skill\b", "a power"),
(r"\bA skill\b", "A power"),
(r"\bSkill\b(?!\.(md|json))", "Power"),
(r"\bClaude Code skill\b", "Kiro Power"),
(r"\bClaude Code Skill\b", "Kiro Power"),
(r"\bClaude skill\b", "Kiro Power"),
(r"\bClaude Skill\b", "Kiro Power"),
(r"\bClaude Code\b", "Kiro"),
(r"\bClaude marketplace\b", "Kiro Powers panel"),
(r"\bClaude code marketplace\b", "Kiro Powers panel"),
(r"SKILL\.md", "POWER.md"),
]
cleaned_count = 0
for pattern, replacement in replacements:
new_content, n = re.subn(pattern, replacement, power_content)
cleaned_count += n
power_content = new_content

if cleaned_count:
log.append(f" 🧹 Auto-cleaned {cleaned_count} Claude-specific term(s)")
log.append(" ✅ Links and language updated")

# Update links + auto-clean in steering files too
steering_dir = out / "steering"
if steering_dir.is_dir():
for sf in steering_dir.glob("*.md"):
text = sf.read_text()
text = text.replace("references/", "steering/")
text = text.replace("../SKILL.md", "Main POWER.md")
text = re.sub(
r"\[Main Skill File\]\(\.\./SKILL\.md\)", "Main POWER.md", text
)
for pattern, replacement in replacements:
text = re.sub(pattern, replacement, text)
sf.write_text(text)

# Step 5: Add steering files section
log.append("[5/6] Adding steering files section...")
if steering_files:
section = "\n## Available Steering Files\n\n"
for sf in steering_files:
name_no_ext = sf.replace(".md", "")
section += f"- **{name_no_ext}** - Content from {sf}\n"
section += "\n"

headings = [m.start() for m in re.finditer(r"^## ", power_content, re.MULTILINE)]
if len(headings) >= 2:
insert_pos = headings[1]
power_content = power_content[:insert_pos] + section + power_content[insert_pos:]
else:
power_content += section
log.append(f" ✅ Added {len(steering_files)} steering file(s) to section")

# Step 6: Add attribution
log.append("[6/6] Adding attribution...")
version = "unknown"
mp_file = src / ".claude-plugin" / "marketplace.json"
if mp_file.is_file():
try:
version = json.load(open(mp_file)).get("version", "unknown")
except Exception:
pass

power_content += f"""
## License & Attribution

**Power Author:** {author}

**Original Work:** This Kiro Power was converted from the original {name} skill by {author}.

**Source Version:** Based on v{version}.
"""


(out / "POWER.md").write_text(power_content)
log.append(" ✅ POWER.md written")

files = [str(f.relative_to(out)) for f in sorted(out.rglob("*")) if f.is_file()]
log.append(f"\n🎉 Conversion complete! {len(files)} file(s) created.")

validation = validate_power(str(out))

return {
"success": True,
"log": log,
"files": files,
"validation": validation,
"output": str(out),
}

class ConverterHandler(http.server.BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass

def send_json(self, data, status=200):
body = json.dumps(data).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)

def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
qs = dict(urllib.parse.parse_qsl(parsed.query))

if parsed.path == "/":
html_path = SCRIPT_DIR / "index.html"
if not html_path.is_file():
self.send_error(404, "index.html not found")
return
body = html_path.read_bytes()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)

elif parsed.path == "/api/browse":
self.send_json(browse_directory(qs.get("path", "")))

elif parsed.path == "/api/scan":
path = qs.get("path", "")
if not path:
self.send_json({"error": "Missing path parameter"}, 400)
return
self.send_json(scan_skill_dir(path))

else:
self.send_error(404)

def do_POST(self):
parsed = urllib.parse.urlparse(self.path)

if parsed.path == "/api/convert":
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length))
result = convert_skill(
source=body.get("source", ""),
output=body.get("output", ""),
options=body.get("options", {}),
)
self.send_json(result)
else:
self.send_error(404)

if __name__ == "__main__":
server = http.server.HTTPServer(("127.0.0.1", PORT), ConverterHandler)
print(f"\n 🔧 Claude Skill → Kiro Power Converter")
print(f" 🌐 http://localhost:{PORT}")
print(f" Press Ctrl+C to stop\n")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nShutting down.")
server.server_close()

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Skill → Kiro Power Converter</title>
<style>
:root {
--bg: #0a0b10; --surface: #13151e; --surface2: #1b1e2b;
--surface3: #232738; --border: #2a2f45; --border2: #363c58;
--text: #e8eaf2; --text2: #8890a8; --text3: #5c6380;
--accent: #7c6cf0; --accent2: #a78bfa; --accent-glow: rgba(124,108,240,0.15);
--green: #34d399; --green-bg: rgba(52,211,153,0.08);
--yellow: #fbbf24; --yellow-bg: rgba(251,191,36,0.08);
--red: #f87171; --red-bg: rgba(248,113,113,0.08);
--radius: 16px; --radius-sm: 10px;
--shadow: 0 4px 24px rgba(0,0,0,0.3), 0 1px 3px rgba(0,0,0,0.2);
--shadow-lg: 0 8px 40px rgba(0,0,0,0.4), 0 2px 8px rgba(0,0,0,0.3);
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg); color: var(--text); line-height: 1.6;
min-height: 100vh;
background-image:
radial-gradient(ellipse 80% 60% at 50% -20%, rgba(124,108,240,0.08), transparent),
radial-gradient(ellipse 60% 40% at 80% 100%, rgba(52,211,153,0.04), transparent);
}
::selection { background: rgba(124,108,240,0.3); }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--border2); }

.container { max-width: 880px; margin: 0 auto; padding: 2.5rem 2rem; }

header {
text-align: center; padding: 2.5rem 0 2rem;
margin-bottom: 2rem; position: relative;
}
.logo {
width: 56px; height: 56px; margin: 0 auto 1.25rem;
background: linear-gradient(135deg, var(--accent), #a78bfa);
border-radius: 16px; display: flex; align-items: center;
justify-content: center; font-size: 1.6rem;
box-shadow: 0 4px 20px rgba(124,108,240,0.3);
}
header h1 {
font-size: 1.75rem; font-weight: 700; letter-spacing: -0.02em;
background: linear-gradient(135deg, var(--text) 0%, var(--accent2) 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
header p {
color: var(--text2); margin-top: 0.6rem; font-size: 0.92rem;
max-width: 420px; margin-left: auto; margin-right: auto;
}
.header-line {
width: 48px; height: 3px; margin: 1.5rem auto 0;
background: linear-gradient(90deg, var(--accent), var(--accent2));
border-radius: 2px;
}

.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 1.75rem;
margin-bottom: 1.25rem; box-shadow: var(--shadow);
transition: border-color var(--transition), box-shadow var(--transition);
animation: cardIn 0.4s ease both;
}
.card:hover { border-color: var(--border2); }
@keyframes cardIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.card h2 {
font-size: 1rem; font-weight: 600; margin-bottom: 1.25rem;
display: flex; align-items: center; gap: 0.65rem;
letter-spacing: -0.01em;
}
.card h2 .step {
background: linear-gradient(135deg, var(--accent), var(--accent2));
color: white; width: 30px; height: 30px; border-radius: 10px;
display: inline-flex; align-items: center; justify-content: center;
font-size: 0.82rem; font-weight: 700; flex-shrink: 0;
box-shadow: 0 2px 8px rgba(124,108,240,0.25);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">⚡</div>
<h1>Claude Skill → Kiro Power</h1>
<p>Select a Claude Skill folder and convert it into a Kiro Power with one click</p>
<div class="header-line"></div>
</header>

<style>
label {
display: block; color: var(--text2); font-size: 0.8rem;
margin-bottom: 0.35rem; font-weight: 500; letter-spacing: 0.02em;
text-transform: uppercase;
}
input[type="text"], select {
width: 100%; padding: 0.65rem 0.9rem; background: var(--surface2);
border: 1px solid var(--border); border-radius: var(--radius-sm);
color: var(--text); font-size: 0.9rem; outline: none;
transition: border-color var(--transition), box-shadow var(--transition);
font-family: inherit;
}
input:focus, select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
input[readonly] { color: var(--text2); cursor: default; }
.input-row { display: flex; gap: 0.75rem; margin-bottom: 0.85rem; }
.input-row > div { flex: 1; }
.mb { margin-bottom: 0.85rem; }

.btn {
padding: 0.7rem 1.6rem; border: none; border-radius: var(--radius-sm);
font-size: 0.9rem; font-weight: 600; cursor: pointer;
transition: all var(--transition); display: inline-flex;
align-items: center; gap: 0.5rem; font-family: inherit;
letter-spacing: -0.01em;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent), #9b8afb);
color: white; box-shadow: 0 2px 12px rgba(124,108,240,0.3);
}
.btn-primary:hover {
box-shadow: 0 4px 20px rgba(124,108,240,0.45);
transform: translateY(-1px);
}
.btn-primary:active { transform: translateY(0); }
.btn-primary:disabled {
opacity: 0.5; cursor: not-allowed;
transform: none; box-shadow: none;
}
.btn-secondary {
background: var(--surface2); color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover {
border-color: var(--accent); background: var(--surface3);
}

.browser {
background: var(--surface2); border-radius: var(--radius-sm);
max-height: 300px; overflow-y: auto; border: 1px solid var(--border);
}
.browser-item {
padding: 0.6rem 0.9rem; cursor: pointer; display: flex;
align-items: center; gap: 0.6rem; font-size: 0.88rem;
border-bottom: 1px solid rgba(42,47,69,0.5);
transition: background var(--transition);
}
.browser-item:last-child { border-bottom: none; }
.browser-item:hover { background: var(--accent-glow); }
.browser-item.skill { background: var(--green-bg); }
.browser-item.skill:hover { background: rgba(52,211,153,0.14); }
.browser-path {
padding: 0.55rem 0.9rem; background: var(--bg); font-size: 0.78rem;
color: var(--text3); border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 0.5rem;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
position: sticky; top: 0; z-index: 1;
backdrop-filter: blur(8px);
}
.browser-path button {
background: var(--surface2); border: 1px solid var(--border);
color: var(--accent2); cursor: pointer; font-size: 0.75rem;
padding: 0.2rem 0.55rem; border-radius: 6px;
transition: all var(--transition); font-family: inherit;
}
.browser-path button:hover {
background: var(--surface3); border-color: var(--accent);
}
.badge {
font-size: 0.65rem; padding: 0.2rem 0.55rem; border-radius: 6px;
font-weight: 600; margin-left: auto; letter-spacing: 0.04em;
text-transform: uppercase;
}
.badge-skill { background: rgba(52,211,153,0.15); color: var(--green); }
.badge-dir { background: rgba(124,108,240,0.1); color: var(--accent2); }

.scan-result {
display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.6rem; margin-top: 0.75rem;
}
.scan-item {
background: var(--surface2); padding: 0.6rem 0.85rem;
border-radius: var(--radius-sm); font-size: 0.85rem;
display: flex; align-items: center; gap: 0.5rem;
border: 1px solid var(--border);
transition: border-color var(--transition);
}
.scan-item:hover { border-color: var(--border2); }

.log-area {
background: var(--bg); border-radius: var(--radius-sm); padding: 1.1rem;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 0.82rem; line-height: 1.9; max-height: 320px;
overflow-y: auto; border: 1px solid var(--border);
}
.log-line {
padding: 0.1rem 0;
animation: logIn 0.3s ease both;
}
@keyframes logIn {
from { opacity: 0; transform: translateX(-6px); }
to { opacity: 1; transform: translateX(0); }
}

.validation-item {
padding: 0.55rem 0.75rem; margin: 0.35rem 0;
border-radius: var(--radius-sm); font-size: 0.85rem;
display: flex; align-items: center; gap: 0.5rem;
border: 1px solid transparent;
transition: all var(--transition);
}
.validation-item.error {
background: var(--red-bg); color: var(--red);
border-color: rgba(248,113,113,0.15);
}
.validation-item.warning {
background: var(--yellow-bg); color: var(--yellow);
border-color: rgba(251,191,36,0.15);
}
.validation-item.success {
background: var(--green-bg); color: var(--green);
border-color: rgba(52,211,153,0.15);
}

.file-tree {
background: var(--surface2); border-radius: var(--radius-sm);
padding: 0.85rem 1.1rem; border: 1px solid var(--border);
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 0.82rem; margin-top: 0.75rem;
}
.file-tree div {
padding: 0.2rem 0; color: var(--text2);
transition: color var(--transition);
}
.file-tree div:hover { color: var(--text); }

.section-title {
font-size: 0.8rem; font-weight: 600; color: var(--text2);
text-transform: uppercase; letter-spacing: 0.04em;
margin-top: 1.25rem; margin-bottom: 0.5rem;
}

.hidden { display: none; }
.actions { display: flex; gap: 0.75rem; margin-top: 1.25rem; }

.checkbox-row {
display: flex; align-items: center; gap: 0.6rem;
margin: 0.65rem 0; font-size: 0.88rem;
}
.checkbox-row input[type="checkbox"] {
accent-color: var(--accent); width: 16px; height: 16px;
}
.checkbox-row label { text-transform: none; font-size: 0.88rem; margin: 0; }

.divider {
height: 1px; background: var(--border); margin: 1.5rem 0;
}

footer {
text-align: center; color: var(--text3); font-size: 0.78rem;
padding: 2.5rem 0 1.5rem; letter-spacing: 0.01em;
}
footer span { color: var(--accent2); }

@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
display: inline-block; width: 16px; height: 16px;
border: 2px solid rgba(255,255,255,0.2);
border-top-color: white; border-radius: 50%;
animation: spin 0.6s linear infinite;
}
</style>

<!-- Step 1: Browse -->
<div class="card" id="step1">
<h2><span class="step">1</span> Select Claude Skill Folder</h2>
<div class="browser" id="browser"></div>
<div id="selected-path" class="hidden" style="margin-top:0.85rem;">
<label>Selected skill</label>
<input type="text" id="source-path" readonly>
</div>
</div>

<!-- Step 2: Scan -->
<div class="card hidden" id="step2">
<h2><span class="step">2</span> Skill Analysis</h2>
<div id="scan-result"></div>
</div>

<!-- Step 3: Configure -->
<div class="card hidden" id="step3">
<h2><span class="step">3</span> Configure Power Metadata</h2>
<div class="input-row">
<div><label>Power Name (kebab-case)</label><input type="text" id="opt-name"></div>
<div><label>Display Name</label><input type="text" id="opt-display"></div>
</div>
<div class="mb"><label>Description</label><input type="text" id="opt-desc"></div>
<div class="input-row">
<div><label>Keywords (comma-separated)</label><input type="text" id="opt-keywords"></div>
<div><label>Author</label><input type="text" id="opt-author"></div>
</div>
<div class="mb"><label>Output Directory</label><input type="text" id="opt-output"></div>
<div class="checkbox-row">
<input type="checkbox" id="opt-claude-md" checked>
<label for="opt-claude-md">Include CLAUDE.md as contributing-guidelines.md</label>
</div>
<div class="actions">
<button class="btn btn-primary" id="convert-btn" onclick="doConvert()">⚡ Convert to Kiro Power</button>
</div>
</div>

<!-- Step 4: Results -->
<div class="card hidden" id="step4">
<h2><span class="step">4</span> Conversion Results</h2>
<div id="result-log" class="log-area"></div>
<div class="section-title">Generated Files</div>
<div id="result-files" class="file-tree"></div>
<div class="section-title">Validation</div>
<div id="result-validation"></div>
<div class="actions">
<button class="btn btn-secondary" onclick="location.reload()">↻ Convert Another</button>
</div>
</div>

<footer>Runs locally — no data leaves your machine · Built with <span>♥</span> for Kiro</footer>
</div>

<script>
let currentPath = '';
let scanData = null;

async function browse(path) {
const res = await fetch('/api/browse?path=' + encodeURIComponent(path || ''));
const data = await res.json();
if (data.error) { alert(data.error); return; }
currentPath = data.current;
renderBrowser(data);
}

function renderBrowser(data) {
const el = document.getElementById('browser');
let html = '<div class="browser-path">';
html += '<span>' + data.current + '</span>';
if (data.parent) {
html += ' <button onclick="browse(\'' + esc(data.parent) + '\')">↑ Up</button>';
}
html += '</div>';
for (const e of data.entries) {
const cls = e.is_skill ? 'browser-item skill' : 'browser-item';
const badge = e.is_skill
? '<span class="badge badge-skill">skill</span>'
: '<span class="badge badge-dir">dir</span>';
const icon = e.is_skill ? '📦' : '📁';
const action = e.is_skill
? 'selectSkill(\'' + esc(e.path) + '\')'
: 'browse(\'' + esc(e.path) + '\')';
html += '<div class="' + cls + '" onclick="' + action + '">';
html += icon + ' ' + e.name + badge + '</div>';
}
if (!data.entries.length) {
html += '<div class="browser-item" style="color:var(--text3)">No subdirectories found</div>';
}
el.innerHTML = html;
}

function esc(s) { return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); }

async function selectSkill(path) {
document.getElementById('source-path').value = path;
document.getElementById('selected-path').classList.remove('hidden');
const res = await fetch('/api/scan?path=' + encodeURIComponent(path));
scanData = await res.json();
renderScan(scanData);
document.getElementById('step2').classList.remove('hidden');
document.getElementById('step3').classList.remove('hidden');
prefillOptions(scanData);
document.getElementById('step2').scrollIntoView({behavior:'smooth'});
}

function renderScan(data) {
let html = '<div class="scan-result">';
html += scanItem(data.has_skill_md, 'SKILL.md');
html += scanItem(data.has_marketplace, 'marketplace.json');
html += scanItem(data.has_claude_md, 'CLAUDE.md');
html += scanItem(data.has_readme, 'README.md');
html += '</div>';
if (data.reference_files.length) {
html += '<div style="margin-top:0.85rem;font-size:0.85rem;color:var(--text2)">';
html += '📎 Reference files: ' + data.reference_files.join(', ') + '</div>';
}
document.getElementById('scan-result').innerHTML = html;
}

function scanItem(present, name) {
const icon = present ? '✅' : '⬜';
return '<div class="scan-item"><span>' + icon + '</span>' + name + '</div>';
}

function prefillOptions(data) {
const m = data.metadata || {};
document.getElementById('opt-name').value = m.name || '';
document.getElementById('opt-display').value = m.name
? m.name.split('-').map(function(w){return w.charAt(0).toUpperCase()+w.slice(1)}).join(' ') : '';
document.getElementById('opt-desc').value = m.description || '';
document.getElementById('opt-keywords').value = (m.keywords || []).join(', ');
document.getElementById('opt-author').value = m.author || '';
var name = m.name || data.path.split('/').pop().toLowerCase().replace(/[^a-z0-9-]/g,'-');
var parentDir = data.path.substring(0, data.path.lastIndexOf('/'));
document.getElementById('opt-output').value = parentDir + '/powers/' + name;
document.getElementById('opt-claude-md').checked = data.has_claude_md;
document.getElementById('opt-claude-md').disabled = !data.has_claude_md;
}

async function doConvert() {
var btn = document.getElementById('convert-btn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Converting...';

var kw = document.getElementById('opt-keywords').value;
var body = {
source: document.getElementById('source-path').value,
output: document.getElementById('opt-output').value,
options: {
name: document.getElementById('opt-name').value,
displayName: document.getElementById('opt-display').value,
description: document.getElementById('opt-desc').value,
keywords: kw ? kw.split(',').map(function(k){return k.trim()}).filter(Boolean) : [],
author: document.getElementById('opt-author').value,
include_claude_md: document.getElementById('opt-claude-md').checked
}
};

var res = await fetch('/api/convert', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
});
var result = await res.json();

if (!result.success) {
alert('Error: ' + result.error);
btn.disabled = false; btn.innerHTML = '⚡ Convert to Kiro Power';
return;
}

document.getElementById('step4').classList.remove('hidden');

var logEl = document.getElementById('result-log');
logEl.innerHTML = '';
result.log.forEach(function(l, i) {
var div = document.createElement('div');
div.className = 'log-line';
div.textContent = l;
div.style.animationDelay = (i * 0.05) + 's';
logEl.appendChild(div);
});

document.getElementById('result-files').innerHTML =
result.files.map(function(f){return '<div>📄 '+f+'</div>'}).join('');

document.getElementById('result-validation').innerHTML =
result.validation.map(function(v){
var icon = v.level==='error'?'❌':v.level==='warning'?'⚠️':'✅';
return '<div class="validation-item '+v.level+'">'+icon+' '+v.msg+'</div>';
}).join('');

btn.disabled = false; btn.innerHTML = '⚡ Convert to Kiro Power';
document.getElementById('step4').scrollIntoView({behavior:'smooth'});
}

browse('');
</script>
</body>
</html>

How I Built It

The whole thing came together in a single conversation with Kiro. Here's the rough sequence:
  1. I described the file mapping between Claude Skills and Kiro Powers
  2. Asked Kiro to build a zero-dependency Python server with a browser-based GUI
  3. The first version embedded HTML inside Python multi-line strings — that broke across multiple file writes. Lesson learned: keep HTML in a separate file and serve it from disk
  4. The conversion logic mirrors the shell script (convert-skill-to-power.sh) but adds auto-detection of skill metadata from marketplace.json and a visual validation step
  5. The auto-clean feature was added after the first conversion showed warnings for leftover Claude-specific language. Now it runs 15 regex replacements across both POWER.md and all steering files
The whole thing is ~380 lines of Python and ~300 lines of HTML/CSS/JS. No build step, no npm, no frameworks. Just python3 server.py and you're running.

What Gets Converted

Here's the file mapping the converter handles:
1
2
3
4
5
6
7
8
9
Claude Skill → Kiro Power
├── SKILL.md → POWER.md (with YAML frontmatter)
├── CLAUDE.md → steering/contributing-guidelines.md
├── references/ → steering/
│ ├── commit-types.md → steering/commit-types.md
│ └── team-rules.md → steering/team-rules.md
├── .claude-plugin/
│ └── marketplace.json → (metadata extracted into frontmatter)
└── README.md → (skipped — not needed)

Language Auto-Clean

The converter automatically rewrites Claude-specific terminology:
BeforeAfter
"This skill""This power"
"Claude Code Skill""Kiro Power"
"Claude Code""Kiro"
"Claude marketplace""Kiro Powers panel"
"SKILL.md""POWER.md"
references/ linkssteering/ links
This runs on both POWER.md and all steering files, so the output is clean and ready to install.

Try It

1
2
3
4
5
mkdir skill-converter-gui
# Save server.py and index.html from above into skill-converter-gui/
cd skill-converter-gui
python3 server.py --port 8888
# Open http://localhost:8888
Navigate to any folder containing a Claude Skill (look for the green "SKILL" badge), click it, review the metadata, and hit convert. The output is a ready-to-install Kiro Power.
Everything runs locally. No data leaves your machine.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article