11import { formatFrontmatter } from "../utils/frontmatter"
2+ import { appendCompatibilityNoteIfNeeded , normalizePiSkillName , transformPiBodyContent , uniquePiSkillName , type PiNameMaps } from "../utils/pi-skills"
23import type { ClaudeAgent , ClaudeCommand , ClaudeMcpServer , ClaudePlugin } from "../types/claude"
34import type {
45 PiBundle ,
@@ -18,13 +19,41 @@ export function convertClaudeToPi(
1819 _options : ClaudeToPiOptions ,
1920) : PiBundle {
2021 const promptNames = new Set < string > ( )
21- const usedSkillNames = new Set < string > ( plugin . skills . map ( ( skill ) => normalizeName ( skill . name ) ) )
22+ const usedSkillNames = new Set < string > ( )
2223
23- const prompts = plugin . commands
24+ const sortedSkills = [ ...plugin . skills ] . sort ( ( a , b ) => a . name < b . name ? - 1 : a . name > b . name ? 1 : 0 )
25+ const sortedAgents = [ ...plugin . agents ] . sort ( ( a , b ) => a . name < b . name ? - 1 : a . name > b . name ? 1 : 0 )
26+
27+ const skillDirs = sortedSkills . map ( ( skill ) => ( {
28+ name : uniquePiSkillName ( normalizePiSkillName ( skill . name ) , usedSkillNames ) ,
29+ sourceDir : skill . sourceDir ,
30+ } ) )
31+
32+ const agentNames = sortedAgents . map ( ( agent ) =>
33+ uniquePiSkillName ( normalizePiSkillName ( agent . name ) , usedSkillNames ) ,
34+ )
35+
36+ const agentMap : Record < string , string > = { }
37+ sortedAgents . forEach ( ( agent , i ) => { agentMap [ agent . name ] = agentNames [ i ] } )
38+
39+ const skillMap : Record < string , string > = { }
40+ sortedSkills . forEach ( ( skill , i ) => { skillMap [ skill . name ] = skillDirs [ i ] . name } )
41+
42+ const convertibleCommands = [ ...plugin . commands ]
2443 . filter ( ( command ) => ! command . disableModelInvocation )
25- . map ( ( command ) => convertPrompt ( command , promptNames ) )
44+ . sort ( ( a , b ) => a . name < b . name ? - 1 : a . name > b . name ? 1 : 0 )
45+ const promptTargetNames = convertibleCommands . map ( ( command ) =>
46+ uniquePiSkillName ( normalizePiSkillName ( command . name ) , promptNames ) ,
47+ )
2648
27- const generatedSkills = plugin . agents . map ( ( agent ) => convertAgent ( agent , usedSkillNames ) )
49+ const promptMap : Record < string , string > = { }
50+ convertibleCommands . forEach ( ( command , i ) => { promptMap [ command . name ] = promptTargetNames [ i ] } )
51+
52+ const nameMaps : PiNameMaps = { agents : agentMap , skills : skillMap , prompts : promptMap }
53+
54+ const prompts = convertibleCommands . map ( ( command , i ) => convertPrompt ( command , promptTargetNames [ i ] , nameMaps ) )
55+
56+ const generatedSkills = sortedAgents . map ( ( agent , i ) => convertAgent ( agent , agentNames [ i ] , nameMaps ) )
2857
2958 const extensions = [
3059 {
@@ -35,34 +64,29 @@ export function convertClaudeToPi(
3564
3665 return {
3766 prompts,
38- skillDirs : plugin . skills . map ( ( skill ) => ( {
39- name : skill . name ,
40- sourceDir : skill . sourceDir ,
41- } ) ) ,
67+ skillDirs,
4268 generatedSkills,
4369 extensions,
4470 mcporterConfig : plugin . mcpServers ? convertMcpToMcporter ( plugin . mcpServers ) : undefined ,
71+ nameMaps,
4572 }
4673}
4774
48- function convertPrompt ( command : ClaudeCommand , usedNames : Set < string > ) {
49- const name = uniqueName ( normalizeName ( command . name ) , usedNames )
75+ function convertPrompt ( command : ClaudeCommand , name : string , nameMaps : PiNameMaps ) {
5076 const frontmatter : Record < string , unknown > = {
5177 description : command . description ,
5278 "argument-hint" : command . argumentHint ,
5379 }
5480
55- let body = transformContentForPi ( command . body )
56- body = appendCompatibilityNoteIfNeeded ( body )
81+ const body = appendCompatibilityNoteIfNeeded ( transformPiBodyContent ( command . body , nameMaps ) )
5782
5883 return {
5984 name,
6085 content : formatFrontmatter ( frontmatter , body . trim ( ) ) ,
6186 }
6287}
6388
64- function convertAgent ( agent : ClaudeAgent , usedNames : Set < string > ) : PiGeneratedSkill {
65- const name = uniqueName ( normalizeName ( agent . name ) , usedNames )
89+ function convertAgent ( agent : ClaudeAgent , name : string , nameMaps : PiNameMaps ) : PiGeneratedSkill {
6690 const description = sanitizeDescription (
6791 agent . description ?? `Converted from Claude agent ${ agent . name } ` ,
6892 )
@@ -77,77 +101,19 @@ function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): PiGeneratedSk
77101 sections . push ( `## Capabilities\n${ agent . capabilities . map ( ( capability ) => `- ${ capability } ` ) . join ( "\n" ) } ` )
78102 }
79103
80- const body = [
104+ const body = transformPiBodyContent ( [
81105 ...sections ,
82106 agent . body . trim ( ) . length > 0
83107 ? agent . body . trim ( )
84108 : `Instructions converted from the ${ agent . name } agent.` ,
85- ] . join ( "\n\n" )
109+ ] . join ( "\n\n" ) , nameMaps )
86110
87111 return {
88112 name,
89113 content : formatFrontmatter ( frontmatter , body ) ,
90114 }
91115}
92116
93- export function transformContentForPi ( body : string ) : string {
94- let result = body
95-
96- // Task repo-research-analyst(feature_description) or Task compound-engineering:research:repo-research-analyst(args)
97- // -> Run subagent with agent="repo-research-analyst" and task="feature_description"
98- const taskPattern = / ^ ( \s * - ? \s * ) T a s k \s + ( [ a - z ] [ a - z 0 - 9 : - ] * ) \( ( [ ^ ) ] * ) \) / gm
99- result = result . replace ( taskPattern , ( _match , prefix : string , agentName : string , args : string ) => {
100- const finalSegment = agentName . includes ( ":" ) ? agentName . split ( ":" ) . pop ( ) ! : agentName
101- const skillName = normalizeName ( finalSegment )
102- const trimmedArgs = args . trim ( ) . replace ( / \s + / g, " " )
103- return trimmedArgs
104- ? `${ prefix } Run subagent with agent=\"${ skillName } \" and task=\"${ trimmedArgs } \".`
105- : `${ prefix } Run subagent with agent=\"${ skillName } \".`
106- } )
107-
108- // Claude-specific tool references
109- result = result . replace ( / \b A s k U s e r Q u e s t i o n \b / g, "ask_user_question" )
110- result = result . replace ( / \b T o d o W r i t e \b / g, "file-based todos (todos/ + /skill:todo-create)" )
111- result = result . replace ( / \b T o d o R e a d \b / g, "file-based todos (todos/ + /skill:todo-create)" )
112-
113- // /command-name or /workflows:command-name -> /workflows-command-name
114- const slashCommandPattern = / (?< ! [: \w] ) \/ ( [ a - z ] [ a - z 0 - 9 _ : - ] * ?) (? = [ \s , . " ' ) \] } ` ] | $ ) / gi
115- result = result . replace ( slashCommandPattern , ( match , commandName : string ) => {
116- if ( commandName . includes ( "/" ) ) return match
117- if ( [ "dev" , "tmp" , "etc" , "usr" , "var" , "bin" , "home" ] . includes ( commandName ) ) {
118- return match
119- }
120-
121- if ( commandName . startsWith ( "skill:" ) ) {
122- const skillName = commandName . slice ( "skill:" . length )
123- return `/skill:${ normalizeName ( skillName ) } `
124- }
125-
126- const withoutPrefix = commandName . startsWith ( "prompts:" )
127- ? commandName . slice ( "prompts:" . length )
128- : commandName
129-
130- return `/${ normalizeName ( withoutPrefix ) } `
131- } )
132-
133- return result
134- }
135-
136- function appendCompatibilityNoteIfNeeded ( body : string ) : string {
137- if ( ! / \b m c p \b / i. test ( body ) ) return body
138-
139- const note = [
140- "" ,
141- "## Pi + MCPorter note" ,
142- "For MCP access in Pi, use MCPorter via the generated tools:" ,
143- "- `mcporter_list` to inspect available MCP tools" ,
144- "- `mcporter_call` to invoke a tool" ,
145- "" ,
146- ] . join ( "\n" )
147-
148- return body + note
149- }
150-
151117function convertMcpToMcporter ( servers : Record < string , ClaudeMcpServer > ) : PiMcporterConfig {
152118 const mcpServers : Record < string , PiMcporterServer > = { }
153119
@@ -173,36 +139,9 @@ function convertMcpToMcporter(servers: Record<string, ClaudeMcpServer>): PiMcpor
173139 return { mcpServers }
174140}
175141
176- function normalizeName ( value : string ) : string {
177- const trimmed = value . trim ( )
178- if ( ! trimmed ) return "item"
179- const normalized = trimmed
180- . toLowerCase ( )
181- . replace ( / [ \\ / ] + / g, "-" )
182- . replace ( / [: \s] + / g, "-" )
183- . replace ( / [ ^ a - z 0 - 9 _ - ] + / g, "-" )
184- . replace ( / - + / g, "-" )
185- . replace ( / ^ - + | - + $ / g, "" )
186- return normalized || "item"
187- }
188-
189142function sanitizeDescription ( value : string , maxLength = PI_DESCRIPTION_MAX_LENGTH ) : string {
190143 const normalized = value . replace ( / \s + / g, " " ) . trim ( )
191144 if ( normalized . length <= maxLength ) return normalized
192145 const ellipsis = "..."
193146 return normalized . slice ( 0 , Math . max ( 0 , maxLength - ellipsis . length ) ) . trimEnd ( ) + ellipsis
194147}
195-
196- function uniqueName ( base : string , used : Set < string > ) : string {
197- if ( ! used . has ( base ) ) {
198- used . add ( base )
199- return base
200- }
201- let index = 2
202- while ( used . has ( `${ base } -${ index } ` ) ) {
203- index += 1
204- }
205- const name = `${ base } -${ index } `
206- used . add ( name )
207- return name
208- }
0 commit comments