na 1.2.86 → 1.2.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/.cursor/commands/changelog.md +4 -0
 - data/.rubocop_todo.yml +30 -17
 - data/2025-10-29-one-more-na-update.md +142 -0
 - data/CHANGELOG.md +97 -1
 - data/Gemfile +8 -1
 - data/Gemfile.lock +40 -1
 - data/README.md +192 -2
 - data/Rakefile +78 -78
 - data/bin/commands/add.rb +31 -1
 - data/bin/commands/changes.rb +1 -0
 - data/bin/commands/complete.rb +11 -0
 - data/bin/commands/find.rb +71 -1
 - data/bin/commands/next.rb +100 -2
 - data/bin/commands/plugin.rb +75 -0
 - data/bin/commands/tagged.rb +153 -57
 - data/bin/commands/update.rb +90 -5
 - data/bin/na +7 -0
 - data/lib/na/action.rb +39 -3
 - data/lib/na/actions.rb +136 -6
 - data/lib/na/next_action.rb +180 -31
 - data/lib/na/plugins.rb +419 -0
 - data/lib/na/string.rb +15 -6
 - data/lib/na/theme.rb +1 -0
 - data/lib/na/types.rb +190 -0
 - data/lib/na/version.rb +1 -1
 - data/lib/na.rb +2 -0
 - data/na/Test.todo.markdown +32 -0
 - data/na/test.md +21 -0
 - data/na.gemspec +1 -0
 - data/plugins.md +38 -0
 - data/src/_README.md +153 -1
 - metadata +23 -1
 
    
        data/lib/na/string.rb
    CHANGED
    
    | 
         @@ -189,22 +189,24 @@ class ::String 
     | 
|
| 
       189 
189 
     | 
    
         
             
              # @param indent [Integer] Number of spaces to indent each line
         
     | 
| 
       190 
190 
     | 
    
         
             
              # @return [String] Wrapped string
         
     | 
| 
       191 
191 
     | 
    
         
             
              def wrap(width, indent)
         
     | 
| 
       192 
     | 
    
         
            -
                return  
     | 
| 
      
 192 
     | 
    
         
            +
                return to_s if width.nil? || width <= 0
         
     | 
| 
       193 
193 
     | 
    
         | 
| 
       194 
194 
     | 
    
         
             
                output = []
         
     | 
| 
       195 
195 
     | 
    
         
             
                line = []
         
     | 
| 
       196 
     | 
    
         
            -
                length  
     | 
| 
      
 196 
     | 
    
         
            +
                # Track visible length of current line (exclude the separating space before first word)
         
     | 
| 
      
 197 
     | 
    
         
            +
                length = -1
         
     | 
| 
       197 
198 
     | 
    
         
             
                text = gsub(/(@\S+)\((.*?)\)/) { "#{Regexp.last_match(1)}(#{Regexp.last_match(2).gsub(/ /, '†')})" }
         
     | 
| 
       198 
199 
     | 
    
         | 
| 
       199 
200 
     | 
    
         
             
                text.split.each do |word|
         
     | 
| 
       200 
201 
     | 
    
         
             
                  uncolored = NA::Color.uncolor(word)
         
     | 
| 
       201 
     | 
    
         
            -
                   
     | 
| 
      
 202 
     | 
    
         
            +
                  candidate = length + 1 + uncolored.length
         
     | 
| 
      
 203 
     | 
    
         
            +
                  if candidate <= width
         
     | 
| 
       202 
204 
     | 
    
         
             
                    line << word
         
     | 
| 
       203 
     | 
    
         
            -
                    length  
     | 
| 
      
 205 
     | 
    
         
            +
                    length = candidate
         
     | 
| 
       204 
206 
     | 
    
         
             
                  else
         
     | 
| 
       205 
207 
     | 
    
         
             
                    output << line.join(' ')
         
     | 
| 
       206 
208 
     | 
    
         
             
                    line = [word]
         
     | 
| 
       207 
     | 
    
         
            -
                    length = uncolored.length 
     | 
| 
      
 209 
     | 
    
         
            +
                    length = uncolored.length
         
     | 
| 
       208 
210 
     | 
    
         
             
                  end
         
     | 
| 
       209 
211 
     | 
    
         
             
                end
         
     | 
| 
       210 
212 
     | 
    
         
             
                output << line.join(' ')
         
     | 
| 
         @@ -310,7 +312,14 @@ class ::String 
     | 
|
| 
       310 
312 
     | 
    
         
             
                  m = Regexp.last_match
         
     | 
| 
       311 
313 
     | 
    
         
             
                  t = m['tag']
         
     | 
| 
       312 
314 
     | 
    
         
             
                  d = m['date']
         
     | 
| 
       313 
     | 
    
         
            -
                   
     | 
| 
      
 315 
     | 
    
         
            +
                  # Determine whether to bias toward future or past parsing
         
     | 
| 
      
 316 
     | 
    
         
            +
                  # Non-done tags usually bias to future, except explicit past phrases like "ago", "yesterday", or "last ..."
         
     | 
| 
      
 317 
     | 
    
         
            +
                  explicit_past = d =~ /(\bago\b|yesterday|\blast\b)/i
         
     | 
| 
      
 318 
     | 
    
         
            +
                  future = if t =~ /^(done|complete)/
         
     | 
| 
      
 319 
     | 
    
         
            +
                             false
         
     | 
| 
      
 320 
     | 
    
         
            +
                           else
         
     | 
| 
      
 321 
     | 
    
         
            +
                             explicit_past ? false : true
         
     | 
| 
      
 322 
     | 
    
         
            +
                           end
         
     | 
| 
       314 
323 
     | 
    
         
             
                  parsed_date = d =~ iso_rx ? Time.parse(d) : d.chronify(guess: :begin, future: future)
         
     | 
| 
       315 
324 
     | 
    
         
             
                  parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})"
         
     | 
| 
       316 
325 
     | 
    
         
             
                end
         
     | 
    
        data/lib/na/theme.rb
    CHANGED
    
    
    
        data/lib/na/types.rb
    ADDED
    
    | 
         @@ -0,0 +1,190 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'na/string'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module NA
         
     | 
| 
      
 6 
     | 
    
         
            +
              # Custom types for GLI
         
     | 
| 
      
 7 
     | 
    
         
            +
              # Provides natural language date/time and duration parsing
         
     | 
| 
      
 8 
     | 
    
         
            +
              # Uses chronify gem for parsing
         
     | 
| 
      
 9 
     | 
    
         
            +
              module Types
         
     | 
| 
      
 10 
     | 
    
         
            +
                module_function
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                # Normalize shorthand relative durations to phrases Chronic can parse.
         
     | 
