@@ -13,6 +13,7 @@ export interface ObsidianConfig {
1313 vaultPath : string ;
1414 folder : string ;
1515 plan : string ;
16+ filenameFormat ?: string ; // Custom format string, e.g. '{YYYY}-{MM}-{DD} - {title}'
1617}
1718
1819export interface BearConfig {
@@ -104,26 +105,62 @@ export function extractTitle(markdown: string): string {
104105 return 'Plan' ;
105106}
106107
108+ /** Default filename format matching original behavior */
109+ export const DEFAULT_FILENAME_FORMAT = '{title} - {Mon} {D}, {YYYY} {h}-{mm}{ampm}' ;
110+
107111/**
108- * Generate human-readable filename: Title - Mon D, YYYY H-MMam.md
109- * Example: User Authentication - Jan 2, 2026 2-30pm.md
112+ * Generate filename from a format string with variable substitution.
113+ *
114+ * Supported variables:
115+ * {title} - Plan title from first H1 heading
116+ * {YYYY} - 4-digit year
117+ * {MM} - 2-digit month (01-12)
118+ * {DD} - 2-digit day (01-31)
119+ * {Mon} - Abbreviated month name (Jan, Feb, ...)
120+ * {D} - Day without leading zero
121+ * {HH} - 2-digit hour, 24h (00-23)
122+ * {h} - Hour without leading zero, 12h
123+ * {hh} - 2-digit hour, 12h (01-12)
124+ * {mm} - 2-digit minutes (00-59)
125+ * {ss} - 2-digit seconds (00-59)
126+ * {ampm} - am/pm
127+ *
128+ * Default format: '{title} - {Mon} {D}, {YYYY} {h}-{mm}{ampm}'
129+ * Example output: 'User Authentication - Jan 2, 2026 2-30pm.md'
110130 */
111- export function generateFilename ( markdown : string ) : string {
131+ export function generateFilename ( markdown : string , format ?: string ) : string {
112132 const title = extractTitle ( markdown ) ;
113133 const now = new Date ( ) ;
114134
115135 const months = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' ,
116136 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] ;
117- const month = months [ now . getMonth ( ) ] ;
118- const day = now . getDate ( ) ;
119- const year = now . getFullYear ( ) ;
120-
121- let hours = now . getHours ( ) ;
122- const minutes = now . getMinutes ( ) . toString ( ) . padStart ( 2 , '0' ) ;
123- const ampm = hours >= 12 ? 'pm' : 'am' ;
124- hours = hours % 12 || 12 ;
125137
126- return `${ title } - ${ month } ${ day } , ${ year } ${ hours } -${ minutes } ${ ampm } .md` ;
138+ const hour24 = now . getHours ( ) ;
139+ const hour12 = hour24 % 12 || 12 ;
140+ const ampm = hour24 >= 12 ? 'pm' : 'am' ;
141+
142+ const vars : Record < string , string > = {
143+ title,
144+ YYYY : String ( now . getFullYear ( ) ) ,
145+ MM : String ( now . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ,
146+ DD : String ( now . getDate ( ) ) . padStart ( 2 , '0' ) ,
147+ Mon : months [ now . getMonth ( ) ] ,
148+ D : String ( now . getDate ( ) ) ,
149+ HH : String ( hour24 ) . padStart ( 2 , '0' ) ,
150+ h : String ( hour12 ) ,
151+ hh : String ( hour12 ) . padStart ( 2 , '0' ) ,
152+ mm : String ( now . getMinutes ( ) ) . padStart ( 2 , '0' ) ,
153+ ss : String ( now . getSeconds ( ) ) . padStart ( 2 , '0' ) ,
154+ ampm,
155+ } ;
156+
157+ const template = format ?. trim ( ) || DEFAULT_FILENAME_FORMAT ;
158+ const result = template . replace ( / \{ ( \w + ) \} / g, ( match , key ) => vars [ key ] ?? match ) ;
159+
160+ // Sanitize: remove characters invalid in filenames
161+ const sanitized = result . replace ( / [ < > : " / \\ | ? * ] / g, '' ) . replace ( / \s + / g, ' ' ) . trim ( ) ;
162+
163+ return sanitized . endsWith ( '.md' ) ? sanitized : `${ sanitized } .md` ;
127164}
128165
129166// --- Obsidian Integration ---
@@ -208,7 +245,7 @@ export async function saveToObsidian(config: ObsidianConfig): Promise<Integratio
208245 mkdirSync ( targetFolder , { recursive : true } ) ;
209246
210247 // Generate filename and full path
211- const filename = generateFilename ( plan ) ;
248+ const filename = generateFilename ( plan , config . filenameFormat ) ;
212249 const filePath = join ( targetFolder , filename ) ;
213250
214251 // Generate content with frontmatter and backlink
0 commit comments