na 1.2.87 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d30aa922aad5901cf7d844f0a41250ab6245cf3bb2eb89e92e14e890f74088b5
4
- data.tar.gz: 253d3825d1a0c2cb8666cd478b608ac77fee44ceb75cee15f9192f57c33a4b5b
3
+ metadata.gz: b06708529c5a33331a67f4295783bee29e559343ff3e2aa097f0802efac04ca6
4
+ data.tar.gz: 4bf4437737504d54b4cd018c339a4643ca1d108d94e407624040905c0fa5e9f8
5
5
  SHA512:
6
- metadata.gz: 80c1708db72e9683f0de4acb35a762b31d3de02c4062ecf8f028d83997d2f40c9545e0c3601505dd80e7e2a985c53bfa9504011ff0a5f9321da066e633649a75
7
- data.tar.gz: 7e95f4f33952c819bd33cdad8316aeafbc1c6bf05dbed1ff59ea887b64a7dd4bee3d70de9b5a1b092e53e2c1db3626813e38e679d5544b27b2607b0f78eda22a
6
+ metadata.gz: b3aac3037d6a51782dad07a452e4fc3e987ef4b3165dfc718119a7b9b5afc23ad42c069243769bf160cd6852f65a239ab655300e465524d03e9480fbef853282
7
+ data.tar.gz: 67aa8edd664f3023a9cef4478c1ef607aba7f8b435413579d51110fdeeddbd9786674ad9320c65a3b7ef3d3b00b34860510fcae089fba37843fc010253306a92
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2025-10-28 10:51:34 UTC using RuboCop version 1.75.7.
3
+ # on 2025-10-28 13:13:50 UTC using RuboCop version 1.75.7.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -21,7 +21,7 @@ Lint/UnusedMethodArgument:
21
21
  Exclude:
22
22
  - 'lib/na/string.rb'
23
23
 
24
- # Offense count: 39
24
+ # Offense count: 45
25
25
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
26
26
  Metrics/AbcSize:
27
27
  Max: 276
@@ -40,29 +40,29 @@ Metrics/BlockNesting:
40
40
  # Offense count: 6
41
41
  # Configuration parameters: CountComments, CountAsOne.
42
42
  Metrics/ClassLength:
43
- Max: 829
43
+ Max: 904
44
44
 
45
- # Offense count: 29
45
+ # Offense count: 34
46
46
  # Configuration parameters: AllowedMethods, AllowedPatterns.
47
47
  Metrics/CyclomaticComplexity:
48
48
  Max: 82
49
49
 
50
- # Offense count: 44
50
+ # Offense count: 53
51
51
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
52
52
  Metrics/MethodLength:
53
53
  Max: 183
54
54
 
55
- # Offense count: 4
55
+ # Offense count: 5
56
56
  # Configuration parameters: CountComments, CountAsOne.
57
57
  Metrics/ModuleLength:
58
- Max: 831
58
+ Max: 906
59
59
 
60
60
  # Offense count: 5
61
61
  # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
62
62
  Metrics/ParameterLists:
63
63
  Max: 22
64
64
 
65
- # Offense count: 29
65
+ # Offense count: 33
66
66
  # Configuration parameters: AllowedMethods, AllowedPatterns.
67
67
  Metrics/PerceivedComplexity:
68
68
  Max: 94
@@ -96,6 +96,14 @@ Style/ModuleFunction:
96
96
  Exclude:
97
97
  - 'lib/na/colors.rb'
98
98
 
99
+ # Offense count: 1
100
+ # This cop supports safe autocorrection (--autocorrect).
101
+ # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
102
+ # SupportedStyles: single_quotes, double_quotes
103
+ Style/StringLiterals:
104
+ Exclude:
105
+ - 'lib/na/next_action.rb'
106
+
99
107
  # Offense count: 1
100
108
  # This cop supports safe autocorrection (--autocorrect).
101
109
  # Configuration parameters: EnforcedStyle.
@@ -110,7 +118,7 @@ Style/YAMLFileRead:
110
118
  Exclude:
111
119
  - 'lib/na/theme.rb'
112
120
 
113
- # Offense count: 27
121
+ # Offense count: 30
114
122
  # This cop supports safe autocorrection (--autocorrect).
115
123
  # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