| 
      
 13 
     | 
    
         
            +
                # Examples:
         
     | 
| 
      
 14 
     | 
    
         
            +
                #  - "30m ago"    => "30 minutes ago"
         
     | 
| 
      
 15 
     | 
    
         
            +
                #  - "-30m"       => "30 minutes ago"
         
     | 
| 
      
 16 
     | 
    
         
            +
                #  - "2h30m"      => "2 hours 30 minutes ago" (when default_past)
         
     | 
| 
      
 17 
     | 
    
         
            +
                #  - "2h 30m ago" => "2 hours 30 minutes ago"
         
     | 
| 
      
 18 
     | 
    
         
            +
                #  - "2:30 ago"   => "2 hours 30 minutes ago"
         
     | 
| 
      
 19 
     | 
    
         
            +
                #  - "-2:30"      => "2 hours 30 minutes ago"
         
     | 
| 
      
 20 
     | 
    
         
            +
                # Accepts d,h,m units; hours:minutes pattern; optional leading '-'; optional 'ago'.
         
     | 
| 
      
 21 
     | 
    
         
            +
                # @param value [String] the duration string to normalize
         
     | 
| 
      
 22 
     | 
    
         
            +
                # @param default_past [Boolean] whether to default to past tense
         
     | 
| 
      
 23 
     | 
    
         
            +
                # @return [String] the normalized duration string
         
     | 
| 
      
 24 
     | 
    
         
            +
                def normalize_relative_duration(value, default_past: false)
         
     | 
| 
      
 25 
     | 
    
         
            +
                  return value if value.nil?
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                  s = value.to_s.strip
         
     | 
| 
      
 28 
     | 
    
         
            +
                  return s if s.empty?
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                  has_ago = s =~ /\bago\b/i
         
     | 
| 
      
 31 
     | 
    
         
            +
                  negative = s.start_with?('-')
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  text = s.sub(/^[-+]/, '')
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                  # hours:minutes pattern (e.g., 2:30, 02:30)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  if (m = text.match(/^(\d{1,2}):(\d{1,2})(?:\s*ago)?$/i))
         
     | 
| 
      
 37 
     | 
    
         
            +
                    hours = m[1].to_i
         
     | 
| 
      
 38 
     | 
    
         
            +
                    minutes = m[2].to_i
         
     | 
| 
      
 39 
     | 
    
         
            +
                    parts = []
         
     | 
| 
      
 40 
     | 
    
         
            +
                    parts << "#{hours} hours" if hours.positive?
         
     | 
| 
      
 41 
     | 
    
         
            +
                    parts << "#{minutes} minutes" if minutes.positive?
         
     | 
| 
      
 42 
     | 
    
         
            +
                    return "#{parts.join(' ')} ago"
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  # Compound d/h/m (order independent, allow spaces): e.g., 1d2h30m, 2h 30m, 30m
         
     | 
| 
      
 46 
     | 
    
         
            +
                  days = hours = minutes = 0
         
     | 
| 
      
 47 
     | 
    
         
            +
                  found = false
         
     | 
| 
      
 48 
     | 
    
         
            +
                  if (dm = text.match(/(?:(\d+)\s*d)/i))
         
     | 
| 
      
 49 
     | 
    
         
            +
                    days = dm[1].to_i
         
     | 
| 
      
 50 
     | 
    
         
            +
                    found = true
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
                  if (hm = text.match(/(?:(\d+)\s*h)/i))
         
     | 
| 
      
 53 
     | 
    
         
            +
                    hours = hm[1].to_i
         
     | 
| 
      
 54 
     | 
    
         
            +
                    found = true
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
                  if (mm = text.match(/(?:(\d+)\s*m)/i))
         
     | 
| 
      
 57 
     | 
    
         
            +
                    minutes = mm[1].to_i
         
     | 
| 
      
 58 
     | 
    
         
            +
                    found = true
         
     | 
| 
      
 59 
     | 
    
         
            +
                  end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  if found
         
     | 
| 
      
 62 
     | 
    
         
            +
                    parts = []
         
     | 
| 
      
 63 
     | 
    
         
            +
                    parts << "#{days} days" if days.positive?
         
     | 
| 
      
 64 
     | 
    
         
            +
                    parts << "#{hours} hours" if hours.positive?
         
     | 
| 
      
 65 
     | 
    
         
            +
                    parts << "#{minutes} minutes" if minutes.positive?
         
     | 
| 
      
 66 
     | 
    
         
            +
                    # Determine if we should make it past-tense
         
     | 
| 
      
 67 
     | 
    
         
            +
                    return "#{parts.join(' ')} ago" if negative || has_ago || default_past
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                    return parts.join(' ')
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                  end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                  # Fall through: not a shorthand we handle
         
     | 
| 
      
 74 
     | 
    
         
            +
                  s
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                # Parse a natural-language/iso date string for a start time
         
     | 
| 
      
 78 
     | 
    
         
            +
                # @param value [String] the date string to parse
         
     | 
| 
      
 79 
     | 
    
         
            +
                # @return [Time] the parsed date, or nil if parsing fails
         
     | 
| 
      
 80 
     | 
    
         
            +
                def parse_date_begin(value)
         
     | 
| 
      
 81 
     | 
    
         
            +
                  return nil if value.nil? || value.to_s.strip.empty?
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                  # Prefer explicit ISO first (only if the value looks ISO-like)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  iso_rx = /\A\d{4}-\d{2}-\d{2}(?:[ T]\d{1,2}:\d{2}(?::\d{2})?)?\z/
         
     | 
| 
      
 85 
     | 
    
         
            +
                  if value.to_s.strip =~ iso_rx
         
     | 
| 
      
 86 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 87 
     | 
    
         
            +
                      return Time.parse(value)
         
     | 
| 
      
 88 
     | 
    
         
            +
                    rescue StandardError
         
     | 
| 
      
 89 
     | 
    
         
            +
                      # fall through to chronify
         
     | 
| 
      
 90 
     | 
    
         
            +
                    end
         
     | 
| 
      
 91 
     | 
    
         
            +
                  end
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                  # Fallback to chronify with guess begin
         
     | 
| 
      
 94 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 95 
     | 
    
         
            +
                    # Normalize shorthand (e.g., 2h30m, -2:30, 30m ago)
         
     | 
| 
      
 96 
     | 
    
         
            +
                    txt = normalize_relative_duration(value.to_s, default_past: true)
         
     | 
| 
      
 97 
     | 
    
         
            +
                    # Bias to past for expressions like "ago", "yesterday", or "last ..."
         
     | 
| 
      
 98 
     | 
    
         
            +
                    future = txt !~ /(\bago\b|yesterday|\blast\b)/i
         
     | 
