1+ import { existsSync , readFileSync , readdirSync } from 'node:fs' ;
2+ import { join , resolve } from 'node:path' ;
3+ import yaml from 'js-yaml' ;
4+ import { loadAgentManifest , loadFileIfExists } from '../utils/loader.js' ;
5+ import { loadAllSkills , getAllowedTools } from '../utils/skill-loader.js' ;
6+ import { buildComplianceSection } from './shared.js' ;
7+
8+ /**
9+ * Export a gitagent to AWS Kiro CLI custom agent format.
10+ *
11+ * Kiro CLI uses a JSON config file (`.kiro/agents/<name>.json`) with:
12+ * - name, description, prompt (inline or file:// URI)
13+ * - mcpServers, tools, allowedTools
14+ * - model, hooks, resources
15+ *
16+ * Reference: https://kiro.dev/docs/cli/custom-agents/configuration-reference/
17+ */
18+ export interface KiroExport {
19+ config : Record < string , unknown > ;
20+ prompt : string ;
21+ }
22+
23+ export function exportToKiro ( dir : string ) : KiroExport {
24+ const agentDir = resolve ( dir ) ;
25+ const manifest = loadAgentManifest ( agentDir ) ;
26+
27+ const prompt = buildPrompt ( agentDir , manifest ) ;
28+ const config = buildConfig ( agentDir , manifest ) ;
29+
30+ return { config, prompt } ;
31+ }
32+
33+ export function exportToKiroString ( dir : string ) : string {
34+ const exp = exportToKiro ( dir ) ;
35+ const parts : string [ ] = [ ] ;
36+
37+ parts . push ( '# === .kiro/agents/<name>.json ===' ) ;
38+ parts . push ( JSON . stringify ( exp . config , null , 2 ) ) ;
39+ parts . push ( '\n# === prompt.md (referenced via file://./prompt.md) ===' ) ;
40+ parts . push ( exp . prompt ) ;
41+
42+ return parts . join ( '\n' ) ;
43+ }
44+
45+ function buildPrompt (
46+ agentDir : string ,
47+ manifest : ReturnType < typeof loadAgentManifest > ,
48+ ) : string {
49+ const parts : string [ ] = [ ] ;
50+
51+ parts . push ( `# ${ manifest . name } ` ) ;
52+ parts . push ( `${ manifest . description } ` ) ;
53+ parts . push ( '' ) ;
54+
55+ const soul = loadFileIfExists ( join ( agentDir , 'SOUL.md' ) ) ;
56+ if ( soul ) {
57+ parts . push ( soul ) ;
58+ parts . push ( '' ) ;
59+ }
60+
61+ const rules = loadFileIfExists ( join ( agentDir , 'RULES.md' ) ) ;
62+ if ( rules ) {
63+ parts . push ( rules ) ;
64+ parts . push ( '' ) ;
65+ }
66+
67+ const duty = loadFileIfExists ( join ( agentDir , 'DUTIES.md' ) ) ;
68+ if ( duty ) {
69+ parts . push ( duty ) ;
70+ parts . push ( '' ) ;
71+ }
72+
73+ const skillsDir = join ( agentDir , 'skills' ) ;
74+ const skills = loadAllSkills ( skillsDir ) ;
75+ if ( skills . length > 0 ) {
76+ parts . push ( '## Skills' ) ;
77+ parts . push ( '' ) ;
78+ for ( const skill of skills ) {
79+ const toolsList = getAllowedTools ( skill . frontmatter ) ;
80+ const toolsNote = toolsList . length > 0 ? `\nAllowed tools: ${ toolsList . join ( ', ' ) } ` : '' ;
81+ parts . push ( `### ${ skill . frontmatter . name } ` ) ;
82+ parts . push ( `${ skill . frontmatter . description } ${ toolsNote } ` ) ;
83+ parts . push ( '' ) ;
84+ parts . push ( skill . instructions ) ;
85+ parts . push ( '' ) ;
86+ }
87+ }
88+
89+ const toolsDir = join ( agentDir , 'tools' ) ;
90+ if ( existsSync ( toolsDir ) ) {
91+ const toolFiles = readdirSync ( toolsDir ) . filter ( f => f . endsWith ( '.yaml' ) ) ;
92+ if ( toolFiles . length > 0 ) {
93+ parts . push ( '## Tools' ) ;
94+ parts . push ( '' ) ;
95+ for ( const file of toolFiles ) {
96+ try {
97+ const content = readFileSync ( join ( toolsDir , file ) , 'utf-8' ) ;
98+ const toolConfig = yaml . load ( content ) as {
99+ name ?: string ;
100+ description ?: string ;
101+ input_schema ?: Record < string , unknown > ;
102+ } ;
103+ if ( toolConfig ?. name ) {
104+ parts . push ( `### ${ toolConfig . name } ` ) ;
105+ if ( toolConfig . description ) {
106+ parts . push ( toolConfig . description ) ;
107+ }
108+ if ( toolConfig . input_schema ) {
109+ parts . push ( '' ) ;
110+ parts . push ( '```yaml' ) ;
111+ parts . push ( yaml . dump ( toolConfig . input_schema ) . trimEnd ( ) ) ;
112+ parts . push ( '```' ) ;
113+ }
114+ parts . push ( '' ) ;
115+ }
116+ } catch { /* skip malformed tools */ }
117+ }
118+ }
119+ }
120+
121+ const knowledgeDir = join ( agentDir , 'knowledge' ) ;
122+ const indexPath = join ( knowledgeDir , 'index.yaml' ) ;
123+ if ( existsSync ( indexPath ) ) {
124+ const index = yaml . load ( readFileSync ( indexPath , 'utf-8' ) ) as {
125+ documents ?: Array < { path : string ; always_load ?: boolean } > ;
126+ } ;
127+
128+ if ( index . documents ) {
129+ const alwaysLoad = index . documents . filter ( d => d . always_load ) ;
130+ if ( alwaysLoad . length > 0 ) {
131+ parts . push ( '## Knowledge' ) ;
132+ parts . push ( '' ) ;
133+ for ( const doc of alwaysLoad ) {
134+ const content = loadFileIfExists ( join ( knowledgeDir , doc . path ) ) ;
135+ if ( content ) {
136+ parts . push ( `### ${ doc . path } ` ) ;
137+ parts . push ( content ) ;
138+ parts . push ( '' ) ;
139+ }
140+ }
141+ }
142+ }
143+ }
144+
145+ if ( manifest . compliance ) {
146+ const constraints = buildComplianceSection ( manifest . compliance ) ;
147+ if ( constraints ) {
148+ parts . push ( constraints ) ;
149+ parts . push ( '' ) ;
150+ }
151+ }
152+
153+ return parts . join ( '\n' ) . trimEnd ( ) + '\n' ;
154+ }
155+
156+ function buildConfig (
157+ agentDir : string ,
158+ manifest : ReturnType < typeof loadAgentManifest > ,
159+ ) : Record < string , unknown > {
160+ const config : Record < string , unknown > = { } ;
161+
162+ config . name = manifest . name ;
163+ if ( manifest . description ) {
164+ config . description = manifest . description ;
165+ }
166+
167+ // Use file:// URI for prompt so the markdown file is maintained separately
168+ config . prompt = 'file://./prompt.md' ;
169+
170+ if ( manifest . model ?. preferred ) {
171+ config . model = manifest . model . preferred ;
172+ }
173+
174+ // Collect tools from skills and tool definitions
175+ const tools = collectTools ( agentDir ) ;
176+ if ( tools . length > 0 ) {
177+ config . tools = tools ;
178+ config . allowedTools = tools ;
179+ }
180+
181+ // Map MCP servers from tools/*.yaml that declare mcp_server
182+ const mcpServers = collectMcpServers ( agentDir ) ;
183+ if ( Object . keys ( mcpServers ) . length > 0 ) {
184+ config . mcpServers = mcpServers ;
185+ }
186+
187+ // Hooks
188+ const hooks = buildHooks ( agentDir ) ;
189+ if ( hooks && Object . keys ( hooks ) . length > 0 ) {
190+ config . hooks = hooks ;
191+ }
192+
193+ // Sub-agents as welcome message hint
194+ if ( manifest . agents && Object . keys ( manifest . agents ) . length > 0 ) {
195+ const agentNames = Object . keys ( manifest . agents ) ;
196+ config . welcomeMessage = `This agent delegates to: ${ agentNames . join ( ', ' ) } ` ;
197+ }
198+
199+ return config ;
200+ }
201+
202+ function collectTools ( agentDir : string ) : string [ ] {
203+ const tools : Set < string > = new Set ( ) ;
204+
205+ const skillsDir = join ( agentDir , 'skills' ) ;
206+ const skills = loadAllSkills ( skillsDir ) ;
207+ for ( const skill of skills ) {
208+ for ( const tool of getAllowedTools ( skill . frontmatter ) ) {
209+ tools . add ( tool ) ;
210+ }
211+ }
212+
213+ const toolsDir = join ( agentDir , 'tools' ) ;
214+ if ( existsSync ( toolsDir ) ) {
215+ const files = readdirSync ( toolsDir ) . filter ( f => f . endsWith ( '.yaml' ) ) ;
216+ for ( const file of files ) {
217+ try {
218+ const content = readFileSync ( join ( toolsDir , file ) , 'utf-8' ) ;
219+ const toolConfig = yaml . load ( content ) as { name ?: string } ;
220+ if ( toolConfig ?. name ) {
221+ tools . add ( toolConfig . name ) ;
222+ }
223+ } catch { /* skip malformed tools */ }
224+ }
225+ }
226+
227+ return Array . from ( tools ) ;
228+ }
229+
230+ function collectMcpServers ( agentDir : string ) : Record < string , Record < string , unknown > > {
231+ const servers : Record < string , Record < string , unknown > > = { } ;
232+
233+ const toolsDir = join ( agentDir , 'tools' ) ;
234+ if ( ! existsSync ( toolsDir ) ) return servers ;
235+
236+ const files = readdirSync ( toolsDir ) . filter ( f => f . endsWith ( '.yaml' ) ) ;
237+ for ( const file of files ) {
238+ try {
239+ const content = readFileSync ( join ( toolsDir , file ) , 'utf-8' ) ;
240+ const toolConfig = yaml . load ( content ) as {
241+ mcp_server ?: {
242+ name ?: string ;
243+ command ?: string ;
244+ args ?: string [ ] ;
245+ env ?: Record < string , string > ;
246+ type ?: string ;
247+ url ?: string ;
248+ } ;
249+ } ;
250+ if ( toolConfig ?. mcp_server ?. name ) {
251+ const mcp = toolConfig . mcp_server ;
252+ const entry : Record < string , unknown > = { } ;
253+ if ( mcp . type ) entry . type = mcp . type ;
254+ if ( mcp . command ) entry . command = mcp . command ;
255+ if ( mcp . args ) entry . args = mcp . args ;
256+ if ( mcp . env ) entry . env = mcp . env ;
257+ if ( mcp . url ) entry . url = mcp . url ;
258+ servers [ mcp . name ! ] = entry ;
259+ }
260+ } catch { /* skip malformed tools */ }
261+ }
262+
263+ return servers ;
264+ }
265+
266+ function buildHooks ( agentDir : string ) : Record < string , unknown > | null {
267+ try {
268+ const hooksPath = join ( agentDir , 'hooks' , 'hooks.yaml' ) ;
269+ if ( ! existsSync ( hooksPath ) ) return null ;
270+
271+ const hooksYaml = readFileSync ( hooksPath , 'utf-8' ) ;
272+ const hooksConfig = yaml . load ( hooksYaml ) as {
273+ hooks : Record < string , Array < { script : string ; description ?: string } > > ;
274+ } ;
275+
276+ if ( ! hooksConfig . hooks || Object . keys ( hooksConfig . hooks ) . length === 0 ) return null ;
277+
278+ // Kiro CLI hook events use camelCase: preToolUse, postToolUse, stop, agentSpawn, userPromptSubmit
279+ const eventMap : Record < string , string > = {
280+ 'on_session_start' : 'agentSpawn' ,
281+ 'pre_tool_use' : 'preToolUse' ,
282+ 'post_tool_use' : 'postToolUse' ,
283+ 'pre_response' : 'userPromptSubmit' ,
284+ 'on_session_end' : 'stop' ,
285+ } ;
286+
287+ const kiroHooks : Record < string , Array < { command : string } > > = { } ;
288+
289+ for ( const [ event , hooks ] of Object . entries ( hooksConfig . hooks ) ) {
290+ const kiroEvent = eventMap [ event ] ;
291+ if ( ! kiroEvent ) continue ;
292+
293+ const validHooks = hooks . filter ( hook =>
294+ existsSync ( join ( agentDir , 'hooks' , hook . script ) )
295+ ) ;
296+ if ( validHooks . length === 0 ) continue ;
297+
298+ kiroHooks [ kiroEvent ] = validHooks . map ( hook => ( {
299+ command : `hooks/${ hook . script } ` ,
300+ } ) ) ;
301+ }
302+
303+ return Object . keys ( kiroHooks ) . length > 0 ? kiroHooks : null ;
304+ } catch {
305+ return null ;
306+ }
307+ }
0 commit comments