@@ -60,7 +60,10 @@ const {
6060
6161const PROJECTS_DIR = path . join ( os . homedir ( ) , '.claude' , 'projects' ) ;
6262const PLANS_DIR = path . join ( os . homedir ( ) , '.claude' , 'plans' ) ;
63+ const COMMANDS_DIR = path . join ( os . homedir ( ) , '.claude' , 'commands' ) ;
64+ const SKILLS_DIR = path . join ( os . homedir ( ) , '.claude' , 'skills' ) ;
6365const CLAUDE_DIR = path . join ( os . homedir ( ) , '.claude' ) ;
66+ const CLAUDE_DIRNAME = path . basename ( CLAUDE_DIR ) ;
6467const STATS_CACHE_PATH = path . join ( CLAUDE_DIR , 'stats-cache.json' ) ;
6568const MAX_BUFFER_SIZE = 256 * 1024 ;
6669
@@ -888,6 +891,220 @@ ipcMain.handle('read-memory', (_event, filePath) => {
888891 }
889892} ) ;
890893
894+ // --- IPC: get-skills ---
895+ function parseFrontmatter ( content ) {
896+ const match = content . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - / ) ;
897+ if ( ! match ) return { } ;
898+ const fm = { } ;
899+ for ( const line of match [ 1 ] . split ( '\n' ) ) {
900+ const idx = line . indexOf ( ':' ) ;
901+ if ( idx > 0 ) fm [ line . slice ( 0 , idx ) . trim ( ) ] = line . slice ( idx + 1 ) . trim ( ) . replace ( / ^ [ " ' ] | [ " ' ] $ / g, '' ) ;
902+ }
903+ return fm ;
904+ }
905+
906+ function readMdFiles ( dir , filter ) {
907+ if ( ! fs . existsSync ( dir ) ) return [ ] ;
908+ return fs . readdirSync ( dir , { withFileTypes : true } )
909+ . filter ( filter || ( d => d . isFile ( ) && d . name . endsWith ( '.md' ) ) )
910+ . map ( d => d . name ) ;
911+ }
912+
913+ ipcMain . handle ( 'get-skills' , ( ) => {
914+ const skills = [ ] ;
915+ try {
916+ // Global commands: ~/.claude/commands/*.md
917+ for ( const file of readMdFiles ( COMMANDS_DIR ) ) {
918+ try {
919+ const filePath = path . join ( COMMANDS_DIR , file ) ;
920+ const stat = fs . statSync ( filePath ) ;
921+ const content = fs . readFileSync ( filePath , 'utf8' ) ;
922+ const fm = parseFrontmatter ( content ) ;
923+ const firstLine = content . replace ( / ^ - - - [ \s \S ] * ?- - - \n ? / , '' ) . split ( '\n' ) . find ( l => l . trim ( ) ) ;
924+ const title = fm . name || fm . description || ( firstLine && firstLine . startsWith ( '# ' ) ? firstLine . slice ( 2 ) . trim ( ) : file . replace ( / \. m d $ / , '' ) ) ;
925+ skills . push ( { filename : file , title, description : fm . description || '' , type : 'command' , scope : 'global' , filePath, modified : stat . mtime . toISOString ( ) } ) ;
926+ } catch { }
927+ }
928+
929+ // Global skills: ~/.claude/skills/*/SKILL.md
930+ if ( fs . existsSync ( SKILLS_DIR ) ) {
931+ for ( const d of fs . readdirSync ( SKILLS_DIR , { withFileTypes : true } ) ) {
932+ if ( ! d . isDirectory ( ) ) continue ;
933+ const filePath = path . join ( SKILLS_DIR , d . name , 'SKILL.md' ) ;
934+ if ( ! fs . existsSync ( filePath ) ) continue ;
935+ try {
936+ const stat = fs . statSync ( filePath ) ;
937+ const content = fs . readFileSync ( filePath , 'utf8' ) ;
938+ const fm = parseFrontmatter ( content ) ;
939+ const title = fm . name || d . name ;
940+ skills . push ( { filename : d . name + '/SKILL.md' , title, description : fm . description || '' , type : 'skill' , scope : 'global' , filePath, modified : stat . mtime . toISOString ( ) } ) ;
941+ } catch { }
942+ }
943+ }
944+
945+ // Per-project commands: {actualProjectDir}/.claude/commands/*.md
946+ if ( fs . existsSync ( PROJECTS_DIR ) ) {
947+ const folders = fs . readdirSync ( PROJECTS_DIR , { withFileTypes : true } )
948+ . filter ( d => d . isDirectory ( ) && d . name !== '.git' )
949+ . map ( d => d . name ) ;
950+ const seen = new Set ( ) ;
951+ for ( const folder of folders ) {
952+ const projectPath = deriveProjectPath ( path . join ( PROJECTS_DIR , folder ) , folder ) ;
953+ if ( ! projectPath || seen . has ( projectPath ) ) continue ;
954+ seen . add ( projectPath ) ;
955+ const shortPath = folderToShortPath ( folder ) ;
956+ const cmdDir = path . join ( projectPath , '.claude' , 'commands' ) ;
957+ for ( const file of readMdFiles ( cmdDir ) ) {
958+ try {
959+ const filePath = path . join ( cmdDir , file ) ;
960+ const stat = fs . statSync ( filePath ) ;
961+ const content = fs . readFileSync ( filePath , 'utf8' ) ;
962+ const fm = parseFrontmatter ( content ) ;
963+ const firstLine = content . replace ( / ^ - - - [ \s \S ] * ?- - - \n ? / , '' ) . split ( '\n' ) . find ( l => l . trim ( ) ) ;
964+ const title = fm . name || fm . description || ( firstLine && firstLine . startsWith ( '# ' ) ? firstLine . slice ( 2 ) . trim ( ) : file . replace ( / \. m d $ / , '' ) ) ;
965+ skills . push ( { filename : file , title, description : fm . description || '' , type : 'command' , scope : shortPath , filePath, modified : stat . mtime . toISOString ( ) } ) ;
966+ } catch { }
967+ }
968+ }
969+ }
970+
971+ skills . sort ( ( a , b ) => new Date ( b . modified ) - new Date ( a . modified ) ) ;
972+
973+ // Index for FTS
974+ try {
975+ deleteSearchType ( 'skill' ) ;
976+ upsertSearchEntries ( skills . map ( s => ( {
977+ id : s . filePath , type : 'skill' , folder : null ,
978+ title : s . title + ' ' + s . type + ' ' + s . scope ,
979+ body : fs . readFileSync ( s . filePath , 'utf8' ) ,
980+ } ) ) ) ;
981+ } catch { }
982+
983+ return skills ;
984+ } catch ( err ) {
985+ console . error ( 'Error reading skills:' , err ) ;
986+ return [ ] ;
987+ }
988+ } ) ;
989+
990+ // --- IPC: read-skill ---
991+ ipcMain . handle ( 'read-skill' , ( _event , filePath ) => {
992+ try {
993+ const resolved = path . resolve ( filePath ) ;
994+ // Allow paths under ~/.claude/ or inside .claude/ of known projects
995+ if ( ! resolved . includes ( path . sep + CLAUDE_DIRNAME + path . sep ) ) return { content : '' , filePath : '' } ;
996+ return { content : fs . readFileSync ( resolved , 'utf8' ) , filePath : resolved } ;
997+ } catch ( err ) {
998+ console . error ( 'Error reading skill:' , err ) ;
999+ return { content : '' , filePath : '' } ;
1000+ }
1001+ } ) ;
1002+
1003+ // --- IPC: get-agents ---
1004+ ipcMain . handle ( 'get-agents' , ( ) => {
1005+ const agents = [ ] ;
1006+ try {
1007+ if ( fs . existsSync ( PROJECTS_DIR ) ) {
1008+ const folders = fs . readdirSync ( PROJECTS_DIR , { withFileTypes : true } )
1009+ . filter ( d => d . isDirectory ( ) && d . name !== '.git' )
1010+ . map ( d => d . name ) ;
1011+ const seen = new Set ( ) ;
1012+ for ( const folder of folders ) {
1013+ const projectPath = deriveProjectPath ( path . join ( PROJECTS_DIR , folder ) , folder ) ;
1014+ if ( ! projectPath || seen . has ( projectPath ) ) continue ;
1015+ seen . add ( projectPath ) ;
1016+ const shortPath = folderToShortPath ( folder ) ;
1017+ const agentDir = path . join ( projectPath , '.claude' , 'agents' ) ;
1018+ for ( const file of readMdFiles ( agentDir ) ) {
1019+ try {
1020+ const filePath = path . join ( agentDir , file ) ;
1021+ const stat = fs . statSync ( filePath ) ;
1022+ const content = fs . readFileSync ( filePath , 'utf8' ) ;
1023+ const fm = parseFrontmatter ( content ) ;
1024+ const firstLine = content . replace ( / ^ - - - [ \s \S ] * ?- - - \n ? / , '' ) . split ( '\n' ) . find ( l => l . trim ( ) ) ;
1025+ const title = fm . name || ( firstLine && firstLine . startsWith ( '# ' ) ? firstLine . slice ( 2 ) . trim ( ) : file . replace ( / \. m d $ / , '' ) ) ;
1026+ agents . push ( { filename : file , title, description : fm . description || '' , model : fm . model || '' , scope : shortPath , filePath, modified : stat . mtime . toISOString ( ) } ) ;
1027+ } catch { }
1028+ }
1029+ }
1030+ }
1031+
1032+ agents . sort ( ( a , b ) => new Date ( b . modified ) - new Date ( a . modified ) ) ;
1033+
1034+ // Index for FTS
1035+ try {
1036+ deleteSearchType ( 'agent' ) ;
1037+ upsertSearchEntries ( agents . map ( a => ( {
1038+ id : a . filePath , type : 'agent' , folder : null ,
1039+ title : a . title + ' ' + a . scope ,
1040+ body : fs . readFileSync ( a . filePath , 'utf8' ) ,
1041+ } ) ) ) ;
1042+ } catch { }
1043+
1044+ return agents ;
1045+ } catch ( err ) {
1046+ console . error ( 'Error reading agents:' , err ) ;
1047+ return [ ] ;
1048+ }
1049+ } ) ;
1050+
1051+ // --- IPC: read-agent ---
1052+ ipcMain . handle ( 'read-agent' , ( _event , filePath ) => {
1053+ try {
1054+ const resolved = path . resolve ( filePath ) ;
1055+ if ( ! resolved . includes ( path . sep + CLAUDE_DIRNAME + path . sep ) ) return '' ;
1056+ return fs . readFileSync ( resolved , 'utf8' ) ;
1057+ } catch ( err ) {
1058+ console . error ( 'Error reading agent:' , err ) ;
1059+ return '' ;
1060+ }
1061+ } ) ;
1062+
1063+ // --- IPC: save-memory ---
1064+ ipcMain . handle ( 'save-memory' , ( _event , filePath , content ) => {
1065+ try {
1066+ const resolved = path . resolve ( filePath ) ;
1067+ if ( ! resolved . startsWith ( CLAUDE_DIR ) ) {
1068+ return { ok : false , error : 'path outside .claude directory' } ;
1069+ }
1070+ fs . writeFileSync ( resolved , content , 'utf8' ) ;
1071+ return { ok : true } ;
1072+ } catch ( err ) {
1073+ console . error ( 'Error saving memory:' , err ) ;
1074+ return { ok : false , error : err . message } ;
1075+ }
1076+ } ) ;
1077+
1078+ // --- IPC: save-skill ---
1079+ ipcMain . handle ( 'save-skill' , ( _event , filePath , content ) => {
1080+ try {
1081+ const resolved = path . resolve ( filePath ) ;
1082+ if ( ! resolved . includes ( path . sep + CLAUDE_DIRNAME + path . sep ) ) {
1083+ return { ok : false , error : 'path outside .claude directory' } ;
1084+ }
1085+ fs . writeFileSync ( resolved , content , 'utf8' ) ;
1086+ return { ok : true } ;
1087+ } catch ( err ) {
1088+ console . error ( 'Error saving skill:' , err ) ;
1089+ return { ok : false , error : err . message } ;
1090+ }
1091+ } ) ;
1092+
1093+ // --- IPC: save-agent ---
1094+ ipcMain . handle ( 'save-agent' , ( _event , filePath , content ) => {
1095+ try {
1096+ const resolved = path . resolve ( filePath ) ;
1097+ if ( ! resolved . includes ( path . sep + CLAUDE_DIRNAME + path . sep ) ) {
1098+ return { ok : false , error : 'path outside .claude directory' } ;
1099+ }
1100+ fs . writeFileSync ( resolved , content , 'utf8' ) ;
1101+ return { ok : true } ;
1102+ } catch ( err ) {
1103+ console . error ( 'Error saving agent:' , err ) ;
1104+ return { ok : false , error : err . message } ;
1105+ }
1106+ } ) ;
1107+
8911108// --- IPC: search ---
8921109ipcMain . handle ( 'search' , ( _event , type , query ) => {
8931110 return searchByType ( type , query , 50 ) ;
0 commit comments