11import { formatFrontmatter } from "../utils/frontmatter"
2+ import { normalizePiSkillName , transformPiBodyContent , uniquePiSkillName } from "../utils/pi-skills"
23import type { ClaudeAgent , ClaudeCommand , ClaudeMcpServer , ClaudePlugin } from "../types/claude"
34import type {
45 PiBundle ,
@@ -18,12 +19,17 @@ 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
2324 const prompts = plugin . commands
2425 . filter ( ( command ) => ! command . disableModelInvocation )
2526 . map ( ( command ) => convertPrompt ( command , promptNames ) )
2627
28+ const skillDirs = plugin . skills . map ( ( skill ) => ( {
29+ name : uniquePiSkillName ( normalizePiSkillName ( skill . name ) , usedSkillNames ) ,
30+ sourceDir : skill . sourceDir ,
31+ } ) )
32+
2733 const generatedSkills = plugin . agents . map ( ( agent ) => convertAgent ( agent , usedSkillNames ) )
2834
2935 const extensions = [
@@ -35,25 +41,21 @@ export function convertClaudeToPi(
3541
3642 return {
3743 prompts,
38- skillDirs : plugin . skills . map ( ( skill ) => ( {
39- name : skill . name ,
40- sourceDir : skill . sourceDir ,
41- } ) ) ,
44+ skillDirs,
4245 generatedSkills,
4346 extensions,
4447 mcporterConfig : plugin . mcpServers ? convertMcpToMcporter ( plugin . mcpServers ) : undefined ,
4548 }
4649}
4750
4851function convertPrompt ( command : ClaudeCommand , usedNames : Set < string > ) {
49- const name = uniqueName ( normalizeName ( command . name ) , usedNames )
52+ const name = uniquePiSkillName ( normalizePiSkillName ( command . name ) , usedNames )
5053 const frontmatter : Record < string , unknown > = {
5154 description : command . description ,
5255 "argument-hint" : command . argumentHint ,
5356 }
5457
55- let body = transformContentForPi ( command . body )
56- body = appendCompatibilityNoteIfNeeded ( body )
58+ const body = transformPiBodyContent ( command . body )
5759
5860 return {
5961 name,
@@ -62,7 +64,7 @@ function convertPrompt(command: ClaudeCommand, usedNames: Set<string>) {
6264}
6365
6466function convertAgent ( agent : ClaudeAgent , usedNames : Set < string > ) : PiGeneratedSkill {
65- const name = uniqueName ( normalizeName ( agent . name ) , usedNames )
67+ const name = uniquePiSkillName ( normalizePiSkillName ( agent . name ) , usedNames )
6668 const description = sanitizeDescription (
6769 agent . description ?? `Converted from Claude agent ${ agent . name } ` ,
6870 )
@@ -77,74 +79,19 @@ function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): PiGeneratedSk
7779 sections . push ( `## Capabilities\n${ agent . capabilities . map ( ( capability ) => `- ${ capability } ` ) . join ( "\n" ) } ` )
7880 }
7981
80- const body = [
82+ const body = transformPiBodyContent ( [
8183 ...sections ,
8284 agent . body . trim ( ) . length > 0
8385 ? agent . body . trim ( )
8486 : `Instructions converted from the ${ agent . name } agent.` ,
85- ] . join ( "\n\n" )
87+ ] . join ( "\n\n" ) )
8688
8789 return {
8890 name,
8991 content : formatFrontmatter ( frontmatter , body ) ,
9092 }
9193}
9294
93- function transformContentForPi ( body : string ) : string {
94- let result = body
95-
96- // Task repo-research-analyst(feature_description)
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 skillName = normalizeName ( agentName )
101- const trimmedArgs = args . trim ( ) . replace ( / \s + / g, " " )
102- return `${ prefix } Run subagent with agent=\"${ skillName } \" and task=\"${ trimmedArgs } \".`
103- } )
104-
105- // Claude-specific tool references
106- result = result . replace ( / \b A s k U s e r Q u e s t i o n \b / g, "ask_user_question" )
107- result = result . replace ( / \b T o d o W r i t e \b / g, "file-based todos (todos/ + /skill:file-todos)" )
108- result = result . replace ( / \b T o d o R e a d \b / g, "file-based todos (todos/ + /skill:file-todos)" )
109-
110- // /command-name or /workflows:command-name -> /workflows-command-name
111- const slashCommandPattern = / (?< ! [: \w] ) \/ ( [ a - z ] [ a - z 0 - 9 _ : - ] * ?) (? = [ \s , . " ' ) \] } ` ] | $ ) / gi
112- result = result . replace ( slashCommandPattern , ( match , commandName : string ) => {
113- if ( commandName . includes ( "/" ) ) return match
114- if ( [ "dev" , "tmp" , "etc" , "usr" , "var" , "bin" , "home" ] . includes ( commandName ) ) {
115- return match
116- }
117-
118- if ( commandName . startsWith ( "skill:" ) ) {
119- const skillName = commandName . slice ( "skill:" . length )
120- return `/skill:${ normalizeName ( skillName ) } `
121- }
122-
123- const withoutPrefix = commandName . startsWith ( "prompts:" )
124- ? commandName . slice ( "prompts:" . length )
125- : commandName
126-
127- return `/${ normalizeName ( withoutPrefix ) } `
128- } )
129-
130- return result
131- }
132-
133- function appendCompatibilityNoteIfNeeded ( body : string ) : string {
134- if ( ! / \b m c p \b / i. test ( body ) ) return body
135-
136- const note = [
137- "" ,
138- "## Pi + MCPorter note" ,
139- "For MCP access in Pi, use MCPorter via the generated tools:" ,
140- "- `mcporter_list` to inspect available MCP tools" ,
141- "- `mcporter_call` to invoke a tool" ,
142- "" ,
143- ] . join ( "\n" )
144-
145- return body + note
146- }
147-
14895function convertMcpToMcporter ( servers : Record < string , ClaudeMcpServer > ) : PiMcporterConfig {
14996 const mcpServers : Record < string , PiMcporterServer > = { }
15097
@@ -170,36 +117,10 @@ function convertMcpToMcporter(servers: Record<string, ClaudeMcpServer>): PiMcpor
170117 return { mcpServers }
171118}
172119
173- function normalizeName ( value : string ) : string {
174- const trimmed = value . trim ( )
175- if ( ! trimmed ) return "item"
176- const normalized = trimmed
177- . toLowerCase ( )
178- . replace ( / [ \\ / ] + / g, "-" )
179- . replace ( / [: \s] + / g, "-" )
180- . replace ( / [ ^ a - z 0 - 9 _ - ] + / g, "-" )
181- . replace ( / - + / g, "-" )
182- . replace ( / ^ - + | - + $ / g, "" )
183- return normalized || "item"
184- }
185-
186120function sanitizeDescription ( value : string , maxLength = PI_DESCRIPTION_MAX_LENGTH ) : string {
187121 const normalized = value . replace ( / \s + / g, " " ) . trim ( )
188122 if ( normalized . length <= maxLength ) return normalized
189123 const ellipsis = "..."
190124 return normalized . slice ( 0 , Math . max ( 0 , maxLength - ellipsis . length ) ) . trimEnd ( ) + ellipsis
191125}
192126
193- function uniqueName ( base : string , used : Set < string > ) : string {
194- if ( ! used . has ( base ) ) {
195- used . add ( base )
196- return base
197- }
198- let index = 2
199- while ( used . has ( `${ base } -${ index } ` ) ) {
200- index += 1
201- }
202- const name = `${ base } -${ index } `
203- used . add ( name )
204- return name
205- }
0 commit comments