feat(annotate): configurable extra markdown extensions - #1309
Merged
Conversation
Adds a config-only `markdownExtensions` key to ~/.plannotator/config.json,
e.g. { "markdownExtensions": [".livemd"] } for Livebook notebooks. A listed
extension is accepted everywhere .md is on the annotate path: CLI target
resolution, folder discovery and the file browser, /api/doc plus relative and
wiki-link navigation between sibling docs, the 2MB size cap, and per-file
version history. Listed extensions render as markdown with frontmatter
stripped, never as raw HTML, and they only widen the accepted set.
Design:
- packages/core/annotatable.ts stays browser-safe and zero-dep. Its regexes
and predicates now take an optional, defaulted-empty list of extra
extensions, plus a normalizer and regex builders.
- packages/shared/markdown-extensions.ts is the node-side seam: it reads
config.json once per process through the existing loadConfig() and threads
the normalized list into those pure functions. resolve-file re-exports the
config-aware predicates so both runtimes pick them up; the Bun server, the
Pi mirror, the OpenCode plugin and the CLI all go through them.
- The annotate /api/plan payload ships the resolved list so the renderer can
linkify links to sibling documents (module-level UI registry, empty by
default, so nothing changes without config).
Validation: entries must be dot-led, lowercase-normalized, and free of path
separators, globs and whitespace. Invalid entries are dropped silently,
built-ins are deduplicated, and `.env` is denylisted so config can never
register it (annotate copies file contents into the data dir).
Deliberately unchanged: the Pi plan-write allowlist (ALLOWED_PLAN_EXTENSIONS
in tool-scope.ts) and Edit Mode source save (SOURCE_SAVE_FILE_REGEX), which
keep their own narrower allowlists.
Review follow-ups on #1309: - deny the whole dotenv family (.prod.env, .env.local, ...) in normalizeMarkdownExtensions, not just the exact .env name - resolve config.json path per call instead of at module scope so PLANNOTATOR_DATA_DIR sandboxing works in single-process test runs - stop resolve-file.test.ts reading the real user config: pure predicate imports plus pinned empty extras on every resolve call - add the config.json -> memo -> predicate integration test using resetMarkdownExtensionsCache under a temp data dir
The read-only GET only probes the node runtime while Call flow is enabled. The stale-read test relied on earlier tests' settings POSTs leaking callFlow=true through the process-frozen config path; with lazy config resolution each sandbox is genuinely isolated, so the test now enables Call flow in its own data dir. Locally the dependency was masked by an fnm-shimmed sem sidecar spawning node coincidentally.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR: New config-only setting
markdownExtensionsin~/.plannotator/config.jsonlets extra file extensions (e.g. Livebook's.livemd) be annotated as Markdown. Closes #1307. AI-assisted.Summary
{ "markdownExtensions": [".livemd"] }in~/.plannotator/config.jsonaccepts the listed extensions anywhere annotate accepts.md: the annotate CLI and skills, folder file browser,/api/docand/api/doc/exists, frontmatter stripping, and wiki-link/relative-link navigation in annotate sessions.@plannotator/corestays browser-safe and zero-dep: the built-in lists became pure builders taking an optional extras list (default empty, byte-identical behavior). The config is read once per process through the existingloadConfig()path in a newpackages/shared/markdown-extensions.tsseam.vendor.sh), and the OpenCode command parser.[link](doc.livemd)and[[doc.livemd]]for configured extensions.Validation and security
.envis explicitly denylisted and cannot be registered (its exclusion from annotate is deliberate: annotate history copies file contents). Verified end to end with a config listing both.livemdand.env..md.Deliberately out of scope
ALLOWED_PLAN_EXTENSIONS) is unchanged; noted in AGENTS.md..md/.mdx/.txtonly; configured extras open read-annotate.Verification
bun run typecheckpassesPLANNOTATOR_DATA_DIR; none touch the real user configThanks @sgiath for the detailed report and for tracing both allowlists in #1307.