@every-env/compound-plugin 0.8.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to the `@every-env/compound-plugin` CLI tool will be documen
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.9.0] - 2026-02-17
9
+
10
+ ### Added
11
+
12
+ - **Kiro CLI target** — `--to kiro` converts plugins to `.kiro/` format with custom agent JSON configs, prompt files, skills, steering files, and `mcp.json`. Only stdio MCP servers are supported ([#196](https://github.com/EveryInc/compound-engineering-plugin/pull/196)) — thanks [@krthr](https://github.com/krthr)!
13
+
14
+ ---
15
+
8
16
  ## [0.8.0] - 2026-02-17
9
17
 
10
18
  ### Added
package/README.md CHANGED
@@ -18,9 +18,9 @@ A Claude Code plugin marketplace featuring the **Compound Engineering Plugin**
18
18
  /add-plugin compound-engineering
19
19
  ```
20
20
 
21
- ## OpenCode, Codex, Droid, Pi, Gemini & GitHub Copilot (experimental) Install
21
+ ## OpenCode, Codex, Droid, Pi, Gemini, Copilot & Kiro (experimental) Install
22
22
 
23
- This repo includes a Bun/TypeScript CLI that converts Claude Code plugins to OpenCode, Codex, Factory Droid, Pi, Gemini CLI and GitHub Copilot.
23
+ This repo includes a Bun/TypeScript CLI that converts Claude Code plugins to OpenCode, Codex, Factory Droid, Pi, Gemini CLI, GitHub Copilot, and Kiro CLI.
24
24
 
25
25
  ```bash
26
26
  # convert the compound-engineering plugin into OpenCode format
@@ -40,6 +40,9 @@ bunx @every-env/compound-plugin install compound-engineering --to gemini
40
40
 
41
41
  # convert to GitHub Copilot format
42
42
  bunx @every-env/compound-plugin install compound-engineering --to copilot
43
+
44
+ # convert to Kiro CLI format
45
+ bunx @every-env/compound-plugin install compound-engineering --to kiro
43
46
  ```
44
47
 
45
48
  Local dev:
@@ -54,6 +57,7 @@ Droid output is written to `~/.factory/` with commands, droids (agents), and ski
54
57
  Pi output is written to `~/.pi/agent/` by default with prompts, skills, extensions, and `compound-engineering/mcporter.json` for MCPorter interoperability.
55
58
  Gemini output is written to `.gemini/` with skills (from agents), commands (`.toml`), and `settings.json` (MCP servers). Namespaced commands create directory structure (`workflows:plan` → `commands/workflows/plan.toml`). Skills use the identical SKILL.md standard and pass through unchanged.
56
59
  Copilot output is written to `.github/` with agents (`.agent.md`), skills (`SKILL.md`), and `copilot-mcp-config.json`. Agents get Copilot frontmatter (`description`, `tools: ["*"]`, `infer: true`), commands are converted to agent skills, and MCP server env vars are prefixed with `COPILOT_MCP_`.
60
+ Kiro output is written to `.kiro/` with custom agents (`.json` configs + prompt `.md` files), skills (from commands), pass-through skills, steering files (from CLAUDE.md), and `mcp.json`. Agents get `includeMcpJson: true` for MCP server access. Only stdio MCP servers are supported (HTTP servers are skipped with a warning).
57
61
 
58
62
  All provider targets are experimental and may change as the formats evolve.
59
63
 
@@ -0,0 +1,171 @@
1
+ # Kiro CLI Spec (Custom Agents, Skills, Steering, MCP, Settings)
2
+
3
+ Last verified: 2026-02-17
4
+
5
+ ## Primary sources
6
+
7
+ ```
8
+ https://kiro.dev/docs/cli/
9
+ https://kiro.dev/docs/cli/custom-agents/configuration-reference/
10
+ https://kiro.dev/docs/cli/skills/
11
+ https://kiro.dev/docs/cli/steering/
12
+ https://kiro.dev/docs/cli/mcp/
13
+ https://kiro.dev/docs/cli/hooks/
14
+ https://agentskills.io
15
+ ```
16
+
17
+ ## Config locations
18
+
19
+ - Project-level config: `.kiro/` directory at project root.
20
+ - No global/user-level config directory — all config is project-scoped.
21
+
22
+ ## Directory structure
23
+
24
+ ```
25
+ .kiro/
26
+ ├── agents/
27
+ │ ├── <name>.json # Agent configuration
28
+ │ └── prompts/
29
+ │ └── <name>.md # Agent prompt files
30
+ ├── skills/
31
+ │ └── <name>/
32
+ │ └── SKILL.md # Skill definition
33
+ ├── steering/
34
+ │ └── <name>.md # Always-on context files
35
+ └── settings/
36
+ └── mcp.json # MCP server configuration
37
+ ```
38
+
39
+ ## Custom agents (JSON config + prompt files)
40
+
41
+ - Custom agents are JSON files in `.kiro/agents/`.
42
+ - Each agent has a corresponding prompt `.md` file, referenced via `file://` URI.
43
+ - Agent config has 14 possible fields (see below).
44
+ - Agents are activated by user selection (no auto-activation).
45
+ - The converter outputs a subset of fields relevant to converted plugins.
46
+
47
+ ### Agent config fields
48
+
49
+ | Field | Type | Used in conversion | Notes |
50
+ |---|---|---|---|
51
+ | `name` | string | Yes | Agent display name |
52
+ | `description` | string | Yes | Human-readable description |
53
+ | `prompt` | string or `file://` URI | Yes | System prompt or file reference |
54
+ | `tools` | string[] | Yes (`["*"]`) | Available tools |
55
+ | `resources` | string[] | Yes | `file://`, `skill://`, `knowledgeBase` URIs |
56
+ | `includeMcpJson` | boolean | Yes (`true`) | Inherit project MCP servers |
57
+ | `welcomeMessage` | string | Yes | Agent switch greeting |
58
+ | `mcpServers` | object | No | Per-agent MCP config (use includeMcpJson instead) |
59
+ | `toolAliases` | Record | No | Tool name remapping |
60
+ | `allowedTools` | string[] | No | Auto-approve patterns |
61
+ | `toolsSettings` | object | No | Per-tool configuration |
62
+ | `hooks` | object | No (future work) | 5 trigger types |
63
+ | `model` | string | No | Model selection |
64
+ | `keyboardShortcut` | string | No | Quick-switch shortcut |
65
+
66
+ ### Example agent config
67
+
68
+ ```json
69
+ {
70
+ "name": "security-reviewer",
71
+ "description": "Reviews code for security vulnerabilities",
72
+ "prompt": "file://./prompts/security-reviewer.md",
73
+ "tools": ["*"],
74
+ "resources": [
75
+ "file://.kiro/steering/**/*.md",
76
+ "skill://.kiro/skills/**/SKILL.md"
77
+ ],
78
+ "includeMcpJson": true,
79
+ "welcomeMessage": "Switching to security-reviewer. Reviews code for security vulnerabilities"
80
+ }
81
+ ```
82
+
83
+ ## Skills (SKILL.md standard)
84
+
85
+ - Skills follow the open [Agent Skills](https://agentskills.io) standard.
86
+ - A skill is a folder containing `SKILL.md` plus optional supporting files.
87
+ - Skills live in `.kiro/skills/`.
88
+ - `SKILL.md` uses YAML frontmatter with `name` and `description` fields.
89
+ - Kiro activates skills on demand based on description matching.
90
+ - The `description` field is critical — Kiro uses it to decide when to activate the skill.
91
+
92
+ ### Constraints
93
+
94
+ - Skill name: max 64 characters, pattern `^[a-z][a-z0-9-]*$`, no consecutive hyphens (`--`).
95
+ - Skill description: max 1024 characters.
96
+ - Skill name must match parent directory name.
97
+
98
+ ### Example
99
+
100
+ ```yaml
101
+ ---
102
+ name: workflows-plan
103
+ description: Plan work by analyzing requirements and creating actionable steps
104
+ ---
105
+
106
+ # Planning Workflow
107
+
108
+ Detailed instructions...
109
+ ```
110
+
111
+ ## Steering files
112
+
113
+ - Markdown files in `.kiro/steering/`.
114
+ - Always loaded into every agent session's context.
115
+ - Equivalent to Claude Code's CLAUDE.md.
116
+ - Used for project-wide instructions, coding standards, and conventions.
117
+
118
+ ## MCP server configuration
119
+
120
+ - MCP servers are configured in `.kiro/settings/mcp.json`.
121
+ - **Only stdio transport is supported** — `command` + `args` + `env`.
122
+ - HTTP/SSE transport (`url`, `headers`) is NOT supported by Kiro CLI.
123
+ - The converter skips HTTP-only MCP servers with a warning.
124
+
125
+ ### Example
126
+
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "playwright": {
131
+ "command": "npx",
132
+ "args": ["-y", "@anthropic/mcp-playwright"]
133
+ },
134
+ "context7": {
135
+ "command": "npx",
136
+ "args": ["-y", "@context7/mcp-server"]
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ ## Hooks
143
+
144
+ - Kiro supports 5 hook trigger types: `agentSpawn`, `userPromptSubmit`, `preToolUse`, `postToolUse`, `stop`.
145
+ - Hooks are configured inside agent JSON configs (not separate files).
146
+ - 3 of 5 triggers map to Claude Code hooks (`preToolUse`, `postToolUse`, `stop`).
147
+ - Not converted by the plugin converter for MVP — a warning is emitted.
148
+
149
+ ## Conversion lossy mappings
150
+
151
+ | Claude Code Feature | Kiro Status | Notes |
152
+ |---|---|---|
153
+ | `Edit` tool (surgical replacement) | Degraded -> `write` (full-file) | Kiro write overwrites entire files |
154
+ | `context: fork` | Lost | No execution isolation control |
155
+ | `!`command`` dynamic injection | Lost | No pre-processing of markdown |
156
+ | `disable-model-invocation` | Lost | No invocation control |
157
+ | `allowed-tools` per skill | Lost | No tool permission scoping per skill |
158
+ | `$ARGUMENTS` interpolation | Lost | No structured argument passing |
159
+ | Claude hooks | Skipped | Future follow-up (near-1:1 for 3/5 triggers) |
160
+ | HTTP MCP servers | Skipped | Kiro only supports stdio transport |
161
+
162
+ ## Overwrite behavior during conversion
163
+
164
+ | Content Type | Strategy | Rationale |
165
+ |---|---|---|
166
+ | Generated agents (JSON + prompt) | Overwrite | Generated, not user-authored |
167
+ | Generated skills (from commands) | Overwrite | Generated, not user-authored |
168
+ | Copied skills (pass-through) | Overwrite | Plugin is source of truth |
169
+ | Steering files | Overwrite | Generated from CLAUDE.md |
170
+ | `mcp.json` | Merge with backup | User may have added their own servers |
171
+ | User-created agents/skills | Preserved | Don't delete orphans |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@every-env/compound-plugin",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {
@@ -23,7 +23,7 @@ export default defineCommand({
23
23
  to: {
24
24
  type: "string",
25
25
  default: "opencode",
26
- description: "Target format (opencode | codex | droid | cursor | pi | gemini)",
26
+ description: "Target format (opencode | codex | droid | cursor | pi | gemini | kiro)",
27
27
  },
28
28
  output: {
29
29
  type: "string",
@@ -146,5 +146,6 @@ function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHo
146
146
  if (targetName === "droid") return path.join(os.homedir(), ".factory")
147
147
  if (targetName === "cursor") return path.join(outputRoot, ".cursor")
148
148
  if (targetName === "gemini") return path.join(outputRoot, ".gemini")
149
+ if (targetName === "kiro") return path.join(outputRoot, ".kiro")
149
150
  return outputRoot
150
151
  }
@@ -25,7 +25,7 @@ export default defineCommand({
25
25
  to: {
26
26
  type: "string",
27
27
  default: "opencode",
28
- description: "Target format (opencode | codex | droid | cursor | pi | copilot | gemini)",
28
+ description: "Target format (opencode | codex | droid | cursor | pi | copilot | gemini | kiro)",
29
29
  },
30
30
  output: {
31
31
  type: "string",
@@ -191,6 +191,10 @@ function resolveTargetOutputRoot(
191
191
  const base = hasExplicitOutput ? outputRoot : process.cwd()
192
192
  return path.join(base, ".github")
193
193
  }
194
+ if (targetName === "kiro") {
195
+ const base = hasExplicitOutput ? outputRoot : process.cwd()
196
+ return path.join(base, ".kiro")
197
+ }
194
198
  return outputRoot
195
199
  }
196
200
 
@@ -0,0 +1,262 @@
1
+ import { readFileSync, existsSync } from "fs"
2
+ import path from "path"
3
+ import { formatFrontmatter } from "../utils/frontmatter"
4
+ import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
5
+ import type {
6
+ KiroAgent,
7
+ KiroAgentConfig,
8
+ KiroBundle,
9
+ KiroMcpServer,
10
+ KiroSkill,
11
+ KiroSteeringFile,
12
+ } from "../types/kiro"
13
+ import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
14
+
15
+ export type ClaudeToKiroOptions = ClaudeToOpenCodeOptions
16
+
17
+ const KIRO_SKILL_NAME_MAX_LENGTH = 64
18
+ const KIRO_SKILL_NAME_PATTERN = /^[a-z][a-z0-9-]*$/
19
+ const KIRO_DESCRIPTION_MAX_LENGTH = 1024
20
+
21
+ const CLAUDE_TO_KIRO_TOOLS: Record<string, string> = {
22
+ Bash: "shell",
23
+ Write: "write",
24
+ Read: "read",
25
+ Edit: "write", // NOTE: Kiro write is full-file, not surgical edit. Lossy mapping.
26
+ Glob: "glob",
27
+ Grep: "grep",
28
+ WebFetch: "web_fetch",
29
+ Task: "use_subagent",
30
+ }
31
+
32
+ export function convertClaudeToKiro(
33
+ plugin: ClaudePlugin,
34
+ _options: ClaudeToKiroOptions,
35
+ ): KiroBundle {
36
+ const usedSkillNames = new Set<string>()
37
+
38
+ // Pass-through skills are processed first — they're the source of truth
39
+ const skillDirs = plugin.skills.map((skill) => ({
40
+ name: skill.name,
41
+ sourceDir: skill.sourceDir,
42
+ }))
43
+ for (const skill of skillDirs) {
44
+ usedSkillNames.add(normalizeName(skill.name))
45
+ }
46
+
47
+ // Convert agents to Kiro custom agents
48
+ const agentNames = plugin.agents.map((a) => normalizeName(a.name))
49
+ const agents = plugin.agents.map((agent) => convertAgentToKiroAgent(agent, agentNames))
50
+
51
+ // Convert commands to skills (generated)
52
+ const generatedSkills = plugin.commands.map((command) =>
53
+ convertCommandToSkill(command, usedSkillNames, agentNames),
54
+ )
55
+
56
+ // Convert MCP servers (stdio only)
57
+ const mcpServers = convertMcpServers(plugin.mcpServers)
58
+
59
+ // Build steering files from CLAUDE.md
60
+ const steeringFiles = buildSteeringFiles(plugin, agentNames)
61
+
62
+ // Warn about hooks
63
+ if (plugin.hooks && Object.keys(plugin.hooks.hooks).length > 0) {
64
+ console.warn(
65
+ "Warning: Kiro CLI hooks use a different format (preToolUse/postToolUse inside agent configs). Hooks were skipped during conversion.",
66
+ )
67
+ }
68
+
69
+ return { agents, generatedSkills, skillDirs, steeringFiles, mcpServers }
70
+ }
71
+
72
+ function convertAgentToKiroAgent(agent: ClaudeAgent, knownAgentNames: string[]): KiroAgent {
73
+ const name = normalizeName(agent.name)
74
+ const description = sanitizeDescription(
75
+ agent.description ?? `Use this agent for ${agent.name} tasks`,
76
+ )
77
+
78
+ const config: KiroAgentConfig = {
79
+ name,
80
+ description,
81
+ prompt: `file://./prompts/${name}.md`,
82
+ tools: ["*"],
83
+ resources: [
84
+ "file://.kiro/steering/**/*.md",
85
+ "skill://.kiro/skills/**/SKILL.md",
86
+ ],
87
+ includeMcpJson: true,
88
+ welcomeMessage: `Switching to the ${name} agent. ${description}`,
89
+ }
90
+
91
+ let body = transformContentForKiro(agent.body.trim(), knownAgentNames)
92
+ if (agent.capabilities && agent.capabilities.length > 0) {
93
+ const capabilities = agent.capabilities.map((c) => `- ${c}`).join("\n")
94
+ body = `## Capabilities\n${capabilities}\n\n${body}`.trim()
95
+ }
96
+ if (body.length === 0) {
97
+ body = `Instructions converted from the ${agent.name} agent.`
98
+ }
99
+
100
+ return { name, config, promptContent: body }
101
+ }
102
+
103
+ function convertCommandToSkill(
104
+ command: ClaudeCommand,
105
+ usedNames: Set<string>,
106
+ knownAgentNames: string[],
107
+ ): KiroSkill {
108
+ const rawName = normalizeName(command.name)
109
+ const name = uniqueName(rawName, usedNames)
110
+
111
+ const description = sanitizeDescription(
112
+ command.description ?? `Converted from Claude command ${command.name}`,
113
+ )
114
+
115
+ const frontmatter: Record<string, unknown> = { name, description }
116
+
117
+ let body = transformContentForKiro(command.body.trim(), knownAgentNames)
118
+ if (body.length === 0) {
119
+ body = `Instructions converted from the ${command.name} command.`
120
+ }
121
+
122
+ const content = formatFrontmatter(frontmatter, body)
123
+ return { name, content }
124
+ }
125
+
126
+ /**
127
+ * Transform Claude Code content to Kiro-compatible content.
128
+ *
129
+ * 1. Task agent calls: Task agent-name(args) -> Use the use_subagent tool ...
130
+ * 2. Path rewriting: .claude/ -> .kiro/, ~/.claude/ -> ~/.kiro/
131
+ * 3. Slash command refs: /workflows:plan -> use the workflows-plan skill
132
+ * 4. Claude tool names: Bash -> shell, Read -> read, etc.
133
+ * 5. Agent refs: @agent-name -> the agent-name agent (only for known agent names)
134
+ */
135
+ export function transformContentForKiro(body: string, knownAgentNames: string[] = []): string {
136
+ let result = body
137
+
138
+ // 1. Transform Task agent calls
139
+ const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm
140
+ result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
141
+ return `${prefix}Use the use_subagent tool to delegate to the ${normalizeName(agentName)} agent: ${args.trim()}`
142
+ })
143
+
144
+ // 2. Rewrite .claude/ paths to .kiro/ (with word-boundary-like lookbehind)
145
+ result = result.replace(/(?<=^|\s|["'`])~\/\.claude\//gm, "~/.kiro/")
146
+ result = result.replace(/(?<=^|\s|["'`])\.claude\//gm, ".kiro/")
147
+
148
+ // 3. Slash command refs: /command-name -> skill activation language
149
+ result = result.replace(/(?<=^|\s)`?\/([a-zA-Z][a-zA-Z0-9_:-]*)`?/gm, (_match, cmdName: string) => {
150
+ const skillName = normalizeName(cmdName)
151
+ return `the ${skillName} skill`
152
+ })
153
+
154
+ // 4. Claude tool names -> Kiro tool names
155
+ for (const [claudeTool, kiroTool] of Object.entries(CLAUDE_TO_KIRO_TOOLS)) {
156
+ // Match tool name references: "the X tool", "using X", "use X to"
157
+ const toolPattern = new RegExp(`\\b${claudeTool}\\b(?=\\s+tool|\\s+to\\s)`, "g")
158
+ result = result.replace(toolPattern, kiroTool)
159
+ }
160
+
161
+ // 5. Transform @agent-name references (only for known agent names)
162
+ if (knownAgentNames.length > 0) {
163
+ const escapedNames = knownAgentNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
164
+ const agentRefPattern = new RegExp(`@(${escapedNames.join("|")})\\b`, "g")
165
+ result = result.replace(agentRefPattern, (_match, agentName: string) => {
166
+ return `the ${normalizeName(agentName)} agent`
167
+ })
168
+ }
169
+
170
+ return result
171
+ }
172
+
173
+ function convertMcpServers(
174
+ servers?: Record<string, ClaudeMcpServer>,
175
+ ): Record<string, KiroMcpServer> {
176
+ if (!servers || Object.keys(servers).length === 0) return {}
177
+
178
+ const result: Record<string, KiroMcpServer> = {}
179
+ for (const [name, server] of Object.entries(servers)) {
180
+ if (!server.command) {
181
+ console.warn(
182
+ `Warning: MCP server "${name}" has no command (HTTP/SSE transport). Kiro only supports stdio. Skipping.`,
183
+ )
184
+ continue
185
+ }
186
+
187
+ const entry: KiroMcpServer = { command: server.command }
188
+ if (server.args && server.args.length > 0) entry.args = server.args
189
+ if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
190
+
191
+ console.log(`MCP server "${name}" will execute: ${server.command}${server.args ? " " + server.args.join(" ") : ""}`)
192
+ result[name] = entry
193
+ }
194
+ return result
195
+ }
196
+
197
+ function buildSteeringFiles(plugin: ClaudePlugin, knownAgentNames: string[]): KiroSteeringFile[] {
198
+ const claudeMdPath = path.join(plugin.root, "CLAUDE.md")
199
+ if (!existsSync(claudeMdPath)) return []
200
+
201
+ let content: string
202
+ try {
203
+ content = readFileSync(claudeMdPath, "utf8")
204
+ } catch {
205
+ return []
206
+ }
207
+
208
+ if (!content || content.trim().length === 0) return []
209
+
210
+ const transformed = transformContentForKiro(content, knownAgentNames)
211
+ return [{ name: "compound-engineering", content: transformed }]
212
+ }
213
+
214
+ function normalizeName(value: string): string {
215
+ const trimmed = value.trim()
216
+ if (!trimmed) return "item"
217
+ let normalized = trimmed
218
+ .toLowerCase()
219
+ .replace(/[\\/]+/g, "-")
220
+ .replace(/[:\s]+/g, "-")
221
+ .replace(/[^a-z0-9_-]+/g, "-")
222
+ .replace(/-+/g, "-") // Collapse consecutive hyphens (Agent Skills standard)
223
+ .replace(/^-+|-+$/g, "")
224
+
225
+ // Enforce max length (truncate at last hyphen boundary)
226
+ if (normalized.length > KIRO_SKILL_NAME_MAX_LENGTH) {
227
+ normalized = normalized.slice(0, KIRO_SKILL_NAME_MAX_LENGTH)
228
+ const lastHyphen = normalized.lastIndexOf("-")
229
+ if (lastHyphen > 0) {
230
+ normalized = normalized.slice(0, lastHyphen)
231
+ }
232
+ normalized = normalized.replace(/-+$/g, "")
233
+ }
234
+
235
+ // Ensure name starts with a letter
236
+ if (normalized.length === 0 || !/^[a-z]/.test(normalized)) {
237
+ return "item"
238
+ }
239
+
240
+ return normalized
241
+ }
242
+
243
+ function sanitizeDescription(value: string, maxLength = KIRO_DESCRIPTION_MAX_LENGTH): string {
244
+ const normalized = value.replace(/\s+/g, " ").trim()
245
+ if (normalized.length <= maxLength) return normalized
246
+ const ellipsis = "..."
247
+ return normalized.slice(0, Math.max(0, maxLength - ellipsis.length)).trimEnd() + ellipsis
248
+ }
249
+
250
+ function uniqueName(base: string, used: Set<string>): string {
251
+ if (!used.has(base)) {
252
+ used.add(base)
253
+ return base
254
+ }
255
+ let index = 2
256
+ while (used.has(`${base}-${index}`)) {
257
+ index += 1
258
+ }
259
+ const name = `${base}-${index}`
260
+ used.add(name)
261
+ return name
262
+ }
@@ -5,18 +5,21 @@ import type { DroidBundle } from "../types/droid"
5
5
  import type { PiBundle } from "../types/pi"
6
6
  import type { CopilotBundle } from "../types/copilot"
7
7
  import type { GeminiBundle } from "../types/gemini"
8
+ import type { KiroBundle } from "../types/kiro"
8
9
  import { convertClaudeToOpenCode, type ClaudeToOpenCodeOptions } from "../converters/claude-to-opencode"
9
10
  import { convertClaudeToCodex } from "../converters/claude-to-codex"
10
11
  import { convertClaudeToDroid } from "../converters/claude-to-droid"
11
12
  import { convertClaudeToPi } from "../converters/claude-to-pi"
12
13
  import { convertClaudeToCopilot } from "../converters/claude-to-copilot"
13
14
  import { convertClaudeToGemini } from "../converters/claude-to-gemini"
15
+ import { convertClaudeToKiro } from "../converters/claude-to-kiro"
14
16
  import { writeOpenCodeBundle } from "./opencode"
15
17
  import { writeCodexBundle } from "./codex"
16
18
  import { writeDroidBundle } from "./droid"
17
19
  import { writePiBundle } from "./pi"
18
20
  import { writeCopilotBundle } from "./copilot"
19
21
  import { writeGeminiBundle } from "./gemini"
22
+ import { writeKiroBundle } from "./kiro"
20
23
 
21
24
  export type TargetHandler<TBundle = unknown> = {
22
25
  name: string
@@ -62,4 +65,10 @@ export const targets: Record<string, TargetHandler> = {
62
65
  convert: convertClaudeToGemini as TargetHandler<GeminiBundle>["convert"],
63
66
  write: writeGeminiBundle as TargetHandler<GeminiBundle>["write"],
64
67
  },
68
+ kiro: {
69
+ name: "kiro",
70
+ implemented: true,
71
+ convert: convertClaudeToKiro as TargetHandler<KiroBundle>["convert"],
72
+ write: writeKiroBundle as TargetHandler<KiroBundle>["write"],
73
+ },
65
74
  }
@@ -0,0 +1,122 @@
1
+ import path from "path"
2
+ import { backupFile, copyDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files"
3
+ import type { KiroBundle } from "../types/kiro"
4
+
5
+ export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): Promise<void> {
6
+ const paths = resolveKiroPaths(outputRoot)
7
+ await ensureDir(paths.kiroDir)
8
+
9
+ // Write agents
10
+ if (bundle.agents.length > 0) {
11
+ for (const agent of bundle.agents) {
12
+ // Validate name doesn't escape agents directory
13
+ validatePathSafe(agent.name, "agent")
14
+
15
+ // Write agent JSON config
16
+ await writeJson(
17
+ path.join(paths.agentsDir, `${agent.name}.json`),
18
+ agent.config,
19
+ )
20
+
21
+ // Write agent prompt file
22
+ await writeText(
23
+ path.join(paths.agentsDir, "prompts", `${agent.name}.md`),
24
+ agent.promptContent + "\n",
25
+ )
26
+ }
27
+ }
28
+
29
+ // Write generated skills (from commands)
30
+ if (bundle.generatedSkills.length > 0) {
31
+ for (const skill of bundle.generatedSkills) {
32
+ validatePathSafe(skill.name, "skill")
33
+ await writeText(
34
+ path.join(paths.skillsDir, skill.name, "SKILL.md"),
35
+ skill.content + "\n",
36
+ )
37
+ }
38
+ }
39
+
40
+ // Copy skill directories (pass-through)
41
+ if (bundle.skillDirs.length > 0) {
42
+ for (const skill of bundle.skillDirs) {
43
+ validatePathSafe(skill.name, "skill directory")
44
+ const destDir = path.join(paths.skillsDir, skill.name)
45
+
46
+ // Validate destination doesn't escape skills directory
47
+ const resolvedDest = path.resolve(destDir)
48
+ if (!resolvedDest.startsWith(path.resolve(paths.skillsDir))) {
49
+ console.warn(`Warning: Skill name "${skill.name}" escapes .kiro/skills/. Skipping.`)
50
+ continue
51
+ }
52
+
53
+ await copyDir(skill.sourceDir, destDir)
54
+ }
55
+ }
56
+
57
+ // Write steering files
58
+ if (bundle.steeringFiles.length > 0) {
59
+ for (const file of bundle.steeringFiles) {
60
+ validatePathSafe(file.name, "steering file")
61
+ await writeText(
62
+ path.join(paths.steeringDir, `${file.name}.md`),
63
+ file.content + "\n",
64
+ )
65
+ }
66
+ }
67
+
68
+ // Write MCP servers to mcp.json
69
+ if (Object.keys(bundle.mcpServers).length > 0) {
70
+ const mcpPath = path.join(paths.settingsDir, "mcp.json")
71
+ const backupPath = await backupFile(mcpPath)
72
+ if (backupPath) {
73
+ console.log(`Backed up existing mcp.json to ${backupPath}`)
74
+ }
75
+
76
+ // Merge with existing mcp.json if present
77
+ let existingConfig: Record<string, unknown> = {}
78
+ if (await pathExists(mcpPath)) {
79
+ try {
80
+ existingConfig = await readJson<Record<string, unknown>>(mcpPath)
81
+ } catch {
82
+ console.warn("Warning: existing mcp.json could not be parsed and will be replaced.")
83
+ }
84
+ }
85
+
86
+ const existingServers =
87
+ existingConfig.mcpServers && typeof existingConfig.mcpServers === "object"
88
+ ? (existingConfig.mcpServers as Record<string, unknown>)
89
+ : {}
90
+ const merged = { ...existingConfig, mcpServers: { ...existingServers, ...bundle.mcpServers } }
91
+ await writeJson(mcpPath, merged)
92
+ }
93
+ }
94
+
95
+ function resolveKiroPaths(outputRoot: string) {
96
+ const base = path.basename(outputRoot)
97
+ // If already pointing at .kiro, write directly into it
98
+ if (base === ".kiro") {
99
+ return {
100
+ kiroDir: outputRoot,
101
+ agentsDir: path.join(outputRoot, "agents"),
102
+ skillsDir: path.join(outputRoot, "skills"),
103
+ steeringDir: path.join(outputRoot, "steering"),
104
+ settingsDir: path.join(outputRoot, "settings"),
105
+ }
106
+ }
107
+ // Otherwise nest under .kiro
108
+ const kiroDir = path.join(outputRoot, ".kiro")
109
+ return {
110
+ kiroDir,
111
+ agentsDir: path.join(kiroDir, "agents"),
112
+ skillsDir: path.join(kiroDir, "skills"),
113
+ steeringDir: path.join(kiroDir, "steering"),
114
+ settingsDir: path.join(kiroDir, "settings"),
115
+ }
116
+ }
117
+
118
+ function validatePathSafe(name: string, label: string): void {
119
+ if (name.includes("..") || name.includes("/") || name.includes("\\")) {
120
+ throw new Error(`${label} name contains unsafe path characters: ${name}`)
121
+ }
122
+ }