116
124
  # URISchemes: http, https
@@ -0,0 +1,142 @@
1
+ ---
2
+ layout: post
3
+ title: One more na update
4
+ categories:
5
+ - Code
6
+ - Blog
7
+ tags:
8
+ - scripting
9
+ - zsh
10
+ - tagging
11
+ - na
12
+ - cli
13
+ - ruby
14
+ - productivity
15
+ - taskpaper
16
+ date: 2025-10-29 08:00
17
+ slug: one-more-na-update
18
+ post_class: code
19
+ keywords: [next action]
20
+ ---
21
+ This week I pushed a set of focused improvements to [NA](https://brettterpstra.com/projects/na) that make interactive workflows a lot smoother and the codebase a little more robust — including first‑class time tracking.
22
+
23
+ If you run `na update` without arguments you'll now get an interactive menu that helps you pick the file(s) and actions to operate on, with multiple selection and small but important quality-of-life fixes across parsing, move/edit behavior, and documentation.
24
+
25
+ ## TL;DR
26
+
27
+ - `na update` (no args) now launches an interactive, consistent selection flow for files and actions.
28
+ - The update submenu supports multi-select, edit, move, and direct action modes more reliably.
29
+ - Fixed many edge cases: nil-safe string helpers, clearer YARD docs, and better tests for helper code.
30
+ - Under-the-hood improvements to file selection, action movement, and tagging logic.
31
+
32
+ ### New: Time tracking
33
+
34
+ - Add start/finish times right from the CLI:
35
+ - `--started TIME` on `add`, `complete`/`finish`, and `update`
36
+ - `--end TIME` (alias `--finished`) on `add`, `complete`/`finish`, and `update`
37
+ - `--duration DURATION` to backfill a start time from the finish
38
+ - Natural language and shorthand supported everywhere: `30m ago`, `-2h`, `2h30m`, `2:30 ago`, `yesterday 5pm`
39
+ - Durations aren’t stored; they’re computed from `@started(...)` and `@done(...)` when displayed.
40
+ - Display enhancements in `next`/`tagged`:
41
+ - `--times` shows per‑action durations and a grand total (implies `--done`)
42
+ - `--human` switches durations to friendly text
43
+ - `--only_timed` filters to actions with both `@started` and `@done` (implies `--times --done`)
44
+ - `--only_times` outputs only the totals (no action list; implies `--times --done`)
45
+ - `--json_times` emits a JSON object with timed actions, per‑tag totals, and overall totals (implies `--times --done`)
46
+ - Per‑tag totals are shown as a Markdown table with aligned columns and a footer row for the grand total
47
+ - Duration color is theme‑configurable via a `duration` key (default `{y}`)
48
+
49
+ Example:
50
+
51
+ ```bash
52
+ na add --started "30 minutes ago" "Investigate bug"
53
+ na complete --finished now --duration 2h30m "Investigate bug"
54
+ na next --times --human
55
+ na next --only_times
56
+ na tagged bug --json_times | jq
57
+ ```
58
+
59
+ ## What triggered this
60
+
61
+ A number of small UX issues had crept in over time: the interactive menu sometimes re-prompted or skipped expected options, move/edit operations wouldn't always update a project's indexes correctly, and a few helper methods could raise when given `nil` paths or values. I wanted to make the interactive flow predictable and to harden the helpers so the command-line experience is less brittle.
62
+
63
+ ## Interactive `na update` flow
64
+
65
+ Run `na update` with no arguments and you'll see a consistent selection flow:
66
+
67
+ - Choose one or more todo files (fuzzy search / [fzf](https://github.com/junegunn/fzf) / [gum](https://github.com/charmbracelet/gum) used when available).
68
+ - Pick which actions to update (multi-selection supported).
69
+ - Choose an operation: edit, move, add tag, remove tag, mark done, delete.
70
+
71
+ Examples:
72
+
73
+ ```bash
74
+ $ na update
75
+ Select files: (interactive list)
76
+ Select actions (multi):
77
+ [x] 23 % Inbox/Work : - Fix X
78
+ [x] 45 % Inbox/Personal : - Call Y
79
+ Choose operation: (edit / move / done / delete / tag)
80
+ ```
81
+
82
+ The menu now behaves consistently whether you pick a single file or multiple files; if you choose multi-select the update command applies as you'd expect to the set of chosen actions.
83
+
84
+ There's a direct action mode when you know the file and action: `na update PATH -l 23` still works as before. The interactive flow only kicks in when no explicit target is provided.
85
+
86
+ ## Notable fixes
87
+
88
+ A lot of the work was small but important:
89
+
90
+ - Nil-safe string helpers: `trunc_middle`, `highlight_filename`, and friends were guarded against `nil` inputs so tests and UI code don't explode when a file is missing or the database contains a stray blank line.
91
+ - Action move/edit correctness: moving an action to a different project now updates parent indexes and project line numbers properly, avoiding off-by-one bugs that could leave the file in a strange state.
92
+ - `select_file` and fuzzy matching: the fuzzy and database-driven file selection was made more robust — the code handles directories that have a `file.na` or `file/file.na` pattern and falls back to a clear error instead of failing silently.
93
+ - YARD docs: cleaned up a number of `@!method` directives and added top-level `@example` blocks for the main classes and helpers so the docs are friendlier and generate without warnings.
94
+ - Tests: added and fixed unit tests for `Array`, `Hash`, and `String` helpers. TTY screen and color-related test stubs were improved for reliability on CI.
95
+ - New tests for time features: JSON output, totals‑only output, timed‑only filtering.
96
+
97
+ ## Try it
98
+
99
+ You can update to the latest version with:
100
+
101
+ {% iterm "gem install na" %}
102
+
103
+ That should give you v1.2.85 or higher.
104
+
105
+ If you're on a recent development build or want to try the updates locally:
106
+
107
+ ```bash
108
+ # From the gem checkout
109
+ bundle exec bin/na update
110
+
111
+ # Or after building the gem and installing
112
+ na update
113
+ ```
114
+
115
+ If you run into anything odd, please open an issue with the command you ran and a short description of what you expected vs what happened. Small, reproducible steps are the fastest way to a fix.
116
+
117
+ If you hit an error and want to include a backtrace, run the command with debug enabled and paste the output:
118
+
119
+ ```bash
120
+ GLI_DEBUG=true na [COMMAND]
121
+ ```
122
+
123
+ ## Other updates
124
+
125
+ I last wrote about 1.2.80. Here are a few highlights from the subsequent releases:
126
+
127
+ - 1.2.85 (2025-10-26)
128
+ - YARD docs polish — coverage is now effectively complete
129
+ - Nil-safety: guards for `trunc_middle` and `highlight_filename`
130
+ - 1.2.84 (2025-10-25)
131
+ - Fix: handle nil input when traversing depth
132
+ - 1.2.83 (2025-10-25)
133
+ - Fix: ignore `-d X` values that exceed existing structure depth
134
+ - Allow depth > 9 for `-d`
135
+ - 1.2.82 (2025-10-25)
136
+ - New: multi‑select menu when using `na update`
137
+ - 1.2.81 (2025-10-25)
138
+ - New: `na scan` to find untracked todo files (thanks @rhsev)
139
+ - Improvements: RuboCop cleanup, YARD docs, and test coverage
140
+ - Fixes: color reset in parent display; subdirectory traversal with `na next -d X`
141
+
142
+ Thanks for playing with it and for the helpful feedback you've been sending. Check out the [NA project page](https://brettterpstra.com/projects/na) for more info.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,42 @@
1
+ ### 1.2.88
2
+
3
+ 2025-10-28 08:22
4
+
5
+ #### CHANGED
6
+
7
+ - Update command interactive menu lists available plugins
8
+ - README: add detailed Plugins section with examples and schema
9
+ - CHANGELOG: add 1.2.88 entry for plugins feature
10
+
11
+ #### NEW
12
+
13
+ - Plugin system with scripts in ~/.local/share/na/plugins
14
+ - Na plugin command to run plugins on selected actions
15
+ - --plugin/--input/--output/--divider flags on update/next/tagged/find
16
+ - Plugin IO supports json, yaml, csv, and text formats
17
+ - ACTION support in plugin IO (UPDATE/DELETE/COMPLETE/RESTORE/ARCHIVE/ADD_TAG/DELETE_TAG/MOVE)
18
+ - Auto-create plugins dir with README and sample plugins (Add Foo.py, Add Bar.sh)
19
+ - Display commands can pipe through plugins (STDOUT-only, no writes)
20
+ - Add --json_times to next/tagged (JSON of timed actions, tags, total)
21
+ - Add --only_times to next/tagged (show only totals, no action list)
22
+
23
+ #### IMPROVED
24
+
25
+ - Duration/time docs and output clarifications in README
26
+ - Internal plugin README with metadata, IO, and examples
27
+ - --only_timed, --times, and --json_times imply --done automatically
28
+ - Per-tag duration totals rendered as aligned Markdown table with footer
29
+ - Duration color configurable via theme key `duration` (default {y})
30
+
31
+ #### FIXED
32
+
33
+ - Plugin update path writing duplicate actions; now in-place by line
34
+ - Plugin apply finds action via target_line, avoids Symbol->Integer error
35
+ - String#wrap indentation and width handling for multi-line wrapping
36
+ - Lint/DuplicateBranch in plugin parse_actions
37
+ - Style/MapToHash in apply_plugin_result
38
+ - String wrapping now wraps at requested widths (e.g., 60 cols) and indents
39
+
1
40
  ### 1.2.87
2
41
 
3
42
  2025-10-28 06:21
data/Gemfile CHANGED
@@ -11,3 +11,4 @@ group :development do
11
11
  gem 'rubocop-performance', '~> 1.21'
12
12
  end
13
13
  gem 'chronic'
14
+ gem 'csv'
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.87)
4
+ na (1.2.88)
5
5
  chronic (~> 0.10, >= 0.10.2)
6
+ csv (>= 3.2)
6
7
  git (~> 3.0.0)
7
8
  gli (~> 2.21.0)
8
9
  mdless (~> 1.0, >= 1.0.32)
@@ -36,6 +37,7 @@ GEM
36
37
  chronic (0.10.2)
37
38
  concurrent-ruby (1.3.5)
38
39
  connection_pool (2.5.4)
40
+ csv (3.3.5)
39
41
  diff-lcs (1.6.2)
40
42
  docile (1.4.1)
41
43
  drb (2.2.3)
@@ -139,6 +141,7 @@ PLATFORMS
139
141
  DEPENDENCIES
140
142
  bump (~> 0.6.0)
141
143
  chronic
144
+ csv
142
145
  minitest (~> 5.14)
143
146
  na!
144
147
  rake
data/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 1.2.87.
12
+ The current version of `na` is 1.2.88.
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
 
@@ -76,7 +76,7 @@ SYNOPSIS
76
76
  na [global options] command [command options] [arguments...]
77
77
 
78
78
  VERSION
79
- 1.2.87
79
+ 1.2.88
80
80
 
81
81
  GLOBAL OPTIONS
82
82
  -a, --add - Add a next action (deprecated, for backwards compatibility)
@@ -112,6 +112,7 @@ COMMANDS
112
112
  move - Move an existing action to a different section
113
113
  next, show - Show next actions
114
114
  open - Open a todo file in the default editor
115
+ plugin - Run a plugin on selected actions
115
116
  projects - Show list of projects for a file
116
117
  prompt - Show or install prompt hooks for the current shell
117
118
  restore, unfinish - Find and remove @done tag from an action
@@ -224,15 +225,19 @@ DESCRIPTION
224
225
 
225
226
  COMMAND OPTIONS
226
227
  -d, --depth=DEPTH - Recurse to depth (default: none)
228
+ --divider=STRING - Divider string for text IO (default: none)
227
229
  --[no-]done - Include @done actions
228
230
  -e, --regex - Interpret search pattern as regular expression
229
231
  --human - Format durations in human-friendly form
230
232
  --in=TODO_PATH - Show actions from a specific todo file in history. May use wildcards (* and ?) (default: none)
233
+ --input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
231
234
  --nest - Output actions nested by file
232
235
  --no_file - No filename in output
233
236
  --[no-]notes - Include notes in output
234
237
  -o, --or - Combine search tokens with OR, displaying actions matching ANY of the terms
235
238
  --omnifocus - Output actions nested by file and project
239
+ --output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
240
+ --plugin=NAME - Run a plugin on results (STDOUT only; no file writes) (default: none)
236
241
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
237
242
  --save=TITLE - Save this search for future use (default: none)
238
243
  --[no-]search_notes - Include notes in search (default: enabled)
@@ -338,12 +343,14 @@ DESCRIPTION
338
343
  COMMAND OPTIONS
339
344
  --all - Show next actions from all known todo files (in any directory)
340
345
  -d, --depth=DEPTH - Recurse to depth (default: none)
346
+ --divider=STRING - Divider string for text IO (default: none)
341
347
  --[no-]done - Include @done actions
342
348
  --exact - Search query is exact text match (not tokens)
343
349
  --file=TODO_FILE - Display matches from specific todo file ([relative] path) (default: none)
344
350
  --hidden - Include hidden directories while traversing
345
351
  --human - Format durations in human-friendly form
346
352
  --in, --todo=TODO - Display matches from a known todo file anywhere in history (short name) (may be used more than once, default: none)
353
+ --input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
347
354
  --json_times - Output times as JSON object (implies --times and --done)
348
355
  --nest - Output actions nested by file
349
356
  --no_file - No filename in output
@@ -351,7 +358,9 @@ COMMAND OPTIONS
351
358
  --omnifocus - Output actions nested by file and project
352
359
  --only_timed - Show only actions that have a duration (@started and @done)
353
360
  --only_times - Output only elapsed time totals (implies --times and --done)
361
+ --output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
354
362
  -p, --prio, --priority=PRIORITY - Match actions with priority, allows <>= comparison (may be used more than once, default: none)
363
+ --plugin=NAME - Run a plugin on results (STDOUT only; no file writes) (default: none)
355
364
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
356
365
  --regex - Search query is regular expression
357
366
  --save=TITLE - Save this search for future use (default: none)
@@ -499,10 +508,12 @@ DESCRIPTION
499
508
 
500
509
  COMMAND OPTIONS
501
510
  -d, --depth=DEPTH - Recurse to depth (default: 1)
511
+ --divider=STRING - Divider string for text IO (default: none)
502
512
  --[no-]done - Include @done actions
503
513
  --exact - Search query is exact text match (not tokens)
504
514
  --human - Format durations in human-friendly form
505
515
  --in=TODO_PATH - Show actions from a specific todo file in history. May use wildcards (* and ?) (default: none)
516
+ --input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
506
517
  --json_times - Output times as JSON object (implies --times and --done)
507
518
  --nest - Output actions nested by file
508
519
  --no_file - No filename in output
@@ -511,6 +522,8 @@ COMMAND OPTIONS
511
522
  --omnifocus - Output actions nested by file and project
512
523
  --only_timed - Show only actions that have a duration (@started and @done)
513
524
  --only_times - Output only elapsed time totals (implies --times and --done)
525
+ --output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
526
+ --plugin=NAME - Run a plugin on results (STDOUT only; no file writes) (default: none)
514
527
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
515
528
  --regex - Search query is regular expression
516
529
  --save=TITLE - Save this search for future use (default: none)
@@ -609,6 +622,7 @@ COMMAND OPTIONS
609
622
  --at=POSITION - When moving task, add at [s]tart or [e]nd of target project (default: none)
610
623
  -d, --depth=DEPTH - Search for files X directories deep (default: 1)
611
624
  --delete - Delete an action
625
+ --divider=STRING - Divider string for text IO (default: none)
612
626
  --[no-]done - Include @done actions
613
627
  --duration=DURATION - Duration (e.g. 45m, 2h, 1d2h30m, or minutes) (default: none)
614
628
  -e, --regex - Interpret search pattern as regular expression
@@ -617,9 +631,12 @@ COMMAND OPTIONS
617
631
  -f, --finish - Add a @done tag to action
618
632
  --file=PATH - Specify the file to search for the task (default: none)
619
633
  --in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
634
+ --input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
620
635
  -n, --note - Prompt for additional notes. Input will be appended to any existing note. If STDIN input (piped) is detected, it will be used as a note.
621
636
  -o, --overwrite - Overwrite note instead of appending
637
+ --output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
622
638
  -p, --priority=PRIO - Add/change a priority level 1-5 (default: 0)
639
+ --plugin=NAME - Run a plugin by name on selected actions (default: none)
623
640
  --proj, --project=PROJECT[/SUBPROJECT] - Affect actions from a specific project (default: none)
624
641
  -r, --remove=TAG - Remove a tag from the action, use multiple times or combine multiple tags with a comma, wildcards (* and ?) allowed (may be used more than once, default: none)
625
642
  --replace=TEXT - Use with --find to find and replace with new text. Enables --exact when used (default: none)
@@ -898,6 +915,115 @@ If you're using a single global file, you'll need `--cwd_as` to be `tag` or `pro
898
915
 
899
916
  After installing a hook, you'll need to close your terminal and start a new session to initialize the new commands.
900
917
 
918
+ ### Plugins
919
+
920
+ 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.
921
+
922
+ #### Getting Started
923
+
924
+ The first time NA runs, it will create the plugins directory with a README and two sample plugins:
925
+ - `Add Foo.py` - Adds a `@foo` tag with a timestamp
926
+ - `Add Bar.sh` - Adds a `@bar` tag
927
+
928
+ You can delete or modify these sample plugins as needed.
929
+
930
+ #### Running Plugins
931
+
932
+ Run a plugin with:
933
+ ```bash
934
+ na plugin PLUGIN_NAME
935
+ ```
936
+
937
+ Or use plugins through the `update` command's interactive menu, or pipe actions through plugins on display commands:
938
+
939
+ ```bash
940
+ na update --plugin PLUGIN_NAME # Run plugin on selected actions
941
+ na next --plugin PLUGIN_NAME # Transform output only (no file writes)
942
+ na tagged bug --plugin PLUGIN_NAME # Filter and transform
943
+ na find "search term" --plugin PLUGIN_NAME
944
+ ```
945
+
946
+ #### Plugin Metadata
947
+
948
+ Plugins can specify their behavior in a metadata block after the shebang:
949
+
950
+ ```bash
951
+ #!/usr/bin/env python3
952
+ # name: My Plugin
953
+ # input: json
954
+ # output: json
955
+ ```
956
+
957
+ Available metadata keys (case-insensitive):
958
+ - `input`: Input format (`json`, `yaml`, `csv`, `text`)
959
+ - `output`: Output format
960
+ - `name` or `title`: Display name (defaults to filename)
961
+
962
+ #### Input/Output Formats
963
+
964
+ Plugins accept and return action data. Use `--input` and `--output` flags to override metadata:
965
+
966
+ ```bash
967
+ na plugin MY_PLUGIN --input text --output json --divider "||"
968
+ ```
969
+
970
+ **JSON/YAML Schema:**
971
+ ```json
972
+ [
973
+ {
974
+ "file_path": "todo.taskpaper",
975
+ "line": 15,
976
+ "parents": ["Project", "Subproject"],
977
+ "text": "- Action text @tag(value)",
978
+ "note": "Note content",
979
+ "tags": [
980
+ { "name": "tag", "value": "value" }
981
+ ]
982
+ }
983
+ ]
984
+ ```
985
+
986
+ **Text Format:**
987
+ ```
988
+ ACTION||ARGS||file_path:line||parents||text||note||tags
989
+ ```
990
+
991
+ Default divider is `||` (configurable with `--divider`).
992
+ - `parents`: `Parent>Child>Leaf`
993
+ - `tags`: `name(value);name;other(value)`
994
+
995
+ If the first token isn???t a known action, it???s treated as `file_path:line` and the action defaults to UPDATE.
996
+
997
+ #### Actions
998
+
999
+ Plugins may return an optional ACTION with arguments. Supported (case-insensitive):
1000
+ - UPDATE (default; replace text/note/tags/parents)
1001
+ - DELETE
1002
+ - COMPLETE/FINISH
1003
+ - RESTORE/UNFINISH
1004
+ - ARCHIVE
1005
+ - ADD_TAG (args: one or more tags)
1006
+ - DELETE_TAG/REMOVE_TAG (args: one or more tags)
1007
+ - MOVE (args: target project path)
1008
+
1009
+ #### Plugin Behavior
1010
+
1011
+ **On `update` or `plugin` command:**
1012
+ - Plugins can modify text, notes, tags, and parents
1013
+ - Changing `parents` will move the action to the new project location
1014
+ - `file_path` and `line` cannot be changed
1015
+
1016
+ **On display commands (`next`, `tagged`, `find`):**
1017
+ - Plugins only transform STDOUT (no file writes)
1018
+ - Use returned text/note/tags/parents for rendering
1019
+ - Parent changes affect display but not file structure
1020
+
1021
+ #### Override Formats
1022
+
1023
+ You can override plugin defaults with flags on any command that supports `--plugin`:
1024
+ ```bash
1025
+ na next --plugin FOO --input csv --output text
1026
+ ```
901
1027
 
902
1028
  [fzf]: https://github.com/junegunn/fzf
903
1029
  [gum]: https://github.com/charmbracelet/gum
data/bin/commands/find.rb CHANGED
@@ -68,6 +68,22 @@ class App
68
68
  c.desc "Output actions nested by file and project"
69
69
  c.switch %[omnifocus], negatable: false
70
70
 
71
+ c.desc 'Run a plugin on results (STDOUT only; no file writes)'
72
+ c.arg_name 'NAME'
73
+ c.flag %i[plugin]
74
+
75
+ c.desc 'Plugin input format (json|yaml|csv|text)'
76
+ c.arg_name 'TYPE'
77
+ c.flag %i[input]
78
+
79
+ c.desc 'Plugin output format (json|yaml|csv|text)'
80
+ c.arg_name 'TYPE'
81
+ c.flag %i[output]
82
+
83
+ c.desc 'Divider string for text IO'
84
+ c.arg_name 'STRING'
85
+ c.flag %i[divider]
86
+
71
87
  c.action do |global_options, options, args|
72
88
  options[:nest] = true if options[:omnifocus]
73
89
 
@@ -175,6 +191,52 @@ class App
175
191
  [tokens]
176
192
  end
177
193
 
194
+ # Plugin piping (display-only)
195
+ if options[:plugin]
196
+ NA::Plugins.ensure_plugins_home
197
+ plugin_path = options[:plugin]
198
+ unless File.exist?(plugin_path)
199
+ resolved = NA::Plugins.resolve_plugin(plugin_path)
200
+ plugin_path = resolved if resolved
201
+ end
202
+ if plugin_path && File.exist?(plugin_path)
203
+ meta = NA::Plugins.parse_plugin_metadata(plugin_path)
204
+ input_fmt = (options[:input] || meta['input'] || 'json').to_s
205
+ output_fmt = (options[:output] || meta['output'] || input_fmt).to_s
206
+ divider = (options[:divider] || '||')
207
+
208
+ io_actions = todo.actions.map(&:to_plugin_io_hash)
209
+ stdin_str = NA::Plugins.serialize_actions(io_actions, format: input_fmt, divider: divider)
210
+ stdout = NA::Plugins.run_plugin(plugin_path, stdin_str)
211
+ returned = Array(NA::Plugins.parse_actions(stdout, format: output_fmt, divider: divider))
212
+ index = {}
213
+ todo.actions.each { |a| index["#{a.file_path}:#{a.file_line}"] = a }
214
+ returned.each do |h|
215
+ key = "#{h['file_path']}:#{h['line'].to_i}"
216
+ a = index[key]
217
+ next unless a
218
+ new_text = h['text'].to_s
219
+ new_note = h['note'].to_s
220
+ new_tags = Array(h['tags']).map { |t| [t['name'].to_s, t['value'].to_s] }
221
+ new_text = new_text.gsub(/(?<=\A| )@\S+(?:\(.*?\))?/, '')
222
+ unless new_tags.empty?
223
+ tag_str = new_tags.map { |k, v| v.to_s.empty? ? "@#{k}" : "@#{k}(#{v})" }.join(' ')
224
+ new_text = new_text.strip + (tag_str.empty? ? '' : " #{tag_str}")
225
+ end
226
+ a.action = new_text
227
+ a.note = new_note.empty? ? [] : new_note.split("\n")
228
+ a.instance_variable_set(:@tags, a.scan_tags)
229
+ parents = Array(h['parents']).map(&:to_s)
230
+ if parents.any?
231
+ new_proj = parents.first.to_s
232
+ new_chain = parents[1..] || []
233
+ a.instance_variable_set(:@project, new_proj)
234
+ a.parent = new_chain
235
+ end
236
+ end
237
+ end
238
+ end
239
+
178
240
  todo.actions.output(depth,
179
241
  { files: todo.files,
180
242
  regexes: regexes,