@@ -50,6 +50,23 @@ import Observation
5050 private let fileWorker = SkillFileWorker ( )
5151 private let importWorker = SkillImportWorker ( )
5252 private let cliWorker = ClawdhubCLIWorker ( )
53+ private let customPathStore : CustomPathStore
54+
55+ init ( customPathStore: CustomPathStore = CustomPathStore ( ) ) {
56+ self . customPathStore = customPathStore
57+ }
58+
59+ var customPaths : [ CustomSkillPath ] {
60+ customPathStore. customPaths
61+ }
62+
63+ func addCustomPath( _ url: URL ) throws {
64+ try customPathStore. addPath ( url)
65+ }
66+
67+ func removeCustomPath( _ path: CustomSkillPath ) {
68+ customPathStore. removePath ( path)
69+ }
5370
5471 var selectedSkill : Skill ? {
5572 skills. first { $0. id == selectedSkillID }
@@ -69,6 +86,8 @@ import Observation
6986 ( platform, platform. rootURL, platform. storageKey)
7087 }
7188 var skills : [ Skill ] = [ ]
89+
90+ // Scan platform paths
7291 for (platform, rootURL, storageKey) in platforms {
7392 let scanned = try await fileWorker. scanSkills ( at: rootURL, storageKey: storageKey)
7493 skills. append ( contentsOf: scanned. map { scannedSkill in
@@ -78,6 +97,7 @@ import Observation
7897 displayName: scannedSkill. displayName,
7998 description: scannedSkill. description,
8099 platform: platform,
100+ customPath: nil ,
81101 folderURL: scannedSkill. folderURL,
82102 skillMarkdownURL: scannedSkill. skillMarkdownURL,
83103 references: scannedSkill. references,
@@ -86,6 +106,32 @@ import Observation
86106 } )
87107 }
88108
109+ // Scan custom paths - auto-discover platform subpaths
110+ let fileManager = FileManager . default
111+ for customPath in customPathStore. customPaths {
112+ for platform in SkillPlatform . allCases {
113+ let platformURL = platform. skillsURL ( in: customPath. url)
114+ guard fileManager. fileExists ( atPath: platformURL. path) else { continue }
115+
116+ let storageKey = " \( customPath. storageKey) - \( platform. storageKey) "
117+ let scanned = try await fileWorker. scanSkills ( at: platformURL, storageKey: storageKey)
118+ skills. append ( contentsOf: scanned. map { scannedSkill in
119+ Skill (
120+ id: scannedSkill. id,
121+ name: scannedSkill. name,
122+ displayName: scannedSkill. displayName,
123+ description: scannedSkill. description,
124+ platform: platform,
125+ customPath: customPath,
126+ folderURL: scannedSkill. folderURL,
127+ skillMarkdownURL: scannedSkill. skillMarkdownURL,
128+ references: scannedSkill. references,
129+ stats: scannedSkill. stats
130+ )
131+ } )
132+ }
133+ }
134+
89135 self . skills = skills. sorted {
90136 $0. displayName. localizedCaseInsensitiveCompare ( $1. displayName) == . orderedAscending
91137 }
@@ -166,6 +212,10 @@ import Observation
166212 }
167213
168214 func isOwnedSkill( _ skill: Skill ) -> Bool {
215+ // Skills from custom paths are always considered "owned"
216+ if skill. customPath != nil {
217+ return true
218+ }
169219 let originURL = skill. folderURL
170220 . appendingPathComponent ( " .clawdhub " )
171221 . appendingPathComponent ( " origin.json " )
@@ -185,7 +235,7 @@ import Observation
185235 }
186236
187237 func installedPlatforms( for slug: String ) -> Set < SkillPlatform > {
188- Set ( skills. filter { $0. name == slug } . map ( \. platform) )
238+ Set ( skills. filter { $0. name == slug } . compactMap ( \. platform) )
189239 }
190240
191241 func groupedLocalSkills( from filteredSkills: [ Skill ] ) -> [ LocalSkillGroup ] {
@@ -205,10 +255,13 @@ import Observation
205255 . compactMap ( { platform in filteredSkills. first ( where: { $0. platform == platform } ) } )
206256 . first ?? filteredSkills. first ?? preferredSelection
207257
258+ // Collect platforms from all skills with the same slug
259+ let installedPlatforms = Set ( allSkillsForSlug. compactMap ( \. platform) )
260+
208261 return LocalSkillGroup (
209262 id: preferredSelection. id,
210263 skill: preferredContent,
211- installedPlatforms: Set ( allSkillsForSlug . map ( \ . platform ) ) ,
264+ installedPlatforms: installedPlatforms ,
212265 deleteIDs: allSkillsForSlug. map ( \. id)
213266 )
214267 }
@@ -217,6 +270,14 @@ import Observation
217270 }
218271 }
219272
273+ func skillsForCustomPath( _ path: CustomSkillPath ) -> [ Skill ] {
274+ skills. filter { $0. customPath? . id == path. id }
275+ }
276+
277+ func platformSkills( ) -> [ Skill ] {
278+ skills. filter { $0. platform != nil }
279+ }
280+
220281 func skillNeedsPublish( _ skill: Skill ) async -> Bool {
221282 do {
222283 let hash = try await fileWorker. computeSkillHash ( for: skill. folderURL)
0 commit comments