Skip to content

feat: add Cursor AI support with smart slug-to-path resolution#4

Merged
yigitkonur merged 1 commit intoyigitkonur:mainfrom
Evrim267:feat/cursor-support
Feb 20, 2026
Merged

feat: add Cursor AI support with smart slug-to-path resolution#4
yigitkonur merged 1 commit intoyigitkonur:mainfrom
Evrim267:feat/cursor-support

Conversation

@Evrim267
Copy link
Copy Markdown
Contributor

@Evrim267 Evrim267 commented Feb 20, 2026

Summary

Adds full Cursor AI support as the 7th platform in continues, enabling session handoffs to/from Cursor IDE agent sessions.

What's included

  • New parser (src/parsers/cursor.ts): Discovers and parses Cursor agent-transcript JSONL files from ~/.cursor/projects/
  • Smart slug-to-path resolution: Cursor replaces both / and . with - in project directory slugs (e.g. dzcm.testdzcm-test). Uses recursive backtracking to correctly resolve ambiguous paths by trying all three interpretations (/, ., -) at each dash position
  • Full tool extraction: Parses Anthropic-style tool_use/tool_result content blocks for file edits, shell commands, grep, glob, web fetch, MCP tools, and subagents
  • Thinking block extraction: Captures AI reasoning from thinking content blocks as session notes
  • User query cleaning: Strips Cursor's <user_query> wrapper tags from user messages
  • Resume support: Native resume opens project in Cursor; cross-tool handoff generates structured markdown
  • 42 test cases: All 7×6 cross-tool conversion paths tested and passing (was 30 for 6 tools)

Files changed (12 files)

File Change
src/parsers/cursor.ts New parser with parseCursorSessions() and extractCursorContext()
src/parsers/index.ts Barrel export
src/types/index.ts Added 'cursor' to SessionSource union
src/utils/index.ts Wired into buildIndex() and extractContext()
src/utils/markdown.ts Added 'Cursor AI' label
src/utils/resume.ts Added native + cross-tool resume commands
src/cli.ts Added color, quick-resume subcommand, updated help text
src/__tests__/fixtures/index.ts Added createCursorFixture()
src/__tests__/unit-conversions.test.ts Expanded to 42 conversion paths (7 tools)
src/__tests__/conversions.test.ts Added cursor entries
src/__tests__/e2e-conversions.test.ts Added cursor entries
src/__tests__/extract-handoffs.ts Added cursor entries

Slug resolution examples

Users-evolution-Sites-localhost-dzcm-test      → /Users/evolution/Sites/localhost/dzcm.test ✓
Users-evolution-Sites-localhost-readybyte-test  → /Users/evolution/Sites/localhost/readybyte.test ✓
Users-evolution-Desktop-Workspace-laravel-contentai → /Users/evolution/Desktop/Workspace/laravel/contentai ✓

Test plan

  • npx tsc compiles clean
  • All 111 unit tests pass (npx vitest run)
  • continues list --source cursor shows correct CWD paths
  • continues resume <id> --in claude generates valid handoff markdown
  • Tested with real Cursor agent-transcript data on macOS

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced path resolution to more reliably identify project directories, particularly those with complex naming patterns. Improved fallback handling ensures better accuracy across various naming conventions and edge cases.

Cursor replaces both `/` and `.` with `-` in project slugs, making
path reconstruction ambiguous. The previous greedy left-to-right
approach failed for names like `dzcm.test` (resolved as `dzcm/test`).

New approach uses recursive backtracking: at each dash position, tries
treating it as `/`, `.`, or literal `-`, and checks fs.existsSync only
on the final complete path. This correctly resolves all three cases:
- dzcm-test → dzcm.test
- readybyte-test → readybyte.test
- laravel-contentai → laravel/contentai
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

This change refactors the Cursor parser's project slug resolution logic from a greedy incremental filesystem scan to a recursive backtracking algorithm. The new approach explores multiple interpretations of dashes in slugs—as path separators, dots within segments, or literal dashes—and selects the first existing path on disk.

Changes

