@@ -3,11 +3,14 @@ import {
33 type ModelCapabilities ,
44 type OpenCodeSettings ,
55 type ServerProviderModel ,
6+ type ServerProviderSkill ,
67} from "@t3tools/contracts" ;
78import * as Cause from "effect/Cause" ;
89import * as Data from "effect/Data" ;
910import * as DateTime from "effect/DateTime" ;
1011import * as Effect from "effect/Effect" ;
12+ import * as FileSystem from "effect/FileSystem" ;
13+ import * as Path from "effect/Path" ;
1114
1215import { createModelCapabilities } from "@t3tools/shared/model" ;
1316import { compareSemverVersions } from "@t3tools/shared/semver" ;
@@ -217,6 +220,112 @@ function openCodeCapabilitiesForModel(input: {
217220 } ) ;
218221}
219222
223+ const SKILLS_DIR_NAME = ".agents" ;
224+ const SKILLS_SUBDIR_NAME = "skills" ;
225+ const SKILL_FILE_NAME = "SKILL.md" ;
226+
227+ function parseSkillFrontmatter (
228+ content : string ,
229+ ) : { name : string ; description ?: string ; displayName ?: string } | null {
230+ const frontmatterMatch = content . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - / ) ;
231+ if ( ! frontmatterMatch ) {
232+ return null ;
233+ }
234+
235+ const frontmatter = frontmatterMatch [ 1 ] ! ;
236+ const nameMatch = frontmatter . match ( / ^ n a m e : \s * ( .+ ) $ / m) ;
237+ if ( ! nameMatch ) {
238+ return null ;
239+ }
240+
241+ const name = nameMatch [ 1 ] ! . trim ( ) ;
242+ if ( name . length === 0 ) {
243+ return null ;
244+ }
245+
246+ const descMatch = frontmatter . match ( / ^ d e s c r i p t i o n : \s * (?: > \s * \n ( [ \s \S ] * ?) | ( .+ ) ) ? $ / m) ;
247+ let description : string | undefined ;
248+ if ( descMatch ) {
249+ const raw = descMatch [ 1 ] ?? descMatch [ 2 ] ;
250+ if ( raw ) {
251+ description = raw
252+ . split ( "\n" )
253+ . map ( ( line ) => line . replace ( / ^ \s * \| ? \s * / , "" ) )
254+ . join ( " " )
255+ . trim ( ) ;
256+ }
257+ }
258+
259+ const displayNameMatch = frontmatter . match ( / ^ d i s p l a y N a m e : \s * ( .+ ) $ / m) ;
260+ const displayName = displayNameMatch ? displayNameMatch [ 1 ] ! . trim ( ) : undefined ;
261+
262+ const result : { name : string ; description ?: string ; displayName ?: string } = { name } ;
263+ if ( description !== undefined ) {
264+ result . description = description ;
265+ }
266+ if ( displayName !== undefined ) {
267+ result . displayName = displayName ;
268+ }
269+ return result ;
270+ }
271+
272+ function loadOpenCodeSkills (
273+ cwd : string ,
274+ ) : Effect . Effect < ReadonlyArray < ServerProviderSkill > , never , FileSystem . FileSystem | Path . Path > {
275+ return Effect . gen ( function * ( ) {
276+ const fs = yield * FileSystem . FileSystem ;
277+ const path = yield * Path . Path ;
278+
279+ const skillsDir = path . join ( cwd , SKILLS_DIR_NAME , SKILLS_SUBDIR_NAME ) ;
280+ const entriesExit = yield * Effect . exit ( fs . readDirectory ( skillsDir , { recursive : false } ) ) ;
281+
282+ if ( entriesExit . _tag !== "Success" ) {
283+ return [ ] ;
284+ }
285+
286+ const skills : Array < ServerProviderSkill > = [ ] ;
287+
288+ for ( const entryName of entriesExit . value ) {
289+ const fullPath = path . join ( skillsDir , entryName ) ;
290+ const statExit = yield * Effect . exit ( fs . stat ( fullPath ) ) ;
291+
292+ if ( statExit . _tag !== "Success" || statExit . value . type !== "Directory" ) {
293+ continue ;
294+ }
295+
296+ const contentExit = yield * Effect . exit (
297+ fs . readFileString ( path . join ( fullPath , SKILL_FILE_NAME ) ) ,
298+ ) ;
299+
300+ if ( contentExit . _tag !== "Success" ) {
301+ continue ;
302+ }
303+
304+ const parsed = parseSkillFrontmatter ( contentExit . value ) ;
305+ if ( ! parsed ) {
306+ continue ;
307+ }
308+
309+ const shortDescription =
310+ parsed . description && parsed . description . length > 200
311+ ? parsed . description . slice ( 0 , 200 ) + "..."
312+ : parsed . description ;
313+
314+ const skill : ServerProviderSkill = {
315+ name : parsed . name ,
316+ path : fullPath ,
317+ enabled : true ,
318+ ...( parsed . description !== undefined ? { description : parsed . description } : { } ) ,
319+ ...( parsed . displayName !== undefined ? { displayName : parsed . displayName } : { } ) ,
320+ ...( shortDescription !== undefined ? { shortDescription } : { } ) ,
321+ } ;
322+ skills . push ( skill ) ;
323+ }
324+
325+ return skills ;
326+ } ) ;
327+ }
328+
220329function flattenOpenCodeModels ( input : OpenCodeInventory ) : ReadonlyArray < ServerProviderModel > {
221330 const connected = new Set ( input . providerList . connected ) ;
222331 const models : Array < ServerProviderModel > = [ ] ;
@@ -252,7 +361,8 @@ function flattenOpenCodeModels(input: OpenCodeInventory): ReadonlyArray<ServerPr
252361
253362export const makePendingOpenCodeProvider = (
254363 openCodeSettings : OpenCodeSettings ,
255- ) : Effect . Effect < ServerProviderDraft > =>
364+ cwd : string ,
365+ ) : Effect . Effect < ServerProviderDraft , never , FileSystem . FileSystem | Path . Path > =>
256366 Effect . gen ( function * ( ) {
257367 const checkedAt = yield * Effect . map ( DateTime . now , DateTime . formatIso ) ;
258368 const models = providerModelsFromSettings (
@@ -261,13 +371,15 @@ export const makePendingOpenCodeProvider = (
261371 openCodeSettings . customModels ,
262372 DEFAULT_OPENCODE_MODEL_CAPABILITIES ,
263373 ) ;
374+ const skills = yield * loadOpenCodeSkills ( cwd ) ;
264375
265376 if ( ! openCodeSettings . enabled ) {
266377 return buildServerProvider ( {
267378 presentation : OPENCODE_PRESENTATION ,
268379 enabled : false ,
269380 checkedAt,
270381 models,
382+ skills,
271383 probe : {
272384 installed : false ,
273385 version : null ,
@@ -286,6 +398,7 @@ export const makePendingOpenCodeProvider = (
286398 enabled : true ,
287399 checkedAt,
288400 models,
401+ skills,
289402 probe : {
290403 installed : false ,
291404 version : null ,
@@ -300,12 +413,18 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
300413 openCodeSettings : OpenCodeSettings ,
301414 cwd : string ,
302415 environment : NodeJS . ProcessEnv = process . env ,
303- ) : Effect . fn . Return < ServerProviderDraft , never , OpenCodeRuntime > {
416+ ) : Effect . fn . Return <
417+ ServerProviderDraft ,
418+ never ,
419+ OpenCodeRuntime | FileSystem . FileSystem | Path . Path
420+ > {
304421 const openCodeRuntime = yield * OpenCodeRuntime ;
305422 const checkedAt = DateTime . formatIso ( yield * DateTime . now ) ;
306423 const customModels = openCodeSettings . customModels ;
307424 const isExternalServer = openCodeSettings . serverUrl . trim ( ) . length > 0 ;
308425
426+ const skills = yield * loadOpenCodeSkills ( cwd ) ;
427+
309428 const fallback = ( cause : unknown , version : string | null = null ) => {
310429 const failure = formatOpenCodeProbeError ( {
311430 cause,
@@ -322,6 +441,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
322441 customModels ,
323442 DEFAULT_OPENCODE_MODEL_CAPABILITIES ,
324443 ) ,
444+ skills,
325445 probe : {
326446 installed : failure . installed ,
327447 version,
@@ -343,6 +463,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
343463 customModels ,
344464 DEFAULT_OPENCODE_MODEL_CAPABILITIES ,
345465 ) ,
466+ skills,
346467 probe : {
347468 installed : false ,
348469 version : null ,
@@ -394,6 +515,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
394515 customModels ,
395516 DEFAULT_OPENCODE_MODEL_CAPABILITIES ,
396517 ) ,
518+ skills,
397519 probe : {
398520 installed : true ,
399521 version,
@@ -455,6 +577,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
455577 enabled : true ,
456578 checkedAt,
457579 models,
580+ skills,
458581 probe : {
459582 installed : true ,
460583 version,
0 commit comments