Skip to content

Commit 7d30668

Browse files
dwertheimerclaude
andcommitted
Add nplog: a live, filterable viewer for NotePlan plugin logs
Everything a plugin (or its WebView) logs lands in NotePlan's rolling log file, and `tail -f | grep` handles it badly: - NotePlan starts a new timestamped log file on every relaunch, so a plain tail stays pinned to the old file and silently goes quiet. nplog follows the newest file and prints a marker when it switches. - grep commits to a pattern as each line streams past. nplog keeps entries in memory, so retyping the filter re-searches everything already read. - A pretty-printed object spans many physical log lines, and grep hands back one orphaned field. nplog detects the unbalanced brace/bracket and keeps absorbing lines until it balances, storing the whole object as a single entry -- so matching any field shows the entire object, the context modes step by log entry rather than by line, and the body is re-indented by nesting depth for readability. Features: case-insensitive regex filter (invalid patterns fall back to a literal match rather than erroring mid-typing), context modes showing the match plus the next 1/5/10 entries, dimmed-or-hidden timestamps, and live follow with scrollback. All bindings are control keys, since compact Mac keyboards have no PgUp/PgDn/Home/End. Runs on the alternate screen so it leaves the terminal's own scrollback untouched. No dependencies -- a single Node script. Install with scripts/nplog/install.sh, which symlinks it onto PATH so a later git pull updates the tool you actually run. Worth knowing for anyone writing their own log filter: NotePlan prefixes every line with a timestamp, so the payload marker is "<timestamp> JSLog:" and never at the start of the line. Anchoring on /^JSLog:/ matches nothing at all -- and fails silently rather than erroring. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e726ffc commit 7d30668

4 files changed