Cohort / File(s) Summary
Cursor Parser Logic
src/parsers/cursor.ts
Replaced greedy slug-to-path resolution with recursive backtracking search. Now explores three possibilities at each dash boundary and prioritizes existing filesystem paths over simple conversion fallback.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Why it matters (concrete impact):

  1. What breaks? — Path resolution for cursor projects could deviate from previous behavior. If existing code relies on specific slug-to-path mappings (e.g., my-feature-branch always resolving to my/feature/branch), the backtracking logic might resolve to my-feature/branch or my/feature-branch if those paths exist on disk. This changes determinism.

  2. Blast radius—really important?YES. Cursor resolution likely runs at startup to identify working directory context. If this breaks, it cascades to ANY command using cursor functionality. Projects with ambiguous slug patterns (multiple dashes that could be separators or literal) become unstable—different environments resolve differently.

  3. Other locations to look? — Check for:

    • Tests validating specific slug→path mappings (likely missing coverage for edge cases like foo-bar-baz where multiple valid paths exist)
    • How cursor slugs are generated vs. parsed (asymmetry could cause parse failures)
    • Projects with naming patterns using dashes heavily (e.g., feature-v2-hotfix)
  4. Startup-critical or money loss?STARTUP-VITAL. If cursor resolution fails at startup, the CLI can't identify project context. This blocks all operations.

Possibly related PRs

Poem

🌀 Dashes split three ways at a time,
Backtracking searches through the climb,
First match found takes the crown,
Greedy paths now tumbling down!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding Cursor AI platform support with smart slug-to-path resolution, which is the core feature of this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/parsers/cursor.ts (1)

388-388: ⚠️ Potential issue | 🟡 Minor

Missing guarantee: at least one user message in trimmed context.

  1. What breaks? If the last 10 messages are all assistant (long tool-call chains produce this), the handoff markdown has zero user context — the receiving tool has no idea what the user actually asked for.

  2. Blast radius? Every cross-tool handoff from a tool-heavy Cursor session. The user switches to Claude Code or Copilot and gets a context dump with no user intent.

  3. Startup-critical? It's user-facing — a broken handoff means the user has to re-explain everything manually. Defeats the purpose of continues.

Proposed fix
- const trimmed = recentMessages.slice(-10);
+ let trimmed = recentMessages.slice(-10);
+ if (!trimmed.some(m => m.role === 'user')) {
+   const lastUserIdx = recentMessages.findLastIndex(m => m.role === 'user');
+   if (lastUserIdx >= 0) {
+     trimmed = [recentMessages[lastUserIdx], ...trimmed.slice(1)];
+   }
+ }

As per coding guidelines, parsers must "Keep only the last ~10 messages in recentMessages for handoff while ensuring at least one user message is included."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/parsers/cursor.ts` at line 388, The current trim (const trimmed =
recentMessages.slice(-10)) can yield zero user messages; change the logic around
the trimmed variable so it always includes at least one user message when
possible: compute a start index = max(0, recentMessages.length - 10), take
trimmed = recentMessages.slice(start), and if trimmed has no message with role
=== 'user' then find lastUserIdx = last index in recentMessages with role ===
'user'; if found set start = max(0, lastUserIdx - 9) and recompute trimmed =
recentMessages.slice(start); if no user exists keep the original trimmed. Update
the code that uses recentMessages/trimmed in src/parsers/cursor.ts accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/parsers/cursor.ts`:
- Around line 41-75: cwdFromSlug currently explores three branches per dash via
the inner resolve function causing exponential fs.existsSync calls (and freezing
parseCursorSessions); fix it by adding a global candidate counter and hard cap
(e.g. MAX_CANDIDATES ~500) inside cwdFromSlug/resolve to immediately stop
recursion and return the fallback when the cap is reached, and also add a cheap
prefix-pruning check before descending (for non-empty segments, build the
candidate parent path and skip further recursion down that branch if no ancestor
prefix exists on disk) so resolve stops exploring impossible branches early;
update cwdFromSlug (and callers like parseCursorSessions) to rely on the
capped/short-circuit behavior.

---

Outside diff comments:
In `@src/parsers/cursor.ts`:
- Line 388: The current trim (const trimmed = recentMessages.slice(-10)) can
yield zero user messages; change the logic around the trimmed variable so it
always includes at least one user message when possible: compute a start index =
max(0, recentMessages.length - 10), take trimmed = recentMessages.slice(start),
and if trimmed has no message with role === 'user' then find lastUserIdx = last
index in recentMessages with role === 'user'; if found set start = max(0,
lastUserIdx - 9) and recompute trimmed = recentMessages.slice(start); if no user
exists keep the original trimmed. Update the code that uses
recentMessages/trimmed in src/parsers/cursor.ts accordingly.

Comment on lines 41 to 75
function cwdFromSlug(slug: string): string {
// First try a simple reconstruction that checks the filesystem
const parts = slug.split('-');
let resolved = '';

for (let i = 0; i < parts.length; i++) {
const candidate = resolved ? resolved + '/' + parts[i] : '/' + parts[i];
if (fs.existsSync(candidate)) {
resolved = candidate;
} else {
// The rest belongs to the current path segment (directory name with dashes)
const remainder = parts.slice(i).join('-');
resolved = resolved + '/' + remainder;
if (fs.existsSync(resolved)) break;
// If that doesn't exist either, just use the simple dash→slash conversion
resolved = '/' + slug.replace(/-/g, '/');
break;
let best: string | null = null;

function resolve(idx: number, segments: string[]): void {
if (best) return; // already found a match

if (idx >= parts.length) {
const p = '/' + segments.join('/');
if (fs.existsSync(p)) best = p;
return;
}

const part = parts[idx];

// Option 1: treat dash as path separator (new directory)
resolve(idx + 1, [...segments, part]);
if (best) return;

if (segments.length > 0) {
const last = segments[segments.length - 1];
const rest = segments.slice(0, -1);

// Option 2: treat dash as dot (e.g. dzcm-test → dzcm.test)
resolve(idx + 1, [...rest, last + '.' + part]);
if (best) return;

// Option 3: keep as literal dash (e.g. laravel-contentai)
resolve(idx + 1, [...rest, last + '-' + part]);
}
}

return resolved || '/' + slug.replace(/-/g, '/');
resolve(0, []);
return best || '/' + slug.replace(/-/g, '/');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Exponential blowup on unresolvable slugs — CLI will freeze on long paths.

  1. What breaks? The recursion branches 3 ways at each dash. For a slug with N parts, worst case visits 3^(N-1) leaf nodes, each calling fs.existsSync. A moderately deep path with embedded hyphens (e.g. /Users/john/my-projects/some-company/next-js-app) produces ~10+ parts → 19K+ existsSync calls. A path with 15 dashes → ~4.8M calls. The happy path short-circuits early, but the sad path (no match on disk — stale/deleted project) traverses the entire tree before falling back on line 74.

  2. Blast radius? parseCursorSessions() calls cwdFromSlug for every project slug. One stale project with a long slug hangs the entire session-listing CLI command. Users who deleted/moved a project will hit the worst case every time.

  3. Other locations? Only called from parseCursorSessions (line 212), so the blast is contained to Cursor session discovery — but that's on the critical path of cn startup.

  4. Startup-critical. This runs on every cn invocation that lists sessions. A multi-second freeze = users think the tool is broken.

Two concrete mitigations (pick one or both):

  • Cap recursion depth / candidate count — bail to fallback after e.g. 500 candidates explored.
  • Prune impossible branches — if segments already has N segments and no prefix of the path exists, skip deeper exploration (you're already halfway there with the "intermediate directories" comment, but no actual pruning is implemented).
Proposed fix: add a candidate cap
 function cwdFromSlug(slug: string): string {
   const parts = slug.split('-');
   let best: string | null = null;
+  let attempts = 0;
+  const MAX_ATTEMPTS = 500;

   function resolve(idx: number, segments: string[]): void {
-    if (best) return; // already found a match
+    if (best || attempts >= MAX_ATTEMPTS) return;

     if (idx >= parts.length) {
+      attempts++;
       const p = '/' + segments.join('/');
       if (fs.existsSync(p)) best = p;
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/parsers/cursor.ts` around lines 41 - 75, cwdFromSlug currently explores
three branches per dash via the inner resolve function causing exponential
fs.existsSync calls (and freezing parseCursorSessions); fix it by adding a
global candidate counter and hard cap (e.g. MAX_CANDIDATES ~500) inside
cwdFromSlug/resolve to immediately stop recursion and return the fallback when
the cap is reached, and also add a cheap prefix-pruning check before descending
(for non-empty segments, build the candidate parent path and skip further
recursion down that branch if no ancestor prefix exists on disk) so resolve
stops exploring impossible branches early; update cwdFromSlug (and callers like
parseCursorSessions) to rely on the capped/short-circuit behavior.

@yigitkonur yigitkonur merged commit 96bd8f0 into yigitkonur:main Feb 20, 2026
1 check passed
yigitkonur added a commit that referenced this pull request Feb 25, 2026
Rewrote the entire README to read like an actual developer wrote it:
- Punchier intro that leads with the problem, not a feature list
- Collapsed the 14×14 checkmark matrix into one sentence
- Cut 430→238 lines by removing redundancy and AI-doc patterns
- Added Community Contributions section referencing #1, #3, #4, #14
- Documented the 7 new agents (Amp, Kiro, Crush, Cline, Roo Code,
  Kilo Code, Antigravity) and bugs fixed in this round
- Prose over bullet spam, natural flow over exhaustive structure

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.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