1717 * (claude-plugins-official entries)
1818 * - none — pack has no skill/command listing (RTK, claude-code-router)
1919 *
20- * Returns [{ name, kind , description?, path? }]. kind is one of
20+ * Returns [{ name, type , description?, path? }]. type is one of
2121 * 'skill', 'command', 'agent', 'plugin'.
2222 */
2323
@@ -44,7 +44,7 @@ export type ContentItemKind = "skill" | "command" | "agent" | "plugin";
4444
4545export interface ContentItem {
4646 name : string ;
47- kind : ContentItemKind ;
47+ type : ContentItemKind ;
4848 description ?: string | null ;
4949 path ?: string ;
5050 category ?: string ;
@@ -78,10 +78,10 @@ interface ContentsSpec {
7878}
7979
8080export interface CatalogEntry {
81- pack_id : string ;
82- github_url : string ;
83- contents : ContentsSpec | null ;
84- contents_fetched_at ?: string | null ;
81+ packId : string ;
82+ githubUrl : string ;
83+ contents : Record < string , unknown > | null ;
84+ contentsFetchedAt ?: string | null ;
8585}
8686
8787interface GitHubTreeEntry {
@@ -107,6 +107,31 @@ type CatalogDb = DbClient;
107107
108108// ---------- low-level GitHub helpers ----------
109109
110+ function readString (
111+ value : Record < string , unknown > ,
112+ key : keyof ContentsSpec ,
113+ ) : string | undefined {
114+ const raw = value [ key ] ;
115+ return typeof raw === "string" ? raw : undefined ;
116+ }
117+
118+ function readStringArray (
119+ value : Record < string , unknown > ,
120+ key : keyof ContentsSpec ,
121+ ) : string [ ] {
122+ const raw = value [ key ] ;
123+ return Array . isArray ( raw )
124+ ? raw . filter ( ( item ) : item is string => typeof item === "string" )
125+ : [ ] ;
126+ }
127+
128+ function readKind ( value : Record < string , unknown > ) : ContentItemKind | undefined {
129+ const raw = value . kind ;
130+ return raw === "skill" || raw === "command" || raw === "agent" || raw === "plugin"
131+ ? raw
132+ : undefined ;
133+ }
134+
110135function parseGithubUrl ( url : string | null | undefined ) : ParsedRepo | null {
111136 const m = String ( url || "" ) . match ( / g i t h u b \. c o m [ / : ] ( [ ^ / ] + ) \/ ( [ ^ / ? # . ] + ) / ) ;
112137 return m ? { owner : m [ 1 ] , repo : m [ 2 ] . replace ( / \. g i t $ / , "" ) } : null ;
@@ -226,13 +251,13 @@ async function fetchSkillTree(
226251 `repos/${ owner } /${ repo } /contents/${ encodeURI ( entry . path ) } /${ skillMarker } ` ,
227252 ) ;
228253 if ( ! file || ! file . content ) {
229- skills . push ( { name : entry . name , kind : "skill" , path : entry . path } ) ;
254+ skills . push ( { name : entry . name , type : "skill" , path : entry . path } ) ;
230255 continue ;
231256 }
232257 const meta = parseSkillFrontmatterFromBase64 ( file . content ) ;
233258 skills . push ( {
234259 name : meta . name || entry . name ,
235- kind : "skill" ,
260+ type : "skill" ,
236261 description : meta . description || null ,
237262 path : entry . path ,
238263 } ) ;
@@ -259,7 +284,7 @@ async function fetchFlatMd(
259284 )
260285 . map ( ( e ) => ( {
261286 name : e . name . replace ( / \. m d $ / , "" ) ,
262- kind : kind || "command" ,
287+ type : kind || "command" ,
263288 path : e . path ,
264289 } ) ) ;
265290}
@@ -288,7 +313,7 @@ async function fetchNestedMd(
288313 if ( file . name . toLowerCase ( ) . startsWith ( "readme" ) ) continue ;
289314 items . push ( {
290315 name : file . name . replace ( / \. m d $ / , "" ) ,
291- kind : kind || "agent" ,
316+ type : kind || "agent" ,
292317 category : cat . name ,
293318 path : file . path ,
294319 } ) ;
@@ -327,7 +352,7 @@ async function fetchClaudeMarketplace(
327352 if ( Array . isArray ( parsed . plugins ) ) {
328353 const items : ContentItem [ ] = parsed . plugins . map ( ( p ) => ( {
329354 name : p . name ,
330- kind : "plugin" as const ,
355+ type : "plugin" as const ,
331356 description : p . description || null ,
332357 } ) ) ;
333358 // If a plugins_root is declared, walk each plugin's skills/ dir for a
@@ -416,7 +441,7 @@ async function fetchNestedSkillTree(
416441 if ( skillDir . type !== "dir" ) continue ;
417442 items . push ( {
418443 name : skillDir . name ,
419- kind : "skill" ,
444+ type : "skill" ,
420445 category : teamDir . name ,
421446 path : skillDir . path ,
422447 } ) ;
@@ -431,37 +456,54 @@ async function fetchNestedSkillTree(
431456
432457export async function fetchContents ( entry : CatalogEntry ) : Promise < ContentItem [ ] > {
433458 const contents = entry . contents ;
434- if ( ! contents || ! contents . type ) return [ ] ;
435- const parsed = parseGithubUrl ( entry . github_url ) ;
459+ const type = contents ? readString ( contents , "type" ) : undefined ;
460+ if ( ! contents || ! type ) return [ ] ;
461+ const parsed = parseGithubUrl ( entry . githubUrl ) ;
436462 if ( ! parsed ) return [ ] ;
437463 const { owner, repo } = parsed ;
438464
439- switch ( contents . type ) {
440- case "github-skill-tree" :
441- return fetchSkillTree ( owner , repo , contents . skills_path ! , contents . skill_marker ) ;
442- case "github-multi-skill-tree" :
443- return fetchMultiSkillTree ( owner , repo , contents . skill_paths , contents . skill_marker ) ;
444- case "github-flat-md" :
445- return fetchFlatMd ( owner , repo , contents . md_path ! , contents . kind ) ;
446- case "github-nested-md" :
447- return fetchNestedMd ( owner , repo , contents . root_path ! , contents . kind ) ;
465+ switch ( type ) {
466+ case "github-skill-tree" : {
467+ const skillsPath = readString ( contents , "skills_path" ) ;
468+ if ( ! skillsPath ) return [ ] ;
469+ return fetchSkillTree ( owner , repo , skillsPath , readString ( contents , "skill_marker" ) ) ;
470+ }
471+ case "github-multi-skill-tree" : {
472+ const skillPaths = readStringArray ( contents , "skill_paths" ) ;
473+ if ( skillPaths . length === 0 ) return [ ] ;
474+ return fetchMultiSkillTree ( owner , repo , skillPaths , readString ( contents , "skill_marker" ) ) ;
475+ }
476+ case "github-flat-md" : {
477+ const mdPath = readString ( contents , "md_path" ) ;
478+ if ( ! mdPath ) return [ ] ;
479+ return fetchFlatMd ( owner , repo , mdPath , readKind ( contents ) ) ;
480+ }
481+ case "github-nested-md" : {
482+ const rootPath = readString ( contents , "root_path" ) ;
483+ if ( ! rootPath ) return [ ] ;
484+ return fetchNestedMd ( owner , repo , rootPath , readKind ( contents ) ) ;
485+ }
448486 case "github-nested-skill-tree" :
449- return fetchNestedSkillTree ( owner , repo , contents . match_pattern ) ;
487+ return fetchNestedSkillTree ( owner , repo , readString ( contents , " match_pattern" ) ) ;
450488 case "claude-marketplace" : {
451- const repoFromContents = contents . marketplace_repo
452- ? parseGithubUrl ( `https://github.com/${ contents . marketplace_repo } ` )
489+ const marketplaceRepo = readString ( contents , "marketplace_repo" ) ;
490+ const repoFromContents = marketplaceRepo
491+ ? parseGithubUrl ( `https://github.com/${ marketplaceRepo } ` )
453492 : null ;
454493 const mkO = repoFromContents ? repoFromContents . owner : owner ;
455494 const mkR = repoFromContents ? repoFromContents . repo : repo ;
456- return fetchClaudeMarketplace ( mkO , mkR , contents . plugins_root ) ;
495+ return fetchClaudeMarketplace ( mkO , mkR , readString ( contents , " plugins_root" ) ) ;
457496 }
458497 case "github-claude-plugin" : {
459- const repoFromContents = contents . marketplace_repo
460- ? parseGithubUrl ( `https://github.com/${ contents . marketplace_repo } ` )
498+ const marketplaceRepo = readString ( contents , "marketplace_repo" ) ;
499+ const repoFromContents = marketplaceRepo
500+ ? parseGithubUrl ( `https://github.com/${ marketplaceRepo } ` )
461501 : null ;
462502 const pluginO = repoFromContents ? repoFromContents . owner : owner ;
463503 const pluginR = repoFromContents ? repoFromContents . repo : repo ;
464- return fetchClaudePlugin ( pluginO , pluginR , contents . plugin_path ! ) ;
504+ const pluginPath = readString ( contents , "plugin_path" ) ;
505+ if ( ! pluginPath ) return [ ] ;
506+ return fetchClaudePlugin ( pluginO , pluginR , pluginPath ) ;
465507 }
466508 case "none" :
467509 return [ ] ;
@@ -479,13 +521,13 @@ export async function refreshCatalogContents(
479521 catalogEntry : CatalogEntry ,
480522) : Promise < ContentItem [ ] > {
481523 const items = await fetchContents ( catalogEntry ) ;
482- await applyContentsFetch ( db , { pack_id : catalogEntry . pack_id , items } ) ;
524+ await applyContentsFetch ( db , { pack_id : catalogEntry . packId , items } ) ;
483525 return items ;
484526}
485527
486528export function isContentsFresh ( entry : CatalogEntry ) : boolean {
487- if ( ! entry . contents_fetched_at ) return false ;
529+ if ( ! entry . contentsFetchedAt ) return false ;
488530 return (
489- Date . now ( ) - new Date ( entry . contents_fetched_at ) . getTime ( ) < CONTENTS_TTL_MS
531+ Date . now ( ) - new Date ( entry . contentsFetchedAt ) . getTime ( ) < CONTENTS_TTL_MS
490532 ) ;
491533}
0 commit comments