src/
├── search-note.mjs 108 lines [kn-specific]
├── create-note.mjs 44 lines [kn-specific]
├── get-next-mode.mjs 16 lines [kn-specific]
├── get-reload-for-current-mode.mjs 24 lines [kn-specific]
├── preview-note.mjs 16 lines [kn-specific]
├── delete-note.mjs 49 lines [kn-specific]
├── rg-commands.mjs 57 lines [kn-specific]
├── keepnote/
│ ├── constants.mjs 4 lines [kn-specific - MISPLACED!]
│ └── sync-command.mjs 196 lines [keepnote-specific]
├── config.mjs 51 lines [shared]
├── open-in-editor.mjs 45 lines [shared]
├── dependencies.mjs 55 lines [shared]
└── util.mjs 80 lines [shared]
Purpose: Main FZF orchestrator for kn search
Exports: searchNote(notesPath) - async function
Imports:
fileURLToPathfrom 'node:url'spawnSyncfrom 'node:child_process'spawnAndCapturefrom './util.mjs'fileContentSearchCommand,FIELD_DELIMITER,parseRipgrepSelectionfrom './rg-commands.mjs'FZF_PROMPTSfrom './keepnote/constants.mjs'
Contains:
getGitStatusHeader(notesPath)- 43 lines - git status for FZF headerspawnFzf(notesPath)- 32 lines - FZF configuration and spawningsearchNote(notesPath)- 19 lines - main function, parses selection
Used by: kn.mjs line 8
Purpose: Ripgrep command builders and output parser Exports:
FIELD_DELIMITER- constant '//'fileContentSearchCommand(notesPath)- builds rg command for content searchfileNameSearchCommand(notesPath)- builds rg command for filename searchparseRipgrepSelection(selection, notesPath)- parses FZF selection
Imports: path from 'node:path'
Contains:
FIELD_DELIMITER- 1 lineWINDOWS_RESERVED_GLOBS- 7 lines - Windows-specific exclusionsFILE_CONTENT_SEARCH_ARGS- 10 lines - rg args for content searchFILE_NAME_SEARCH_ARGS- 6 lines - rg args for filename searchfileContentSearchCommand()- 3 lines - command builderfileNameSearchCommand()- 3 lines - command builderparseRipgrepSelection()- 20 lines - parser logic for both modes
Used by:
search-note.mjsget-next-mode.mjsget-reload-for-current-mode.mjs
Purpose: FZF prompt definitions
Exports: FZF_PROMPTS object with CONTENT and FILES prompts
Contains:
FZF_PROMPTS.CONTENT = 'Content> 'FZF_PROMPTS.FILES = 'Files> '
Used by:
search-note.mjsline 9get-next-mode.mjsline 4get-reload-for-current-mode.mjsline 4
PROBLEM: This is kn-specific but in keepnote/ directory!
Purpose: Create new note with date-prefixed filename
Exports: createNote(title, notesPath) - default export
Imports:
fsfrom 'node:fs'pathfrom 'node:path'filenamifyfrom 'filenamify'
Contains:
getDateString()- 6 lines - formats YYYY-MM-DDslugifyTitleToFilename(title)- 13 lines - sanitizes title to filenamecreateNote(title, notesPath)- 9 lines - main function
Used by: kn.mjs line 9
get-next-mode.mjs (16 lines)
- Executable script spawned by FZF on Tab key
- Reads
FZF_PROMPTenv var to determine current mode - Outputs FZF transform command to toggle between CONTENT ↔ FILES
- Imports from
rg-commands.mjsandconstants.mjs - Used in
search-note.mjsline 57 (file path only)
get-reload-for-current-mode.mjs (24 lines)
- Executable script spawned by FZF after delete
- Preserves current mode (CONTENT or FILES) during reload
- Outputs FZF reload command with appropriate rg command
- Imports from
rg-commands.mjsandconstants.mjs - Used in
search-note.mjsline 60 (file path only)
preview-note.mjs (16 lines)
- Executable script spawned by FZF for preview window
- Takes filename and line number as arguments
- Spawns
batwith syntax highlighting - No imports (uses node:child_process)
- Used in
search-note.mjsline 58 (file path only)
delete-note.mjs (49 lines)
- Executable script spawned by FZF on Ctrl+D
- Prompts user for deletion confirmation
- Deletes file if confirmed
- No imports from project files
- Used in
search-note.mjsline 59 (file path only)
Purpose: Git sync functionality for keepnote command
Exports: syncNotes(notesPath) - default async export
Imports:
spawnSyncfrom 'node:child_process'readlinefrom 'node:readline'
Contains: Complete git sync workflow (status, commit, push)
Used by: keepnote.mjs line 8
ACTION: No changes needed - stays in place
Exports: spawnAndCapture, checkExecutables, log
Used by: search-note, dependencies, keepnote/sync-command
Exports: getOrCreateNotesPath, getEditorExecutableName, getOrCreateConfigFilePath
Used by: kn.mjs, keepnote.mjs, open-in-editor
Exports: openInEditor({ filepath, lineNumber })
Used by: kn.mjs, keepnote.mjs
Exports: displayDependencyStatus, displayAndExitIfAnyDependencyMissing
Used by: kn.mjs, keepnote.mjs
ACTION: All stay in place at src/ level
src/
├── kn/
│ ├── search-command.mjs ~160 lines (consolidated)
│ ├── create-command.mjs 44 lines (renamed)
│ └── search-scripts/
│ ├── get-next-mode.mjs 16 lines (moved + updated imports)
│ ├── get-reload.mjs 24 lines (renamed + moved + updated imports)
│ ├── preview.mjs 16 lines (renamed + moved)
│ └── delete.mjs 49 lines (renamed + moved)
│
├── keepnote/
│ └── sync-command.mjs 196 lines (no changes)
│
├── config.mjs 51 lines (no changes)
├── open-in-editor.mjs 45 lines (no changes)
├── dependencies.mjs 55 lines (no changes)
└── util.mjs 80 lines (no changes)
Consolidate from:
src/search-note.mjs(108 lines)src/rg-commands.mjs(57 lines)src/keepnote/constants.mjs(4 lines)
Sections (use comment dividers):
// ============================================================================
// Mode Configuration
// ============================================================================
export const MODE_NAMES = {
CONTENT: 'CONTENT',
FILES: 'FILES'
}
export const MODES = {
[MODE_NAMES.CONTENT]: { prompt: 'Content> ' },
[MODE_NAMES.FILES]: { prompt: 'Files> ' }
}
export const DEFAULT_MODE = MODE_NAMES.CONTENT // ← Easy to change! Use MODE_NAMES constant
// ============================================================================
// Ripgrep Commands
// ============================================================================
export const FIELD_DELIMITER = '//'
// ... WINDOWS_RESERVED_GLOBS, args arrays, command builders
// ============================================================================
// Ripgrep Parser
// ============================================================================
export function parseRipgrepSelection(selection, notesPath) { ... }
// ============================================================================
// Git Status Header
// ============================================================================
function getGitStatusHeader(notesPath) { ... }
// ============================================================================
// FZF Orchestration
// ============================================================================
function spawnFzf(notesPath) { ... }
export default async function searchNote(notesPath) { ... }Exports:
MODE_NAMES(constants for mode keys - for scripts)MODES(mode configuration - for scripts)DEFAULT_MODE(default mode constant - for scripts)FIELD_DELIMITER(for FZF config)fileContentSearchCommand(notesPath)(for scripts)fileNameSearchCommand(notesPath)(for scripts)parseRipgrepSelection(selection, notesPath)(for scripts)searchNote(notesPath)(default - for kn.mjs)
Imports:
- Standard:
fileURLToPath,spawnSync,path - From project:
spawnAndCapturefrom '../../util.mjs'
Changes:
- File location only
- Update import in
kn.mjsline 9
get-next-mode.mjs → search-scripts/get-next-mode.mjs
- Update imports:
- FROM:
'./rg-commands.mjs'and'./keepnote/constants.mjs' - TO:
'../search-command.mjs'
- FROM:
- Import:
{ MODE_NAMES, MODES, fileContentSearchCommand, fileNameSearchCommand } - Update logic to use
MODE_NAMESandMODESinstead ofFZF_PROMPTS - Example:
if (process.env.FZF_PROMPT === MODES[MODE_NAMES.CONTENT].prompt)
get-reload-for-current-mode.mjs → search-scripts/get-reload.mjs
- Rename file (shorter name)
- Update imports same as above
- Update logic to use
MODE_NAMESandMODESinstead ofFZF_PROMPTS
preview-note.mjs → search-scripts/preview.mjs
- Rename file (shorter name)
- No import changes needed (uses only node builtins)
delete-note.mjs → search-scripts/delete.mjs
- Rename file (shorter name)
- No import changes needed
Update file path references in spawnFzf():
// OLD:
const toggleScriptPath = fileURLToPath(new URL('./get-next-mode.mjs', import.meta.url))
const previewScriptPath = fileURLToPath(new URL('./preview-note.mjs', import.meta.url))
const deleteScriptPath = fileURLToPath(new URL('./delete-note.mjs', import.meta.url))
const reloadScriptPath = fileURLToPath(new URL('./get-reload-for-current-mode.mjs', import.meta.url))
// NEW:
const toggleScriptPath = fileURLToPath(new URL('./search-scripts/get-next-mode.mjs', import.meta.url))
const previewScriptPath = fileURLToPath(new URL('./search-scripts/preview.mjs', import.meta.url))
const deleteScriptPath = fileURLToPath(new URL('./search-scripts/delete.mjs', import.meta.url))
const reloadScriptPath = fileURLToPath(new URL('./search-scripts/get-reload.mjs', import.meta.url))// OLD:
import searchNote from './src/search-note.mjs'
import createNote from './src/create-note.mjs'
// NEW:
import searchNote from './src/kn/search-command.mjs'
import createNote from './src/kn/create-command.mjs'src/search-note.mjssrc/create-note.mjssrc/rg-commands.mjssrc/get-next-mode.mjssrc/get-reload-for-current-mode.mjssrc/preview-note.mjssrc/delete-note.mjssrc/keepnote/constants.mjs
The FZF scripts use import.meta.url to resolve their own locations. When moved:
search-command.mjsis atsrc/kn/search-command.mjs- Scripts are at
src/kn/search-scripts/*.mjs - Use
new URL('./search-scripts/...', import.meta.url)for relative paths
Scripts move from src/ to src/kn/search-scripts/:
- OLD:
import from './rg-commands.mjs'(sibling) - NEW:
import from '../search-command.mjs'(parent) - Shared utilities:
'../../util.mjs'(grandparent)
Replace FZF_PROMPTS with MODE_NAMES and MODES:
// OLD:
FZF_PROMPTS.CONTENT // "Content> "
FZF_PROMPTS.FILES // "Files> "
// NEW (use constants, not strings):
MODES[MODE_NAMES.CONTENT].prompt // "Content> "
MODES[MODE_NAMES.FILES].prompt // "Files> "
// WRONG - don't use magic strings:
MODES['CONTENT'].prompt // ❌ No!
MODES.CONTENT.prompt // ❌ No!
// RIGHT - use constants:
MODES[MODE_NAMES.CONTENT].prompt // ✓ Yes!In search-command.mjs, simply change:
// Use constant, not string!
export const DEFAULT_MODE = MODE_NAMES.FILES // Changed from MODE_NAMES.CONTENTThen use MODES[DEFAULT_MODE].prompt in FZF spawn configuration.
Important: Always use MODE_NAMES.CONTENT or MODE_NAMES.FILES, never 'CONTENT' or 'FILES' strings.
After migration:
- ✓
kn(no args) - should open FZF search in FILES mode (new default) - ✓
kn some title- should create note and open in editor - ✓ Tab key in FZF - should toggle CONTENT ↔ FILES
- ✓ Ctrl+D in FZF - should delete file and refresh
- ✓ Preview in FZF - should show file with bat
- ✓ Select in FZF - should open in editor at correct line
- ✓
keepnote sync- should still work (no changes) - ✓ All imports resolve correctly
- ✓ No broken file paths
Lines of code impact:
- Before: 274 lines across 8 kn-related files
- After: ~160 lines in 1 main file + 4 script files (105 lines)
- Net reduction: ~9 lines (but much clearer organization)
Key benefits:
- Feature-oriented structure (search vs create)
- No cross-dependencies between kn and keepnote
- Single source of truth for mode configuration
- Easy to change default mode (one line)
- Easier to understand and maintain
Risk level: Low
- All shared utilities unchanged
- Keepnote command unchanged
- Only moving and consolidating kn-specific code
- Clear migration path with specific file operations