Lines changed: 1026 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,21 @@ The common script you will run `npc plugin:dev <plugin>` however, you may need t
218218
- `npm run format`: auto-format all Javascript files using `prettier`
219219
- `gh release delete <release name>`: Will delete the release from the repository, so making it unavailable in NotePlan as well. (Though it won't remove it from anyone who has already downloaded it.)
220220

221+
### Viewing Plugin Logs
222+
223+
Everything your plugin (or its WebView) logs goes into NotePlan's rolling log file. `nplog` is a
224+
terminal viewer for it — it follows the newest log file across NotePlan restarts, filters live by
225+
regex as you type, and keeps pretty-printed objects together as a single entry instead of
226+
scattering them across lines.
227+
228+
```bash
229+
./scripts/nplog/install.sh # one-time; symlinks `nplog` onto your PATH
230+
nplog # follow the log
231+
nplog 'ERROR|WARN' # start with a filter set (though you can change it at any time)
232+
```
233+
234+
See [scripts/nplog/README.md](scripts/nplog/README.md) for the key bindings and options.
235+
221236
## Editor Setup
222237

223238
Use the setup guide for your preferred editor (we prefer Visual Studio Code), and then read the section on Working with Multiple Files.

‎scripts/nplog/README.md‎

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
# nplog
2+
3+
A live, filterable viewer for [NotePlan](https://noteplan.co)'s plugin log output.
4+
5+
NotePlan writes every `console.log` from a plugin (and from plugin WebViews) into a rolling
6+
log file inside its app container (in addition to NotePlan's own log).
7+
8+
`nplog` is a terminal-based program that tails that file, strips it down to just the plugin
9+
output, and puts a **regex filter box** on screen that you can retype at any moment without
10+
restarting anything.
11+
12+
## Why not just `tail | grep`?
13+
14+
- **It follows log rotation.** NotePlan starts a brand-new timestamped file
15+
(`co.noteplan.NotePlan3 2026-07-28--20-41-00-962.log`) every time it relaunches. A plain
16+
`tail -f` stays pinned to the old file and silently goes quiet. `nplog` notices the newer
17+
file, prints a `--- switching to … ---` marker, and keeps going.
18+
- **You can change the filter without losing history.** `grep` decides once, at the moment
19+
the line streams past. `nplog` keeps the lines in memory, so retyping the filter
20+
re-searches everything it has already read.
21+
- **Context modes.** Log lines are rarely useful alone — you usually want the few lines that
22+
came *after* the one that matched.
23+
- **Pretty-printed objects stay intact.** `console.log` of an object spans a dozen physical
24+
log lines; `grep` hands you one orphaned field. `nplog` treats the whole object as a single
25+
entry — see [Multi-line objects](#multi-line-objects).
26+
27+
## Requirements
28+
29+
Node.js (any recent version). No npm packages — it is a single dependency-free script.
30+
31+
### Terminal compatibility
32+
33+
Works in any terminal emulator, including iTerm2. It only uses the universally-supported
34+
ANSI/VT100 basics — alternate screen (`?1049h`), cursor positioning, erase-line, reverse
35+
video, dim, and 8-colour SGR — plus Node's own raw-mode key handling, which is
36+
terminal-agnostic.
37+
38+
Verified on macOS **Terminal.app** and **iTerm2**. It should be equally happy in Ghostty,
39+
kitty, WezTerm, Alacritty, Warp, and the VS Code / Zed integrated terminals.
40+
41+
Two deliberate choices help here: every binding is a **control key**, and the
42+
help line **collapses to a shorter form** on narrower windows rather than wrapping.
43+
44+
## Install
45+
46+
From the root of this repo:
47+
48+
```bash
49+
./scripts/nplog/install.sh
50+
```
51+
52+
That symlinks `nplog` into `~/.local/bin` so it will run from your shell.
53+
Pass a different directory to override the location, e.g.
54+
`./scripts/nplog/install.sh /usr/local/bin`.
55+
56+
Because it's a **symlink**, a later `git pull` updates the tool you actually run — there's no
57+
need to re-install. If `~/.local/bin` isn't on your `PATH`, the installer tells you what to add
58+
to your shell profile.
59+
60+
You can also skip installing and just run it in place: `node scripts/nplog/nplog`.
61+
62+
## Usage
63+
64+
```bash
65+
nplog # follow the newest log
66+
nplog dashboard # start with a filter already applied
67+
nplog 'Section|Perspective' # regex alternation
68+
nplog --mode 5 # start in "match + next 5 lines" mode
69+
nplog --no-time # start with timestamps hidden
70+
nplog --raw # keep non-JSLog lines (NotePlan's own native logging)
71+
nplog --file "/path/to/some.log" # pin one file instead of auto-following
72+
```
73+
74+
### Keys
75+
76+
Everything is a control key, because compact Mac keyboards have no PgUp / PgDn / Home / End.
77+
78+
| Key | Action |
79+
| --- | --- |
80+
| *(type anything)* | edit the filter — applies live |
81+
| `Tab` | more context: `match only` → `+1` → `+5` → `+10` → back |
82+
| `^T` | show / hide timestamps |
83+
| `^U` | clear the filter |
84+
| `^L` | clear the buffer (drop history, keep following) |
85+
| `^B` / `^F` | page **b**ack / **f**orward |
86+
| `^P` / `^N` | one line **p**revious / **n**ext |
87+
| `^A` / `^E` | jump to top / end (`^E` also resumes following) |
88+
| `^C` | quit |
89+
90+
The bottom line always spells out what `Tab` will do next, e.g.
91+
`TAB=more context (now match only → match +1)`.
92+
93+
If you *do* have a full-size keyboard (or use Fn+arrow), `PgUp`, `PgDn`, `Home`, `End` and the
94+
arrow keys work too.
95+
96+
### The status bar
97+
98+
```
99+
filter: getRemindersGenerated|refreshSomeSections re ctx:match only 2942/8723 FOLLOW
100+
```
101+
102+
- `re` — the filter compiled as a regex. If it can't (say you've typed `Section(` and haven't
103+
closed the paren yet) this reads `LITERAL(bad re)` and the text is matched literally
104+
instead. It never errors out mid-typing.
105+
- `no-time` — appears only when timestamps are hidden (`^T`).
106+
- `ctx:match only` — current context mode.
107+
- `2942/8723` — lines currently shown / lines in the buffer.
108+
- `FOLLOW` / `SCROLL` — whether new output auto-scrolls. Scrolling up switches to `SCROLL`;
109+
`^E` returns to `FOLLOW`.
110+
111+
Matches are highlighted, and `┈┈┈` rules mark places where lines were skipped between
112+
non-adjacent matches.
113+
114+
### Timestamps
115+
116+
Every line carries a `2026-07-29 11:34:54`, which is mostly noise once you're reading a
117+
filtered slice. By default they're **dimmed** rather than removed, so they stay available but
118+
recede. `^T` (or `--no-time`) removes them entirely:
119+
120+
```
121+
2026-07-29 11:53:01 | DEBUG | refreshSomeSections :: Starting for TB <- dimmed stamp
122+
DEBUG | refreshSomeSections :: Starting for TB <- after ^T
123+
```
124+
125+
Filtering always runs against the *full* line, so you can still search for a time even while
126+
timestamps are hidden.
127+
128+
### Multi-line objects
129+
130+
When a plugin pretty-prints an object or array, NotePlan writes it across many physical log
131+
lines — and only the *first* carries the `JSLog:` marker; the rest just repeat the timestamp:
132+
133+
```
134+
2026-07-28 14:46:20 JSLog: 2026-07-28 14:46:20 | DEBUG | => Reminder: :: {
135+
2026-07-28 14:46:20 "title": "You should cancel the auto renew on Medawar",
136+
2026-07-28 14:46:20 "listname": "Reminders",
137+
2026-07-28 14:46:20 }
138+
```
139+
140+
`nplog` detects the unbalanced `{` (or `[`), and keeps absorbing following lines until the
141+
brace balances — storing the whole thing as **one entry**:
142+
143+
```
144+
2026-07-28 14:46:20 | DEBUG | => Reminder: :: {
145+
"title": "You should cancel the auto renew on Medawar",
146+
"listname": "Reminders",
147+
}
148+
```
149+
150+
The repeated timestamps on the continuation lines are dropped, and — since NotePlan flattens
151+
the original indentation — the body is **re-indented by nesting depth**, so it reads as real
152+
JSON again. Closing braces line up with their openers:
153+
154+
```
155+
2026-07-29 14:40:33 | DEBUG | => Reminder: :: {
156+
"title": "Send out the newsletter",
157+
"occurences": {
158+
"weekly": true,
159+
"days": [
160+
"mon",
161+
"tue"
162+
]
163+
},
164+
"time": "10:00"
165+
}
166+
```
167+
168+
Because it's one entry:
169+
170+
- Filtering on **any** field shows you the **entire object**, not a lone line. `listname`
171+
matching 178 objects reports `178`, not 178 fragments.
172+
- The `+1/+5/+10` context modes count *entries*, so "+1" means the next log entry — not the
173+
next line of the object you're already looking at.
174+
- A regex can span the newlines (`.` is dotAll here), so `Reminder.*listname` matches.
175+
176+
Robustness details, all verified against real logs:
177+
178+
- Brace counting **ignores braces inside string values**, so `"tricky {"` or a value of `"]"`
179+
can't throw off the count.
180+
- A new `<timestamp> JSLog:` line **always** ends the block, so a truncated or malformed object
181+
can't swallow the rest of the log.
182+
- Blocks render **as they stream** — you see the object growing, rather than waiting for the
183+
closing brace.
184+
- A block left open when the log rotates is closed at the switch.
185+
- Sometimes the `{` lands on the line *after* the `JSLog:` header; that fragment starts its own
186+
block rather than being dropped.
187+
188+
## Filtering is regex
189+
190+
The filter is a case-insensitive JavaScript regular expression, so:
191+
192+
| Pattern | Finds |
193+
| --- | --- |
194+
| `dashboard` | plain substring |
195+
| `Section\|Perspective` | either word |
196+
| `^\[WebView\]` | only WebView lines |
197+
| `ERROR\|WARN` | problems only |
198+
| `refresh.*Sections` | wildcards between terms |
199+
| `:: \d+ items` | digits |
200+
201+
Anything that isn't valid regex is matched literally, so you can paste an arbitrary log
202+
fragment (`sections [TB`) without escaping it.
203+
204+
## Where the logs come from
205+
206+
```
207+
~/Library/Containers/co.noteplan.NotePlan3/Data/Library/Application Support/co.noteplan.NotePlan3/Logs/
208+
```
209+
210+
Files are named `co.noteplan.NotePlan3 <timestamp>.log`; beta builds may nest a second
211+
`Logs/` directory inside that one, and both are searched. If you have override the location with the
212+
`NPLOG_DIR` environment variable, e.g. if you use Setapp.
213+
214+
```bash
215+
NPLOG_DIR=/some/other/Logs nplog
216+
e.g.
217+
NPLOG_DIR=~/Library/Containers/co.noteplan.NotePlan-setapp/Data/Library/Application nplog
218+
```
219+
220+
### What gets shown by default
221+
222+
NotePlan tags plugin output with a `JSLog:` marker (basically all the plugin console contents)
223+
224+
```
225+
2026-07-29 11:34:54 JSLog: [WebView Log] 2026-07-29 11:34:54 | DEBUG | Webview :: sendActionToPlugin
226+
```
227+
228+
`nplog` keeps only those lines (plus the continuation lines of a multi-line object), drops
229+
everything up to and including the marker, and abbreviates `[WebView Log]` to `[WebView]`.
230+
231+
**Use `--raw` to see NotePlan's own non-plugin logging too.**
232+
233+
> **Gotcha if you write your own filter:** don't anchor on `^JSLog:`. NotePlan added the
234+
> leading `2026-07-29 11:34:54` timestamp at some point, and that anchor silently stopped
235+
> matching — the original `noteplan_log_filtered.sh` emitted *nothing* for months rather than
236+
> erroring. Strip the timestamp first, *then* anchor on `JSLog:`.
237+
238+
## Notes
239+
240+
- Runs on the terminal's alternate screen, so it never pollutes your real scrollback, and
241+
quitting leaves the terminal exactly as it was.
242+
- Holds the most recent 50,000 **entries** in memory (`MAX_LINES` in the script) — a
243+
multi-line object counts as one. On startup it seeds from the last 2 MB of the current file
244+
so there's immediate history to search.
245+
- Polls once a second for new bytes and for rotation.
246+
- A block that never closes is capped at 500 continuation lines (`MAX_CONT_LINES`).
247+
248+
## Running it alongside your watchers
249+
250+
`nplog` is most useful in its own terminal tab, next to `npc plugin:dev` and your test watcher —
251+
you edit, the plugin rebuilds, and you watch the log react.
252+
253+
If you drive your dev environment from a personal `.hyperlayout` file (gitignored, so yours is
254+
your own), give it a dedicated tab rather than a narrow pane — the filter bar and context modes
255+
want the width:
256+
257+
```json
258+
"dashboard": [
259+
[ "npc plugin:dev jgclark.Dashboard -nc" ],
260+
[ "npm run test:watch" ],
261+
[ "nplog" ]
262+
]
263+
```

‎scripts/nplog/install.sh‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/bash
2+
# Symlink nplog onto your PATH. Safe to re-run.
3+
#
4+
# ./install.sh -> links into ~/.local/bin
5+
# ./install.sh /usr/local/bin -> links somewhere else
6+
#
7+
# A symlink (rather than a copy) means `git pull` updates the tool you actually
8+
# run -- no need to re-install after pulling changes to this repo.
9+
10+
set -euo pipefail
11+
12+
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/nplog"
13+
BIN_DIR="${1:-$HOME/.local/bin}"
14+
15+
if [ ! -f "$SRC" ]; then
16+
echo "install.sh: cannot find nplog next to this script (looked for $SRC)" >&2
17+
exit 1
18+
fi
19+
20+
if ! command -v node >/dev/null 2>&1; then
21+
echo "install.sh: node is required but was not found on PATH." >&2
22+
echo " Install Node (https://nodejs.org) then re-run." >&2
23+
exit 1
24+
fi
25+
26+
mkdir -p "$BIN_DIR"
27+
chmod +x "$SRC"
28+
ln -sfn "$SRC" "$BIN_DIR/nplog"
29+
30+
echo "Linked $BIN_DIR/nplog -> $SRC"
31+
32+
case ":$PATH:" in
33+
*":$BIN_DIR:"*) ;;
34+
*)
35+
echo
36+
echo "NOTE: $BIN_DIR is not on your PATH. Add this to your shell profile:"
37+
echo " export PATH=\"$BIN_DIR:\$PATH\""
38+
;;
39+
esac

0 commit comments

Comments
 (0)