| 
      
 99 
     | 
    
         
            +
                    result = txt.chronify(guess: :begin, future: future)
         
     | 
| 
      
 100 
     | 
    
         
            +
                    NA.notify("Parsed '#{value}' as #{result}", debug: true) if result
         
     | 
| 
      
 101 
     | 
    
         
            +
                    result
         
     | 
| 
      
 102 
     | 
    
         
            +
                  rescue StandardError
         
     | 
| 
      
 103 
     | 
    
         
            +
                    nil
         
     | 
| 
      
 104 
     | 
    
         
            +
                  end
         
     | 
| 
      
 105 
     | 
    
         
            +
                end
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                # Parse a natural-language/iso date string for an end time
         
     | 
| 
      
 108 
     | 
    
         
            +
                # @param value [String] the date string to parse
         
     | 
| 
      
 109 
     | 
    
         
            +
                # @return [Time] the parsed date, or nil if parsing fails
         
     | 
| 
      
 110 
     | 
    
         
            +
                def parse_date_end(value)
         
     | 
| 
      
 111 
     | 
    
         
            +
                  return nil if value.nil? || value.to_s.strip.empty?
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                  # Prefer explicit ISO first (only if the value looks ISO-like)
         
     | 
| 
      
 114 
     | 
    
         
            +
                  iso_rx = /\A\d{4}-\d{2}-\d{2}(?:[ T]\d{1,2}:\d{2}(?::\d{2})?)?\z/
         
     | 
| 
      
 115 
     | 
    
         
            +
                  if value.to_s.strip =~ iso_rx
         
     | 
| 
      
 116 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 117 
     | 
    
         
            +
                      return Time.parse(value)
         
     | 
| 
      
 118 
     | 
    
         
            +
                    rescue StandardError
         
     | 
| 
      
 119 
     | 
    
         
            +
                      # fall through to chronify
         
     | 
| 
      
 120 
     | 
    
         
            +
                    end
         
     | 
| 
      
 121 
     | 
    
         
            +
                  end
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
                  # Fallback to chronify with guess end
         
     | 
| 
      
 124 
     | 
    
         
            +
                  value.to_s.chronify(guess: :end, future: false)
         
     | 
| 
      
 125 
     | 
    
         
            +
                end
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                # Convert duration expressions to seconds
         
     | 
| 
      
 128 
     | 
    
         
            +
                # Supports: "90" (minutes), "45m", "2h", "1d2h30m", with optional leading '-' or trailing 'ago'
         
     | 
| 
      
 129 
     | 
    
         
            +
                # Also supports "2:30", "2:30 ago", and word forms like "2 hours 30 minutes (ago)"
         
     | 
| 
      
 130 
     | 
    
         
            +
                # @param value [String] the duration string to parse
         
     | 
| 
      
 131 
     | 
    
         
            +
                # @return [Integer] the duration in seconds, or nil if parsing fails
         
     | 
| 
      
 132 
     | 
    
         
            +
                def parse_duration_seconds(value)
         
     | 
| 
      
 133 
     | 
    
         
            +
                  return nil if value.nil?
         
     | 
| 
      
 134 
     | 
    
         
            +
             
     | 
| 
      
 135 
     | 
    
         
            +
                  s = value.to_s.strip
         
     | 
| 
      
 136 
     | 
    
         
            +
                  return nil if s.empty?
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
                  # Strip leading sign and optional 'ago'
         
     | 
| 
      
 139 
     | 
    
         
            +
                  s = s.sub(/^[-+]/, '')
         
     | 
| 
      
 140 
     | 
    
         
            +
                  s = s.sub(/\bago\b/i, '').strip
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
                  # H:MM pattern
         
     | 
| 
      
 143 
     | 
    
         
            +
                  m = s.match(/^(\d{1,2}):(\d{1,2})$/)
         
     | 
| 
      
 144 
     | 
    
         
            +
                  if m
         
     | 
| 
      
 145 
     | 
    
         
            +
                    hours = m[1].to_i
         
     | 
| 
      
 146 
     | 
    
         
            +
                    minutes = m[2].to_i
         
     | 
| 
      
 147 
     | 
    
         
            +
                    return (hours * 3600) + (minutes * 60)
         
     | 
| 
      
 148 
     | 
    
         
            +
                  end
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                  # d/h/m compact with letters, order independent (e.g., 1d2h30m, 2h 30m, 30m)
         
     | 
| 
      
 151 
     | 
    
         
            +
                  m = s.match(/^(?:(?<day>\d+)\s*d)?\s*(?:(?<hour>\d+)\s*h)?\s*(?:(?<min>\d+)\s*m)?$/i)
         
     | 
| 
      
 152 
     | 
    
         
            +
                  if m && !m[0].strip.empty? && (m['day'] || m['hour'] || m['min'])
         
     | 
| 
      
 153 
     | 
    
         
            +
                    return [[m['day'], 86_400], [m['hour'], 3600], [m['min'], 60]].map { |q, mult| q ? q.to_i * mult : 0 }.sum
         
     | 
| 
      
 154 
     | 
    
         
            +
                  end
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
                  # Word forms: e.g., "2 hours 30 minutes", "1 day 2 hours", etc.
         
     | 
| 
      
 157 
     | 
    
         
            +
                  days = 0
         
     | 
| 
      
 158 
     | 
    
         
            +
                  hours = 0
         
     | 
| 
      
 159 
     | 
    
         
            +
                  minutes = 0
         
     | 
| 
      
 160 
     | 
    
         
            +
                  found_word = false
         
     | 
| 
      
 161 
     | 
    
         
            +
                  if (dm = s.match(/(\d+)\s*(?:day|days)\b/i))
         
     | 
| 
      
 162 
     | 
    
         
            +
                    days = dm[1].to_i
         
     | 
| 
      
 163 
     | 
    
         
            +
                    found_word = true
         
     | 
| 
      
 164 
     | 
    
         
            +
                  end
         
     | 
| 
      
 165 
     | 
    
         
            +
                  if (hm = s.match(/(\d+)\s*(?:hour|hours|hr|hrs)\b/i))
         
     | 
| 
      
 166 
     | 
    
         
            +
                    hours = hm[1].to_i
         
     | 
| 
      
 167 
     | 
    
         
            +
                    found_word = true
         
     | 
| 
      
 168 
     | 
    
         
            +
                  end
         
     | 
| 
      
 169 
     | 
    
         
            +
                  if (mm = s.match(/(\d+)\s*(?:minute|minutes|min|mins)\b/i))
         
     | 
| 
      
 170 
     | 
    
         
            +
                    minutes = mm[1].to_i
         
     | 
