From 18b5430decdd93989688c12fe5ec133c15e71eff Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 4 May 2026 10:58:21 +0100 Subject: [PATCH] fix: resolve current Cursor acp-sessions//store.db layout (0.4.0) Cursor moved its ACP session storage from ~/.cursor/chats///store.db to a flat ~/.cursor/acp-sessions//store.db. findSessionStorePath only scanned the legacy chats/ tree, so every session created by current Cursor builds threw SessionNotFoundError and consumers (e.g. laze-ai) silently lost tool-call enrichment. Check the flat path first, fall back to the legacy hashed layout so pre-migration sessions keep resolving. Tests cover both layouts and the preference order. --- CHANGELOG.md | 12 +++++++++ README.md | 10 ++++++-- package-lock.json | 4 +-- package.json | 2 +- src/pathDiscovery.ts | 26 ++++++++++++++++--- test/pathDiscovery.test.ts | 52 +++++++++++++++++++++++++++++++------- 6 files changed, 88 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8099321..dca5a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.0] - 2026-05-04 + +### Fixed + +- `findSessionStorePath` now resolves Cursor's current on-disk layout — sessions are stored at `~/.cursor/acp-sessions//store.db`. Earlier versions only scanned the legacy `~/.cursor/chats///store.db` tree, which caused `SessionNotFoundError` for every session created by recent Cursor builds. The legacy layout is still scanned as a fallback so pre-migration sessions keep working. + +## [0.3.0] - 2026-04-04 + +### Added + +- `richResult: Record | null` field on `EnrichedToolCall` — surfaces Cursor's `providerOptions.cursor.highLevelToolCallResult` for tool-role blobs, exposing structured result data (e.g. `workspaceResults` for Read) in addition to the plain-string `result`. + ## [0.2.1] - 2026-04-02 ### Fixed diff --git a/README.md b/README.md index 634ae86..b4a89eb 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,19 @@ Recovers missing tool call arguments from Cursor's ACP (Agent Client Protocol) e Cursor's ACP stream emits `tool_call` notifications with an empty `rawInput` field — you can see _that_ a tool was called but not _what arguments_ it received (which file was read, which command was run, etc.). -Cursor does write the full tool call data — including arguments — to a local SQLite database at: +Cursor does write the full tool call data — including arguments — to a local SQLite database. Recent Cursor builds use a flat layout: + +``` +~/.cursor/acp-sessions//store.db +``` + +Older sessions still live under the legacy hashed layout, which this package also resolves transparently: ``` ~/.cursor/chats///store.db ``` -This package reads that database to recover the missing arguments, using the `toolCallId` as the join key between ACP events and store.db blobs. +This package reads whichever database is present for a given session and recovers the missing arguments, using the `toolCallId` as the join key between ACP events and store.db blobs. ## Installation diff --git a/package-lock.json b/package-lock.json index cfe08ac..5a4c07d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cursor-acp-enriched", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cursor-acp-enriched", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "better-sqlite3": "^12.8.0" diff --git a/package.json b/package.json index 41feef3..677c4ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cursor-acp-enriched", - "version": "0.3.0", + "version": "0.4.0", "description": "Recovers missing tool call arguments from Cursor's ACP events by reading the local SQLite store.db", "license": "MIT", "type": "module", diff --git a/src/pathDiscovery.ts b/src/pathDiscovery.ts index 9bfb5a6..6ebe7da 100644 --- a/src/pathDiscovery.ts +++ b/src/pathDiscovery.ts @@ -4,17 +4,35 @@ import { homedir } from 'node:os'; export class SessionNotFoundError extends Error { constructor(sessionId: string, cursorDir: string) { - super(`Session "${sessionId}" not found under ${cursorDir}/chats/`); + super( + `Session "${sessionId}" not found under ${cursorDir}/acp-sessions/ or ${cursorDir}/chats/`, + ); this.name = 'SessionNotFoundError'; } } -// Scans ~/.cursor/chats///store.db to locate the SQLite -// database for a given Cursor session. Returns the full path to store.db. +// Locates the SQLite store.db for a given Cursor ACP session. +// +// Cursor has used two on-disk layouts: +// +// 1. Current (Cursor ≥ mid-2026): flat +// ~/.cursor/acp-sessions//store.db +// +// 2. Legacy: hashed +// ~/.cursor/chats///store.db +// +// The current layout is checked first because it is a single existsSync() +// call. If it is missing, fall back to scanning the legacy chats/ tree so +// long-running clients can still enrich pre-migration sessions. export function findSessionStorePath(sessionId: string, options?: { cursorDir?: string }): string { const cursorDir = options?.cursorDir ?? join(homedir(), '.cursor'); - const chatsDir = join(cursorDir, 'chats'); + const flatPath = join(cursorDir, 'acp-sessions', sessionId, 'store.db'); + if (existsSync(flatPath)) { + return flatPath; + } + + const chatsDir = join(cursorDir, 'chats'); let hashDirs: string[]; try { hashDirs = readdirSync(chatsDir); diff --git a/test/pathDiscovery.test.ts b/test/pathDiscovery.test.ts index e618a5b..bf2e50d 100644 --- a/test/pathDiscovery.test.ts +++ b/test/pathDiscovery.test.ts @@ -8,31 +8,65 @@ import { findSessionStorePath, SessionNotFoundError } from '../src/pathDiscovery const __dirname = dirname(fileURLToPath(import.meta.url)); const FIXTURE_DB = join(__dirname, 'fixtures', 'store.db'); -function makeFakeCursorDir(sessionId: string): string { - const base = mkdtempSync(join(tmpdir(), 'cursor-test-')); +function makeLegacyCursorDir(sessionId: string): string { + const base = mkdtempSync(join(tmpdir(), 'cursor-legacy-')); const hashDir = join(base, 'chats', 'abc123def', sessionId); mkdirSync(hashDir, { recursive: true }); copyFileSync(FIXTURE_DB, join(hashDir, 'store.db')); return base; } +function makeFlatCursorDir(sessionId: string): string { + const base = mkdtempSync(join(tmpdir(), 'cursor-flat-')); + const sessionDir = join(base, 'acp-sessions', sessionId); + mkdirSync(sessionDir, { recursive: true }); + copyFileSync(FIXTURE_DB, join(sessionDir, 'store.db')); + return base; +} + +function makeBothLayoutsCursorDir(sessionId: string): string { + const base = mkdtempSync(join(tmpdir(), 'cursor-both-')); + const flatDir = join(base, 'acp-sessions', sessionId); + mkdirSync(flatDir, { recursive: true }); + copyFileSync(FIXTURE_DB, join(flatDir, 'store.db')); + const legacyDir = join(base, 'chats', 'abc123def', sessionId); + mkdirSync(legacyDir, { recursive: true }); + copyFileSync(FIXTURE_DB, join(legacyDir, 'store.db')); + return base; +} + describe('findSessionStorePath', () => { - it('returns the correct store.db path when session exists', () => { - const sessionId = 'test-session-abc'; - const cursorDir = makeFakeCursorDir(sessionId); + it('returns the flat acp-sessions path when only the new layout exists', () => { + const sessionId = 'test-session-flat'; + const cursorDir = makeFlatCursorDir(sessionId); + const result = findSessionStorePath(sessionId, { cursorDir }); + expect(result).toBe(join(cursorDir, 'acp-sessions', sessionId, 'store.db')); + }); + + it('returns the legacy chats path when only the old layout exists', () => { + const sessionId = 'test-session-legacy'; + const cursorDir = makeLegacyCursorDir(sessionId); const result = findSessionStorePath(sessionId, { cursorDir }); expect(result).toBe(join(cursorDir, 'chats', 'abc123def', sessionId, 'store.db')); }); - it('throws SessionNotFoundError when session is missing', () => { - const base = mkdtempSync(join(tmpdir(), 'cursor-test-')); + it('prefers the flat acp-sessions path when both layouts exist', () => { + const sessionId = 'test-session-both'; + const cursorDir = makeBothLayoutsCursorDir(sessionId); + const result = findSessionStorePath(sessionId, { cursorDir }); + expect(result).toBe(join(cursorDir, 'acp-sessions', sessionId, 'store.db')); + }); + + it('throws SessionNotFoundError when session is missing in both layouts', () => { + const base = mkdtempSync(join(tmpdir(), 'cursor-missing-')); mkdirSync(join(base, 'chats'), { recursive: true }); + mkdirSync(join(base, 'acp-sessions'), { recursive: true }); expect(() => findSessionStorePath('nonexistent-session', { cursorDir: base })).toThrow( SessionNotFoundError, ); }); - it('throws SessionNotFoundError when chats dir does not exist', () => { + it('throws SessionNotFoundError when neither chats nor acp-sessions exist', () => { const base = mkdtempSync(join(tmpdir(), 'cursor-empty-')); expect(() => findSessionStorePath('any-session', { cursorDir: base })).toThrow( SessionNotFoundError, @@ -40,7 +74,7 @@ describe('findSessionStorePath', () => { }); it('error message includes sessionId', () => { - const base = mkdtempSync(join(tmpdir(), 'cursor-test-')); + const base = mkdtempSync(join(tmpdir(), 'cursor-msg-')); mkdirSync(join(base, 'chats'), { recursive: true }); expect(() => findSessionStorePath('my-session-id', { cursorDir: base })).toThrow( /my-session-id/,