@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 +8 -0
- package/README.md +6 -2
- package/docs/specs/kiro.md +171 -0
- package/package.json +1 -1
- package/src/commands/convert.ts +2 -1
- package/src/commands/install.ts +5 -1
- package/src/converters/claude-to-kiro.ts +262 -0
- package/src/targets/index.ts +9 -0
- package/src/targets/kiro.ts +122 -0
- package/src/types/kiro.ts +44 -0
- package/tests/kiro-converter.test.ts +381 -0
- package/tests/kiro-writer.test.ts +273 -0
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 &
|
|
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
|
|
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
package/src/commands/convert.ts
CHANGED
|
@@ -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
|
}
|
package/src/commands/install.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/targets/index.ts
CHANGED
|
@@ -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
|
+
}
|