| 
      
 171 
     | 
    
         
            +
                    found_word = true
         
     | 
| 
      
 172 
     | 
    
         
            +
                  end
         
     | 
| 
      
 173 
     | 
    
         
            +
                  return (days * 86_400) + (hours * 3600) + (minutes * 60) if found_word
         
     | 
| 
      
 174 
     | 
    
         
            +
             
     | 
| 
      
 175 
     | 
    
         
            +
                  # Plain number => minutes
         
     | 
| 
      
 176 
     | 
    
         
            +
                  return s.to_i * 60 if s =~ /^\d+$/
         
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
                  # Last resort: try chronify two points and take delta
         
     | 
| 
      
 179 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 180 
     | 
    
         
            +
                    start = Time.now
         
     | 
| 
      
 181 
     | 
    
         
            +
                    finish = s.chronify(context: 'now', guess: :end, future: false)
         
     | 
| 
      
 182 
     | 
    
         
            +
                    return (finish - start).abs.to_i if finish
         
     | 
| 
      
 183 
     | 
    
         
            +
                  rescue StandardError
         
     | 
| 
      
 184 
     | 
    
         
            +
                    # ignore
         
     | 
| 
      
 185 
     | 
    
         
            +
                  end
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
                  nil
         
     | 
| 
      
 188 
     | 
    
         
            +
                end
         
     | 
| 
      
 189 
     | 
    
         
            +
              end
         
     | 
| 
      
 190 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/na/version.rb
    CHANGED
    
    
    
        data/lib/na.rb
    CHANGED
    
    
| 
         @@ -0,0 +1,32 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            ---
         
     | 
| 
      
 2 
     | 
    
         
            +
            comment: 2023-09-03
         
     | 
| 
      
 3 
     | 
    
         
            +
            keywords: 
         
     | 
| 
      
 4 
     | 
    
         
            +
            ---
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            Project3:
         
     | 
| 
      
 7 
     | 
    
         
            +
            Project0:
         
     | 
| 
      
 8 
     | 
    
         
            +
            	- This is another task @na
         
     | 
| 
      
 9 
     | 
    
         
            +
            	- How about this one? @na
         
     | 
| 
      
 10 
     | 
    
         
            +
            	Subproject:
         
     | 
| 
      
 11 
     | 
    
         
            +
            		- Bollocks @na
         
     | 
| 
      
 12 
     | 
    
         
            +
            		Subsub:
         
     | 
| 
      
 13 
     | 
    
         
            +
            			- Hey, I think it's all working @na
         
     | 
| 
      
 14 
     | 
    
         
            +
            			- Is this at the end? @na
         
     | 
| 
      
 15 
     | 
    
         
            +
            	- This better work @na
         
     | 
| 
      
 16 
     | 
    
         
            +
            2023-09-08:
         
     | 
| 
      
 17 
     | 
    
         
            +
            	Project2:
         
     | 
| 
      
 18 
     | 
    
         
            +
            		- new_task @na
         
     | 
| 
      
 19 
     | 
    
         
            +
            		- new_task @na
         
     | 
| 
      
 20 
     | 
    
         
            +
            		- test task @na
         
     | 
| 
      
 21 
     | 
    
         
            +
            	Project0:
         
     | 
| 
      
 22 
     | 
    
         
            +
            		- other task @na
         
     | 
| 
      
 23 
     | 
    
         
            +
            		- other task @na
         
     | 
| 
      
 24 
     | 
    
         
            +
            		- There, that's better @na
         
     | 
| 
      
 25 
     | 
    
         
            +
            		Subproject:
         
     | 
| 
      
 26 
     | 
    
         
            +
            			- new_task 2 @na
         
     | 
| 
      
 27 
     | 
    
         
            +
            			- new_task @na
         
     | 
| 
      
 28 
     | 
    
         
            +
            			- new_task 2 @na
         
     | 
| 
      
 29 
     | 
    
         
            +
            	Project1:
         
     | 
| 
      
 30 
     | 
    
         
            +
            		- Test4
         
     | 
| 
      
 31 
     | 
    
         
            +
            		- Test5
         
     | 
| 
      
 32 
     | 
    
         
            +
            		- Test6
         
     | 
    
        data/na/test.md
    ADDED
    
    | 
         @@ -0,0 +1,21 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            ---
         
     | 
| 
      
 2 
     | 
    
         
            +
            comment: 2023-09-03
         
     | 
| 
      
 3 
     | 
    
         
            +
            keywords:
         
     | 
| 
      
 4 
     | 
    
         
            +
            ---
         
     | 
| 
      
 5 
     | 
    
         
            +
            Other New Project:
         
     | 
| 
      
 6 
     | 
    
         
            +
            	- testing @na @butter
         
     | 
| 
      
 7 
     | 
    
         
            +
            Brand New Project:
         
     | 
| 
      
 8 
     | 
    
         
            +
            	- testing @na
         
     | 
| 
      
 9 
     | 
    
         
            +
            		A multi line (multiline) note
         
     | 
| 
      
 10 
     | 
    
         
            +
            		with a line break
         
     | 
| 
      
 11 
     | 
    
         
            +
            	- testing @na
         
     | 
| 
      
 12 
     | 
    
         
            +
            Project0:
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            - Test1
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            - Test2
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            Project1:
         
     | 
| 
      
 19 
     | 
    
         
            +
            - Test4
         
     | 
| 
      
 20 
     | 
    
         
            +
            - Test5
         
     | 
| 
      
 21 
     | 
    
         
            +
            - Test6
         
     | 
    
        data/na.gemspec
    CHANGED
    
    | 
         @@ -24,6 +24,7 @@ spec = Gem::Specification.new do |s| 
     | 
|
| 
       24 
24 
     | 
    
         
             
              s.add_development_dependency('minitest', '~> 5.14')
         
     | 
| 
       25 
25 
     | 
    
         
             
              s.add_development_dependency('rdoc', '~> 4.3')
         
     | 
| 
       26 
26 
     | 
    
         
             
              s.add_runtime_dependency('chronic', '~> 0.10', '>= 0.10.2')
         
     | 
| 
      
 27 
     | 
    
         
            +
              s.add_runtime_dependency('csv', '>= 3.2')
         
     | 
| 
       27 
28 
     | 
    
         
             
              s.add_runtime_dependency('git', '~> 3.0.0')
         
     | 
| 
       28 
29 
     | 
    
         
             
              s.add_runtime_dependency('gli','~> 2.21.0')
         
     | 
| 
       29 
