@@ -103,6 +103,7 @@ const BINARY_LIKE_EXTENSIONS = new Set([
103103const DRAFT_ALLOWED_TOP_LEVEL_DIRS = new Set ( [ 'references' , 'templates' , 'scripts' , 'assets' ] )
104104const DRAFT_CONVERSATION_ID_PATTERN = / ^ [ A - Z a - z 0 - 9 . _ - ] + $ /
105105const DRAFT_ID_PATTERN = / ^ [ A - Z a - z 0 - 9 . _ - ] + $ /
106+ const DRAFT_ACTIVITY_MARKER = '.lastActivity'
106107const DRAFT_INJECTION_PATTERNS = [
107108 / i g n o r e \s + p r e v i o u s \s + i n s t r u c t i o n s / i,
108109 / d i s r e g a r d \s + a l l \s + p r i o r / i,
@@ -493,82 +494,106 @@ export class SkillPresenter implements ISkillPresenter {
493494 const isPinned = pinnedSkills . includes ( metadata . name )
494495
495496 if ( options ?. filePath ?. trim ( ) ) {
496- const resolvedPath = this . resolveSkillRelativePath (
497- metadata . skillRoot ,
498- options . filePath . trim ( )
499- )
500- if ( ! resolvedPath ) {
501- return {
502- success : false ,
503- error : 'Requested skill file is outside the skill root'
497+ try {
498+ const requestedFilePath = options . filePath . trim ( )
499+ const resolvedPath = this . resolveSkillRelativePath ( metadata . skillRoot , requestedFilePath )
500+ if ( ! resolvedPath ) {
501+ return {
502+ success : false ,
503+ error : 'Requested skill file is outside the skill root'
504+ }
504505 }
505- }
506506
507- if ( ! fs . existsSync ( resolvedPath ) ) {
508- return {
509- success : false ,
510- error : `Skill file not found: ${ options . filePath . trim ( ) } `
507+ if ( ! fs . existsSync ( resolvedPath ) ) {
508+ return {
509+ success : false ,
510+ error : `Skill file not found: ${ requestedFilePath } `
511+ }
512+ }
513+
514+ const stats = fs . statSync ( resolvedPath )
515+ if ( ! stats . isFile ( ) ) {
516+ return {
517+ success : false ,
518+ error : 'Requested skill path is not a file'
519+ }
520+ }
521+ if ( stats . size > SKILL_CONFIG . MAX_LINKED_FILE_SIZE ) {
522+ return {
523+ success : false ,
524+ error : 'Requested skill file is too large to load inline'
525+ }
526+ }
527+ if ( this . isBinaryLikeFile ( resolvedPath ) ) {
528+ return {
529+ success : false ,
530+ error : 'Binary skill files cannot be loaded with skill_view'
531+ }
511532 }
512- }
513533
514- const stats = fs . statSync ( resolvedPath )
515- if ( ! stats . isFile ( ) ) {
516534 return {
517- success : false ,
518- error : 'Requested skill path is not a file'
535+ success : true ,
536+ name : metadata . name ,
537+ category : metadata . category ?? null ,
538+ skillRoot : metadata . skillRoot ,
539+ filePath : path . relative ( metadata . skillRoot , resolvedPath ) ,
540+ content : fs . readFileSync ( resolvedPath , 'utf-8' ) ,
541+ platforms : metadata . platforms ,
542+ metadata : metadata . metadata ,
543+ isPinned
519544 }
520- }
521- if ( stats . size > SKILL_CONFIG . MAX_LINKED_FILE_SIZE ) {
545+ } catch ( error ) {
546+ const errorMessage = error instanceof Error ? error . message : String ( error )
547+ console . error ( '[SkillPresenter] Failed to load requested skill file for skill_view:' , {
548+ name : metadata . name ,
549+ filePath : options . filePath . trim ( ) ,
550+ error
551+ } )
522552 return {
523553 success : false ,
524- error : 'Requested skill file is too large to load inline'
554+ error : `Failed to load requested skill file: ${ errorMessage } `
525555 }
526556 }
527- if ( this . isBinaryLikeFile ( resolvedPath ) ) {
557+ }
558+
559+ try {
560+ const stats = fs . statSync ( metadata . path )
561+ if ( stats . size > SKILL_CONFIG . SKILL_FILE_MAX_SIZE ) {
562+ const errorMessage = `[SkillPresenter] Skill file too large: ${ stats . size } bytes (max: ${ SKILL_CONFIG . SKILL_FILE_MAX_SIZE } )`
563+ console . error ( errorMessage )
528564 return {
529565 success : false ,
530- error : 'Binary skill files cannot be loaded with skill_view'
566+ error : errorMessage
531567 }
532568 }
533569
570+ const rawContent = fs . readFileSync ( metadata . path , 'utf-8' )
571+ const { content } = matter ( rawContent )
572+
534573 return {
535574 success : true ,
536575 name : metadata . name ,
537576 category : metadata . category ?? null ,
538577 skillRoot : metadata . skillRoot ,
539- filePath : path . relative ( metadata . skillRoot , resolvedPath ) ,
540- content : fs . readFileSync ( resolvedPath , 'utf-8' ) ,
578+ filePath : null ,
579+ content : this . replacePathVariables ( content , metadata ) ,
541580 platforms : metadata . platforms ,
542581 metadata : metadata . metadata ,
582+ linkedFiles : this . listSkillLinkedFiles ( metadata . skillRoot ) ,
543583 isPinned
544584 }
545- }
546-
547- const stats = fs . statSync ( metadata . path )
548- if ( stats . size > SKILL_CONFIG . SKILL_FILE_MAX_SIZE ) {
549- const errorMessage = `[SkillPresenter] Skill file too large: ${ stats . size } bytes (max: ${ SKILL_CONFIG . SKILL_FILE_MAX_SIZE } )`
550- console . error ( errorMessage )
585+ } catch ( error ) {
586+ const errorMessage = error instanceof Error ? error . message : String ( error )
587+ console . error ( '[SkillPresenter] Failed to load skill_view content:' , {
588+ name : metadata . name ,
589+ path : metadata . path ,
590+ error
591+ } )
551592 return {
552593 success : false ,
553- error : errorMessage
594+ error : `Failed to load skill view: ${ errorMessage } `
554595 }
555596 }
556-
557- const rawContent = fs . readFileSync ( metadata . path , 'utf-8' )
558- const { content } = matter ( rawContent )
559-
560- return {
561- success : true ,
562- name : metadata . name ,
563- category : metadata . category ?? null ,
564- skillRoot : metadata . skillRoot ,
565- filePath : null ,
566- content : this . replacePathVariables ( content , metadata ) ,
567- platforms : metadata . platforms ,
568- metadata : metadata . metadata ,
569- linkedFiles : this . listSkillLinkedFiles ( metadata . skillRoot ) ,
570- isPinned
571- }
572597 }
573598
574599 async manageDraftSkill (
@@ -586,6 +611,7 @@ export class SkillPresenter implements ISkillPresenter {
586611 }
587612 const { draftId, draftPath } = this . createDraftHandle ( conversationId )
588613 this . atomicWriteFile ( path . join ( draftPath , 'SKILL.md' ) , request . content ! )
614+ this . touchDraftActivity ( draftPath )
589615 return { success : true , action, draftId, skillName : parsed . skillName }
590616 }
591617 case 'edit' : {
@@ -613,6 +639,7 @@ export class SkillPresenter implements ISkillPresenter {
613639 return { success : false , action, error : 'Draft not found' }
614640 }
615641 this . atomicWriteFile ( path . join ( draftPath , 'SKILL.md' ) , request . content ! )
642+ this . touchDraftActivity ( draftPath )
616643 return { success : true , action, draftId, skillName : parsed . skillName }
617644 }
618645 case 'write_file' : {
@@ -656,6 +683,7 @@ export class SkillPresenter implements ISkillPresenter {
656683 }
657684 fs . mkdirSync ( path . dirname ( resolvedFilePath ) , { recursive : true } )
658685 this . atomicWriteFile ( resolvedFilePath , request . fileContent )
686+ this . touchDraftActivity ( draftPath )
659687 return {
660688 success : true ,
661689 action,
@@ -695,6 +723,7 @@ export class SkillPresenter implements ISkillPresenter {
695723 return { success : false , action, error : 'Draft file not found' }
696724 }
697725 fs . rmSync ( resolvedFilePath , { force : true } )
726+ this . touchDraftActivity ( draftPath )
698727 return {
699728 success : true ,
700729 action,
@@ -1733,7 +1762,17 @@ export class SkillPresenter implements ISkillPresenter {
17331762 return acc
17341763 }
17351764
1736- const entries = fs . readdirSync ( currentDir , { withFileTypes : true } )
1765+ let entries : fs . Dirent [ ]
1766+ try {
1767+ entries = fs . readdirSync ( currentDir , { withFileTypes : true } )
1768+ } catch ( error ) {
1769+ logger . warn ( '[SkillPresenter] Failed to scan skill directory, skipping subtree' , {
1770+ currentDir,
1771+ error
1772+ } )
1773+ return acc
1774+ }
1775+
17371776 for ( const entry of entries ) {
17381777 if ( entry . isSymbolicLink ( ) ) {
17391778 continue
@@ -1980,6 +2019,22 @@ export class SkillPresenter implements ISkillPresenter {
19802019 return resolvedPath
19812020 }
19822021
2022+ private getDraftActivityMarkerPath ( draftPath : string ) : string {
2023+ return path . join ( draftPath , DRAFT_ACTIVITY_MARKER )
2024+ }
2025+
2026+ private touchDraftActivity ( draftPath : string ) : void {
2027+ fs . writeFileSync ( this . getDraftActivityMarkerPath ( draftPath ) , `${ Date . now ( ) } ` , 'utf-8' )
2028+ }
2029+
2030+ private getDraftLastActivityMs ( draftPath : string ) : number {
2031+ const markerPath = this . getDraftActivityMarkerPath ( draftPath )
2032+ if ( fs . existsSync ( markerPath ) ) {
2033+ return fs . statSync ( markerPath ) . mtimeMs
2034+ }
2035+ return fs . statSync ( draftPath ) . mtimeMs
2036+ }
2037+
19832038 private atomicWriteFile ( targetPath : string , content : string ) : void {
19842039 const tempPath = path . join (
19852040 path . dirname ( targetPath ) ,
@@ -2009,8 +2064,8 @@ export class SkillPresenter implements ISkillPresenter {
20092064 }
20102065
20112066 const draftDir = path . join ( conversationDir , draftEntry . name )
2012- const stats = fs . statSync ( draftDir )
2013- if ( now - stats . mtimeMs > SKILL_CONFIG . DRAFT_RETENTION_MS ) {
2067+ const lastActivityMs = this . getDraftLastActivityMs ( draftDir )
2068+ if ( now - lastActivityMs > SKILL_CONFIG . DRAFT_RETENTION_MS ) {
20142069 fs . rmSync ( draftDir , { recursive : true , force : true } )
20152070 }
20162071 }
0 commit comments