Skip to content

feat: smart file resolution for plannotator annotate#237

Merged
backnotprop merged 2 commits intobacknotprop:mainfrom
dgrissen2:feat/smart-file-resolution
Mar 7, 2026
Merged

feat: smart file resolution for plannotator annotate#237
backnotprop merged 2 commits intobacknotprop:mainfrom
dgrissen2:feat/smart-file-resolution

Conversation

@dgrissen2
Copy link
Contributor

Summary

Adds case-insensitive file resolution to /plannotator-annotate so users can open markdown files without typing exact paths or full absolute paths.

Before: Only exact paths work. Typo in casing or forgetting the full path → File not found.

After: Three resolution strategies, tried in order:

Strategy Example input What happens
Exact path docs/guides/SETUP.md Opens directly (existing behavior, unchanged)
Case-insensitive relative docs/guides/setup.md Finds docs/guides/SETUP.md
Bare filename search unique-file.md Searches entire project, opens if exactly 1 match

Ambiguity handling

If a bare filename matches multiple files, the command errors with the full list so the user can pick the right one:

Ambiguous filename "SETUP.md" — found 2 matches:
  docs/guides/SETUP.md
  docs/api/SETUP.md

Ignored directories

Search skips: node_modules/, .git/, dist/, build/, .next/, __pycache__/, .obsidian/, .trash/

Security

  • Resolved paths must stay within the project root (no path traversal)
  • Only .md, .mdx, and .markdown files are matched

Implementation

Extracted a shared resolveMarkdownFile(input, projectRoot) function into packages/server/resolve-file.ts. This is now used by:

  1. plannotator annotate CLI (apps/hook/server/index.ts) — prints Resolved: /path on success, error with match list on ambiguity
  2. /api/doc endpoint (packages/server/reference-handlers.ts) — refactored to use the shared resolver, removing ~40 lines of inline resolution logic
  3. OpenCode plugin (apps/opencode-plugin/index.ts) — same smart resolution for OpenCode users

Files changed

File Change
packages/server/resolve-file.ts New — shared resolveMarkdownFile() function
packages/server/package.json Add ./resolve-file export
packages/server/reference-handlers.ts Refactor handleDoc() to use shared resolver
apps/hook/server/index.ts Annotate mode uses shared resolver
apps/opencode-plugin/index.ts Annotate mode uses shared resolver

Testing

Tested locally with the following scenarios:

  • Exact relative path (docs/guides/SETUP.md) → opens correctly
  • Case-insensitive relative path (docs/guides/setup.md) → resolves to SETUP.md
  • Bare filename, unique match (unique-file.md) → finds and opens the single match
  • Bare filename, duplicate (SETUP.md existing in two directories) → errors with both paths listed
  • Not found (nonexistent.md) → clean error message
  • Absolute path (existing behavior) → still works unchanged

🤖 Generated with Claude Code

Add case-insensitive file resolution so users can open markdown files
without typing exact paths. Applies to both the CLI (`plannotator
annotate`) and the `/api/doc` endpoint for linked documents.

Resolution strategies (tried in order):
1. Exact path — absolute or relative to cwd (existing behavior)
2. Case-insensitive relative path — `docs/setup.md` matches `docs/SETUP.md`
3. Bare filename search — `setup.md` searches the entire project tree

Ambiguity handling:
- 1 match → opens the file, prints "Resolved: /full/path"
- 0 matches → "File not found: <input>"
- 2+ matches → "Ambiguous filename: found N matches" with full paths

Skips node_modules, .git, dist, build, .next, __pycache__, .obsidian,
.trash during search. Restricts results to .md/.mdx/.markdown files
and enforces project root boundary (no path traversal).

Extracted shared resolveMarkdownFile() into packages/server/resolve-file.ts
and refactored /api/doc handler to use it, removing ~40 lines of inline
resolution logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@backnotprop
Copy link
Owner

Code review

Found 1 issue:

  1. Regression: reverts Windows path fix from PR fix(annotate): resolve file paths correctly on Windows #233. PR fix(annotate): resolve file paths correctly on Windows #233 (commit c762bbe) added @ prefix stripping for Claude Code file references, PLANNOTATOR_CWD env var support for correct path resolution when the script's CWD differs from the caller's, and debug logging. This PR replaces the entire annotate path resolution block with resolveMarkdownFile(), which handles none of these. Merging this will break the annotate command on Windows and for any user whose agent passes @-prefixed paths. The new code uses process.cwd() directly instead of checking PLANNOTATOR_CWD, and filePath is passed straight to resolveMarkdownFile without stripping the @ prefix.

// Smart file resolution: exact path, case-insensitive relative, or bare filename search
const projectRoot = process.cwd();
const resolved = await resolveMarkdownFile(filePath, projectRoot);

The fix from #233 that this reverts:

// Strip @ prefix if present (Claude Code file reference syntax)
if (filePath.startsWith("@")) {
filePath = filePath.slice(1);
}
// Resolve path - use PLANNOTATOR_CWD if set (original working directory before script cd'd)
const originalCwd = process.env.PLANNOTATOR_CWD || process.cwd();
// Debug: log path resolution (visible in stderr, won't interfere with stdout JSON)
if (process.env.PLANNOTATOR_DEBUG) {
console.error(`[DEBUG] Original CWD: ${originalCwd}`);
console.error(`[DEBUG] File path arg: ${filePath}`);
}
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.join(originalCwd, filePath);
if (process.env.PLANNOTATOR_DEBUG) {
console.error(`[DEBUG] Resolved path: ${absolutePath}`);

Note: the PR branch also appears to be based on a pre-#233 version of apps/hook/server/index.ts, so there will be a merge conflict that needs to be resolved by incorporating the @ stripping and PLANNOTATOR_CWD logic into the new resolveMarkdownFile() call site (or into the function itself).

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…ry and extension mismatch

- Resolve merge conflict with backnotprop#233: keep @ prefix stripping and
  PLANNOTATOR_CWD support before calling resolveMarkdownFile()
- Fix dead ternary in resolve-file.ts: bare filenames glob **/name,
  relative paths with dirs glob the pattern directly
- Align extension regex with rest of codebase (.md/.mdx only, not .markdown)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@backnotprop backnotprop merged commit 026194a into backnotprop:main Mar 7, 2026
3 checks passed
dgrissen2 added a commit to dgrissen2/plannotator_ext that referenced this pull request Mar 9, 2026
…esolution

Add capture-phase paste listener to AttachmentsButton so users can
Cmd+V / Ctrl+V images directly into per-annotation attachments when
the popover is open. Uses stopPropagation to prevent the global paste
handler in App.tsx from also processing the event. Adds a platform-aware
keyboard shortcut hint in the drop zone.

Also fixes a regression from backnotprop#237 where resolveMarkdownFile rejected
absolute paths outside the project root. The original behavior allowed
any absolute path the user explicitly provided; the security boundary
should only apply to relative path / bare filename search.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
backnotprop pushed a commit that referenced this pull request Mar 9, 2026
…esolution (#255)

Add capture-phase paste listener to AttachmentsButton so users can
Cmd+V / Ctrl+V images directly into per-annotation attachments when
the popover is open. Uses stopPropagation to prevent the global paste
handler in App.tsx from also processing the event. Adds a platform-aware
keyboard shortcut hint in the drop zone.

Also fixes a regression from #237 where resolveMarkdownFile rejected
absolute paths outside the project root. The original behavior allowed
any absolute path the user explicitly provided; the security boundary
should only apply to relative path / bare filename search.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants