feat: add Cursor AI support with smart slug-to-path resolution#4
Conversation
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
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Why it matters (concrete impact):
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟡 MinorMissing guarantee: at least one user message in trimmed context.
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.
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.
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.
| 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, '/'); | ||
| } |
There was a problem hiding this comment.
Exponential blowup on unresolvable slugs — CLI will freeze on long paths.
-
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+existsSynccalls. 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. -
Blast radius?
parseCursorSessions()callscwdFromSlugfor 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. -
Other locations? Only called from
parseCursorSessions(line 212), so the blast is contained to Cursor session discovery — but that's on the critical path ofcnstartup. -
Startup-critical. This runs on every
cninvocation 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
segmentsalready 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.
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>
Summary
Adds full Cursor AI support as the 7th platform in
continues, enabling session handoffs to/from Cursor IDE agent sessions.What's included
src/parsers/cursor.ts): Discovers and parses Cursor agent-transcript JSONL files from~/.cursor/projects//and.with-in project directory slugs (e.g.dzcm.test→dzcm-test). Uses recursive backtracking to correctly resolve ambiguous paths by trying all three interpretations (/,.,-) at each dash positiontool_use/tool_resultcontent blocks for file edits, shell commands, grep, glob, web fetch, MCP tools, and subagentsthinkingcontent blocks as session notes<user_query>wrapper tags from user messagesFiles changed (12 files)
src/parsers/cursor.tsparseCursorSessions()andextractCursorContext()src/parsers/index.tssrc/types/index.ts'cursor'toSessionSourceunionsrc/utils/index.tsbuildIndex()andextractContext()src/utils/markdown.ts'Cursor AI'labelsrc/utils/resume.tssrc/cli.tssrc/__tests__/fixtures/index.tscreateCursorFixture()src/__tests__/unit-conversions.test.tssrc/__tests__/conversions.test.tssrc/__tests__/e2e-conversions.test.tssrc/__tests__/extract-handoffs.tsSlug resolution examples
Test plan
npx tsccompiles cleannpx vitest run)continues list --source cursorshows correct CWD pathscontinues resume <id> --in claudegenerates valid handoff markdownSummary by CodeRabbit