30 
     | 
    
         
             
              s.add_runtime_dependency('mdless', '~> 1.0', '>= 1.0.32')
         
     | 
    
        data/plugins.md
    ADDED
    
    | 
         @@ -0,0 +1,38 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            I would like to add a plugin architecture to na_gem. It should allow the user to add plugins to ~/.local/share/na/plugins/. These plugins can be any shell script (with a shebang). They can be run with `na plugin NAME`, which accepts the plugin filename with or without an extension, and with or without spaces (so that `plugin AddFoo` will run `Add Foo.sh` if found, but the user can also use `plugin "Add Foo"`).
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            A plugin will be a shell script that takes input on STDIN. The input should be an action as a JSON object, with the file path, line number, action text, note, and array of tags/values (`tags: [{ name: "done", value: "2025-10-29 03:00"}, { name: "na", value: ""}]`). That should be the default. 
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            The `plugin` command should accept a `--input TYPE` flag that accepts `json`, `yaml` or `text`. The YAML should be the same as the JSON (but as YAML), and the text should just be the file_path:line_number, action text, and note, split with "||" (newlines in the note replaced with \n, and filename and line number are combined with : not the divider), with no colorization. One action per line. The "||" in `--input text` should also be a flag `--divider "STRING"` that defaults to "||", but allows the user to specify a different string to split the parts on. 
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            The plugin will need to return output (on STDOUT) in the same format as the input (yaml, json, or text with specified divider), unless `--output FORMAT` is specified with a different type. The `plugin` command will execute the script for every command passed to it, and update the actions based on the returned output.
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            The `plugin` command should accept all the same filter flags as `finish` or other actions that update commands. 
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
            For the `update` command, it should accept a `--plugin NAME` flag, and if it's using interactive menus, a list of plugin names (basename minus extension) should be added to the list of available operations.
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            Also add a `--plugin NAME`, `--input TYPE`, and `--output TYPE` flag to all search and display commands (next, grep, tagged, etc.). That way the user can filter output with any command and run the result through the plugin.
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            In lieu of the `--input` and `--output` commands, the plugin itself can have a comment block after the shebang with `key: value` pairs. When reading a plugin, check for a comment block with `input: JSON` `output: YAML` (case insensitive). The user can also define a `name` or `title` (interchangeable) in this block, which will be used instead of the base name if provided. We need to ignore leading characters when scanning for this comment block (e.g. # or //). The block can have blank lines before it. The only keys we read are input, output, and name/title. Parsing stops at the first blank line or after all three keys are populated. Other keys might exist, like `author` or `description`, which should be ignored.
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            The plugins shouldn't need to be executable, the hashbang should be read and used to execute the script.
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
            When `na` runs, it should check for the existence of the `plugins` directory, creating it if it's missing, and adding a `~/.local/share/na/plugsin/README.md` file with plugin instructions if one doesn't exist. Any `.md` or `.bak` file in the plugins directory should be ignored. In fact, let's have a helper validate the files in the directory by checking for a shebang and ignoring if none exists, and also ignoring any '.bak' or '.md' files.
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            Have NA also create 2 sample plugins in the `~/.local/share/na/plugins` folder when creating it (do not create plugins if the folder already exists). Have the sample plugins be a Python script and a Bash script. The sample plugins should just do something benign like add a tag with a dynamic value to the passed actions. In the README.md note that the user can delete the sample plugins. Give the sample plugins names "Add Foo.py" and "Add Bar.sh" and have them add @foo and @bar, respectively.
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
            ### Summary ###
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
            - plugins are script files in ~/.local/share/na/plugins
         
     | 
| 
      
 26 
     | 
    
         
            +
            	- plugins require a shebang, which is used to execute them
         
     | 
| 
      
 27 
     | 
    
         
            +
            	- plugin base names (without extension) becomes the command name (spaces are handled)
         
     | 
| 
      
 28 
     | 
    
         
            +
            	- Ignore 
         
     | 
| 
      
 29 
     | 
    
         
            +
            - `plugin` subcommand
         
     | 
| 
      
 30 
     | 
    
         
            +
            	- accepts plugin name as argument
         
     | 
| 
      
 31 
     | 
    
         
            +
            	- has a `--input TYPE` flag that determines the input type (yaml, json, or text)
         
     | 
| 
      
 32 
     | 
    
         
            +
            	- has a `--output TYPE` (yaml, json, or text)
         
     | 
| 
      
 33 
     | 
    
         
            +
            	- has a `--divider` flag that determines the divider when `--input text` is used
         
     | 
| 
      
 34 
     | 
    
         
            +
            - `update` subcommand
         
     | 
| 
      
 35 
     | 
    
         
            +
            	- accepts a `--plugin NAME` flag
         
     | 
| 
      
 36 
     | 
    
         
            +
            	- Adds plugin names to interactive menu when no action is specified
         
     | 
| 
      
 37 
     | 
    
         
            +
            - main script parses the output of the plugin, stripping whitespace and reading it as YAML, JSON, or text split on the divider (based on `--output` and defaulting to the value of `--input`), then updates each action in the result. Line numbers should be passed on both input and output and used to update the specific actions.
         
     | 
| 
      
 38 
     | 
    
         
            +
            - Generate README and scripts
         
     | 
    
        data/src/_README.md
    CHANGED
    
    | 
         @@ -9,7 +9,7 @@ 
     | 
|
| 
       9 
9 
     | 
    
         
             
            _If you're one of the rare people like me who find this useful, feel free to
         
     | 
| 
       10 
10 
     | 
    
         
             
            [buy me some coffee][donate]._
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
       12 
     | 
    
         
            -
            The current version of `na` is <!--VER-->1.2. 
     | 
| 
      
 12 
     | 
    
         
            +
            The current version of `na` is <!--VER-->1.2.87<!--END VER-->.
         
     | 
| 
       13 
13 
     | 
    
         | 
| 
       14 
14 
     | 
    
         
             
            `na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder.
         
     | 
| 
       15 
15 
     | 
    
         | 
| 
         @@ -235,6 +235,49 @@ See the help output for a list of all available actions. 
     | 
|
| 
       235 
235 
     | 
    
         
             
            @cli(bundle exec bin/na help update)
         
     | 
| 
       236 
236 
     | 
    
         
             
            ```
         
     | 
| 
       237 
237 
     | 
    
         | 
| 
      
 238 
     | 
    
         
            +
            #### Time tracking
         
     | 
| 
      
 239 
     | 
    
         
            +
             
     | 
| 
      
 240 
     | 
    
         
            +
            `na` supports tracking elapsed time between a start and finish for actions using `@started(YYYY-MM-DD HH:MM)` and `@done(YYYY-MM-DD HH:MM)` tags. Durations are not stored; they are calculated on the fly from these tags.
         
     | 
| 
      
 241 
     | 
    
         
            +
             
     | 
| 
      
 242 
     | 
    
         
            +
            - Add/Finish/Update flags:
         
     | 
| 
      
 243 
     | 
    
         
            +
              - `--started TIME` set a start time when creating or finishing an item
         
     | 
| 
      
 244 
     | 
    
         
            +
              - `--end TIME` (alias `--finished`) set a done time
         
     | 
| 
      
 245 
     | 
    
         
            +
              - `--duration DURATION` backfill start time from the provided end time
         
     | 
| 
      
 246 
     | 
    
         
            +
              - All flags accept natural language (via Chronic) and shorthand: `30m ago`, `-2h`, `2h30m`, `2:30 ago`, `yesterday 5pm`
         
     | 
| 
      
 247 
     | 
    
         
            +
             
     | 
| 
      
 248 
     | 
    
         
            +
            Examples:
         
     | 
| 
      
 249 
     | 
    
         
            +
             
     | 
| 
      
 250 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 251 
     | 
    
         
            +
            na add --started "30 minutes ago" "Investigate bug"
         
     | 
| 
      
 252 
     | 
    
         
            +
            na complete --finished now --duration 2h30m "Investigate bug"
         
     | 
| 
      
 253 
     | 
    
         
            +
            na update --started "yesterday 3pm" --end "yesterday 5:15pm" "Investigate bug"
         
     | 
| 
      
 254 
     | 
    
         
            +
            ```
         
     | 
| 
      
 255 
     | 
    
         
            +
             
     | 
| 
      
 256 
     | 
    
         
            +
            - Display flags (next/tagged):
         
     | 
| 
      
 257 
     | 
    
         
            +
              - `--times` show per‑action durations and a grand total (implies `--done`)
         
     | 
| 
      
 258 
     | 
    
         
            +
              - `--human` format durations as human‑readable text instead of `DD:HH:MM:SS`
         
     | 
| 
      
 259 
     | 
    
         
            +
              - `--only_timed` show only actions that have both `@started` and `@done` (implies `--times --done`)
         
     | 
| 
      
 260 
     | 
    
         
            +
              - `--only_times` output only the totals section (no action lines; implies `--times --done`)
         
     | 
| 
      
 261 
     | 
    
         
            +
              - `--json_times` output a JSON object with timed items, per‑tag totals, and overall total (implies `--times --done`)
         
     | 
| 
      
 262 
     | 
    
         
            +
             
     | 
| 
      
 263 
     | 
    
         
            +
            Example outputs:
         
     | 
| 
      
 264 
     | 
    
         
            +
             
     | 
| 
      
 265 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 266 
     | 
    
         
            +
            # Per‑action durations appended and totals table
         
     | 
| 
      
 267 
     | 
    
         
            +
            na next --times --human
         
     | 
| 
      
 268 
     | 
    
         
            +
             
     | 
| 
      
 269 
     | 
    
         
            +
            # Only totals table (Markdown), no action lines
         
     | 
| 
      
 270 
     | 
    
         
            +
            na tagged "tag*=bug" --only_times
         
     | 
| 
      
 271 
     | 
    
         
            +
             
     | 
| 
      
 272 
     | 
    
         
            +
            # JSON for scripting
         
     | 
| 
      
 273 
     | 
    
         
            +
            na next --json_times > times.json
         
     | 
| 
      
 274 
     | 
    
         
            +
            ```
         
     | 
| 
      
 275 
     | 
    
         
            +
             
     | 
| 
      
 276 
     | 
    
         
            +
            Notes:
         
     | 
| 
      
 277 
     | 
    
         
            +
             
     | 
| 
      
 278 
     | 
    
         
            +
            - Any newly added or edited action text is scanned for natural‑language values in `@started(...)`/`@done(...)` and normalized to `YYYY‑MM‑DD HH:MM`.
         
     | 
| 
      
 279 
     | 
    
         
            +
            - The color of durations in output is configurable via the theme key `duration` (defaults to `{y}`).
         
     | 
| 
      
 280 
     | 
    
         
            +
             
     | 
| 
       238 
281 
     | 
    
         
             
            ##### changelog
         
     | 
| 
       239 
282 
     | 
    
         | 
| 
       240 
283 
     | 
    
         
             
            View recent changes with `na changelog` or `na changes`.
         
     | 
| 
         @@ -332,6 +375,115 @@ If you're using a single global file, you'll need `--cwd_as` to be `tag` or `pro 
     | 
|
| 
       332 
375 
     | 
    
         | 
| 
       333 
376 
     | 
    
         
             
            After installing a hook, you'll need to close your terminal and start a new session to initialize the new commands.
         
     | 
| 
       334 
377 
     | 
    
         | 
| 
      
 378 
     | 
    
         
            +
            ### Plugins
         
     | 
| 
      
 379 
     | 
    
         
            +
             
     | 
| 
      
 380 
     | 
    
         
            +
            NA supports a plugin system that allows you to run external scripts to transform or process actions. Plugins are stored in `~/.local/share/na/plugins` and can be written in any language with a shebang.
         
     | 
| 
      
 381 
     | 
    
         
            +
             
     | 
| 
      
 382 
     | 
    
         
            +
            #### Getting Started
         
     | 
| 
      
 383 
     | 
    
         
            +
             
     | 
| 
      
 384 
     | 
    
         
            +
            The first time NA runs, it will create the plugins directory with a README and two sample plugins:
         
     | 
| 
      
 385 
     | 
    
         
            +
            - `Add Foo.py` - Adds a `@foo` tag with a timestamp
         
     | 
| 
      
 386 
     | 
    
         
            +
            - `Add Bar.sh` - Adds a `@bar` tag
         
     | 
| 
      
 387 
     | 
    
         
            +
             
     | 
| 
      
 388 
     | 
    
         
            +
            You can delete or modify these sample plugins as needed.
         
     | 
| 
      
 389 
     | 
    
         
            +
             
     | 
| 
      
 390 
     | 
    
         
            +
            #### Running Plugins
         
     | 
| 
      
 391 
     | 
    
         
            +
             
     | 
| 
      
 392 
     | 
    
         
            +
            Run a plugin with:
         
     | 
| 
      
 393 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 394 
     | 
    
         
            +
            na plugin PLUGIN_NAME
         
     | 
| 
      
 395 
     | 
    
         
            +
            ```
         
     | 
| 
      
 396 
     | 
    
         
            +
             
     | 
| 
      
 397 
     | 
    
         
            +
            Or use plugins through the `update` command's interactive menu, or pipe actions through plugins on display commands:
         
     | 
| 
      
 398 
     | 
    
         
            +
             
     | 
| 
      
 399 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 400 
     | 
    
         
            +
            na update --plugin PLUGIN_NAME           # Run plugin on selected actions
         
     | 
| 
      
 401 
     | 
    
         
            +
            na next --plugin PLUGIN_NAME             # Transform output only (no file writes)
         
     | 
| 
      
 402 
     | 
    
         
            +
            na tagged bug --plugin PLUGIN_NAME       # Filter and transform
         
     | 
| 
      
 403 
     | 
    
         
            +
            na find "search term" --plugin PLUGIN_NAME
         
     | 
| 
      
 404 
     | 
    
         
            +
            ```
         
     | 
| 
      
 405 
     | 
    
         
            +
             
     | 
| 
      
 406 
     | 
    
         
            +
            #### Plugin Metadata
         
     | 
| 
      
 407 
     | 
    
         
            +
             
     | 
| 
      
 408 
     | 
    
         
            +
            Plugins can specify their behavior in a metadata block after the shebang:
         
     | 
| 
      
 409 
     | 
    
         
            +
             
     | 
| 
      
 410 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 411 
     | 
    
         
            +
            #!/usr/bin/env python3
         
     | 
| 
      
 412 
     | 
    
         
            +
            # name: My Plugin
         
     | 
| 
      
 413 
     | 
    
         
            +
            # input: json
         
     | 
| 
      
 414 
     | 
    
         
            +
            # output: json
         
     | 
| 
      
 415 
     | 
    
         
            +
            ```
         
     | 
| 
      
 416 
     | 
    
         
            +
             
     | 
| 
      
 417 
     | 
    
         
            +
            Available metadata keys (case-insensitive):
         
     | 
| 
      
 418 
     | 
    
         
            +
            - `input`: Input format (`json`, `yaml`, `csv`, `text`)
         
     | 
| 
      
 419 
     | 
    
         
            +
            - `output`: Output format
         
     | 
| 
      
 420 
     | 
    
         
            +
            - `name` or `title`: Display name (defaults to filename)
         
     | 
| 
      
 421 
     | 
    
         
            +
             
     | 
| 
      
 422 
     | 
    
         
            +
            #### Input/Output Formats
         
     | 
| 
      
 423 
     | 
    
         
            +
             
     | 
| 
      
 424 
     | 
    
         
            +
            Plugins accept and return action data. Use `--input` and `--output` flags to override metadata:
         
     | 
| 
      
 425 
     | 
    
         
            +
             
     | 
| 
      
 426 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 427 
     | 
    
         
            +
            na plugin MY_PLUGIN --input text --output json --divider "||"
         
     | 
| 
      
 428 
     | 
    
         
            +
            ```
         
     | 
| 
      
 429 
     | 
    
         
            +
             
     | 
| 
      
 430 
     | 
    
         
            +
            **JSON/YAML Schema:**
         
     | 
| 
      
 431 
     | 
    
         
            +
            ```json
         
     | 
| 
      
 432 
     | 
    
         
            +
            [
         
     | 
| 
      
 433 
     | 
    
         
            +
              {
         
     | 
| 
      
 434 
     | 
    
         
            +
                "file_path": "todo.taskpaper",
         
     | 
| 
      
 435 
     | 
    
         
            +
                "line": 15,
         
     | 
| 
      
 436 
     | 
    
         
            +
                "parents": ["Project", "Subproject"],
         
     | 
| 
      
 437 
     | 
    
         
            +
                "text": "- Action text @tag(value)",
         
     | 
| 
      
 438 
     | 
    
         
            +
                "note": "Note content",
         
     | 
| 
      
 439 
     | 
    
         
            +
                "tags": [
         
     | 
| 
      
 440 
     | 
    
         
            +
                  { "name": "tag", "value": "value" }
         
     | 
| 
      
 441 
     | 
    
         
            +
                ]
         
     | 
| 
      
 442 
     | 
    
         
            +
              }
         
     | 
| 
      
 443 
     | 
    
         
            +
            ]
         
     | 
| 
      
 444 
     | 
    
         
            +
            ```
         
     | 
| 
      
 445 
     | 
    
         
            +
             
     | 
| 
      
 446 
     | 
    
         
            +
            **Text Format:**
         
     | 
| 
      
 447 
     | 
    
         
            +
            ```
         
     | 
| 
      
 448 
     | 
    
         
            +
            ACTION||ARGS||file_path:line||parents||text||note||tags
         
     | 
| 
      
 449 
     | 
    
         
            +
            ```
         
     | 
| 
      
 450 
     | 
    
         
            +
             
     | 
| 
      
 451 
     | 
    
         
            +
            Default divider is `||` (configurable with `--divider`).
         
     | 
| 
      
 452 
     | 
    
         
            +
            - `parents`: `Parent>Child>Leaf`
         
     | 
| 
      
 453 
     | 
    
         
            +
            - `tags`: `name(value);name;other(value)`
         
     | 
| 
      
 454 
     | 
    
         
            +
             
     | 
| 
      
 455 
     | 
    
         
            +
            If the first token isn’t a known action, it’s treated as `file_path:line` and the action defaults to UPDATE.
         
     | 
| 
      
 456 
     | 
    
         
            +
             
     | 
| 
      
 457 
     | 
    
         
            +
            #### Actions
         
     | 
| 
      
 458 
     | 
    
         
            +
             
     | 
| 
      
 459 
     | 
    
         
            +
            Plugins may return an optional ACTION with arguments. Supported (case-insensitive):
         
     | 
| 
      
 460 
     | 
    
         
            +
            - UPDATE (default; replace text/note/tags/parents)
         
     | 
| 
      
 461 
     | 
    
         
            +
            - DELETE
         
     | 
| 
      
 462 
     | 
    
         
            +
            - COMPLETE/FINISH
         
     | 
| 
      
 463 
     | 
    
         
            +
            - RESTORE/UNFINISH
         
     | 
| 
      
 464 
     | 
    
         
            +
            - ARCHIVE
         
     | 
| 
      
 465 
     | 
    
         
            +
            - ADD_TAG (args: one or more tags)
         
     | 
| 
      
 466 
     | 
    
         
            +
            - DELETE_TAG/REMOVE_TAG (args: one or more tags)
         
     | 
| 
      
 467 
     | 
    
         
            +
            - MOVE (args: target project path)
         
     | 
| 
      
 468 
     | 
    
         
            +
             
     | 
| 
      
 469 
     | 
    
         
            +
            #### Plugin Behavior
         
     | 
| 
      
 470 
     | 
    
         
            +
             
     | 
| 
      
 471 
     | 
    
         
            +
            **On `update` or `plugin` command:**
         
     | 
| 
      
 472 
     | 
    
         
            +
            - Plugins can modify text, notes, tags, and parents
         
     | 
| 
      
 473 
     | 
    
         
            +
            - Changing `parents` will move the action to the new project location
         
     | 
| 
      
 474 
     | 
    
         
            +
            - `file_path` and `line` cannot be changed
         
     | 
| 
      
 475 
     | 
    
         
            +
             
     | 
| 
      
 476 
     | 
    
         
            +
            **On display commands (`next`, `tagged`, `find`):**
         
     | 
| 
      
 477 
     | 
    
         
            +
            - Plugins only transform STDOUT (no file writes)
         
     | 
| 
      
 478 
     | 
    
         
            +
            - Use returned text/note/tags/parents for rendering
         
     | 
| 
      
 479 
     | 
    
         
            +
            - Parent changes affect display but not file structure
         
     | 
| 
      
 480 
     | 
    
         
            +
             
     | 
| 
      
 481 
     | 
    
         
            +
            #### Override Formats
         
     | 
| 
      
 482 
     | 
    
         
            +
             
     | 
| 
      
 483 
     | 
    
         
            +
            You can override plugin defaults with flags on any command that supports `--plugin`:
         
     | 
| 
      
 484 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 485 
     | 
    
         
            +
            na next --plugin FOO --input csv --output text
         
     | 
| 
      
 486 
     | 
    
         
            +
            ```
         
     | 
| 
       335 
487 
     | 
    
         | 
| 
       336 
488 
     | 
    
         
             
            [fzf]: https://github.com/junegunn/fzf
         
     | 
| 
       337 
489 
     | 
    
         
             
            [gum]: https://github.com/charmbracelet/gum
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: na
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 1.2. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 1.2.88
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Brett Terpstra
         
     | 
| 
         @@ -57,6 +57,20 @@ dependencies: 
     | 
|
| 
       57 
57 
     | 
    
         
             
                - - ">="
         
     | 
| 
       58 
58 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       59 
59 
     | 
    
         
             
                    version: 0.10.2
         
     | 
| 
      
 60 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 61 
     | 
    
         
            +
              name: csv
         
     | 
| 
      
 62 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 63 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 64 
     | 
    
         
            +
                - - ">="
         
     | 
| 
      
 65 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 66 
     | 
    
         
            +
                    version: '3.2'
         
     | 
| 
      
 67 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 68 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 69 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 70 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 71 
     | 
    
         
            +
                - - ">="
         
     | 
| 
      
 72 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 73 
     | 
    
         
            +
                    version: '3.2'
         
     | 
| 
       60 
74 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       61 
75 
     | 
    
         
             
              name: git
         
     | 
| 
       62 
76 
     | 
    
         
             
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
         @@ -245,10 +259,12 @@ extra_rdoc_files: 
     | 
|
| 
       245 
259 
     | 
    
         
             
            - README.md
         
     | 
| 
       246 
260 
     | 
    
         
             
            - na.rdoc
         
     | 
| 
       247 
261 
     | 
    
         
             
            files:
         
     | 
| 
      
 262 
     | 
    
         
            +
            - ".cursor/commands/changelog.md"
         
     | 
| 
       248 
263 
     | 
    
         
             
            - ".cursor/commands/priority35m36m335m32m.md"
         
     | 
| 
       249 
264 
     | 
    
         
             
            - ".rubocop.yml"
         
     | 
| 
       250 
265 
     | 
    
         
             
            - ".rubocop_todo.yml"
         
     | 
| 
       251 
266 
     | 
    
         
             
            - ".travis.yml"
         
     | 
| 
      
 267 
     | 
    
         
            +
            - 2025-10-29-one-more-na-update.md
         
     | 
| 
       252 
268 
     | 
    
         
             
            - CHANGELOG.md
         
     | 
| 
       253 
269 
     | 
    
         
             
            - Gemfile
         
     | 
| 
       254 
270 
     | 
    
         
             
            - Gemfile.lock
         
     | 
| 
         @@ -268,6 +284,7 @@ files: 
     | 
|
| 
       268 
284 
     | 
    
         
             
            - bin/commands/move.rb
         
     | 
| 
       269 
285 
     | 
    
         
             
            - bin/commands/next.rb
         
     | 
| 
       270 
286 
     | 
    
         
             
            - bin/commands/open.rb
         
     | 
| 
      
 287 
     | 
    
         
            +
            - bin/commands/plugin.rb
         
     | 
| 
       271 
288 
     | 
    
         
             
            - bin/commands/projects.rb
         
     | 
| 
       272 
289 
     | 
    
         
             
            - bin/commands/prompt.rb
         
     | 
| 
       273 
290 
     | 
    
         
             
            - bin/commands/restore.rb
         
     | 
| 
         @@ -298,14 +315,19 @@ files: 
     | 
|
| 
       298 
315 
     | 
    
         
             
            - lib/na/help_monkey_patch.rb
         
     | 
| 
       299 
316 
     | 
    
         
             
            - lib/na/next_action.rb
         
     | 
| 
       300 
317 
     | 
    
         
             
            - lib/na/pager.rb
         
     | 
| 
      
 318 
     | 
    
         
            +
            - lib/na/plugins.rb
         
     | 
| 
       301 
319 
     | 
    
         
             
            - lib/na/project.rb
         
     | 
| 
       302 
320 
     | 
    
         
             
            - lib/na/prompt.rb
         
     | 
| 
       303 
321 
     | 
    
         
             
            - lib/na/string.rb
         
     | 
| 
       304 
322 
     | 
    
         
             
            - lib/na/theme.rb
         
     | 
| 
       305 
323 
     | 
    
         
             
            - lib/na/todo.rb
         
     | 
| 
      
 324 
     | 
    
         
            +
            - lib/na/types.rb
         
     | 
| 
       306 
325 
     | 
    
         
             
            - lib/na/version.rb
         
     | 
| 
       307 
326 
     | 
    
         
             
            - na.gemspec
         
     | 
| 
       308 
327 
     | 
    
         
             
            - na.rdoc
         
     | 
| 
      
 328 
     | 
    
         
            +
            - na/Test.todo.markdown
         
     | 
| 
      
 329 
     | 
    
         
            +
            - na/test.md
         
     | 
| 
      
 330 
     | 
    
         
            +
            - plugins.md
         
     | 
| 
       309 
331 
     | 
    
         
             
            - scripts/fixreadme.rb
         
     | 
| 
       310 
332 
     | 
    
         
             
            - scripts/generate-fish-completions.rb
         
     | 
| 
       311 
333 
     | 
    
         
             
            - scripts/runtests.sh
         
     |