#
tokens: 46258/50000 3/44 files (page 3/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 3 of 3. Use http://codebase.md/vltansky/cursor-chat-history-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   ├── mcp.json
│   └── rules
│       ├── cursor_rules.mdc
│       ├── dev_workflow.mdc
│       ├── general.mdc
│       ├── mcp.mdc
│       ├── project-overview.mdc
│       ├── self_improve.mdc
│       ├── taskmaster.mdc
│       ├── tests.mdc
│       └── typescript-patterns.mdc
├── .github
│   ├── dependabot.yml
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .roo
│   ├── rules
│   │   ├── dev_workflow.md
│   │   ├── roo_rules.md
│   │   ├── self_improve.md
│   │   └── taskmaster.md
│   ├── rules-architect
│   │   └── architect-rules
│   ├── rules-ask
│   │   └── ask-rules
│   ├── rules-boomerang
│   │   └── boomerang-rules
│   ├── rules-code
│   │   └── code-rules
│   ├── rules-debug
│   │   └── debug-rules
│   └── rules-test
│       └── test-rules
├── .roomodes
├── .taskmaster
│   ├── .taskmaster
│   │   └── config.json
│   ├── config.json
│   └── reports
│       └── task-complexity-report.json
├── .taskmasterconfig
├── .windsurfrules
├── docs
│   ├── research.md
│   └── use-cases.md
├── LICENSE
├── package.json
├── README.md
├── scripts
│   └── example_prd.txt
├── src
│   ├── database
│   │   ├── parser.test.ts
│   │   ├── parser.ts
│   │   ├── reader.test.ts
│   │   ├── reader.ts
│   │   └── types.ts
│   ├── server.test.ts
│   ├── server.ts
│   ├── tools
│   │   ├── analytics-tools.ts
│   │   ├── conversation-tools.test.ts
│   │   ├── conversation-tools.ts
│   │   └── extraction-tools.ts
│   └── utils
│       ├── analytics.ts
│       ├── cache.test.ts
│       ├── cache.ts
│       ├── database-utils.test.ts
│       ├── database-utils.ts
│       ├── errors.test.ts
│       ├── errors.ts
│       ├── exporters.ts
│       ├── formatter.ts
│       ├── relationships.ts
│       ├── validation.test.ts
│       └── validation.ts
├── tsconfig.json
├── vitest.config.ts
└── yarn.lock
```

# Files

--------------------------------------------------------------------------------
/.roo/rules/taskmaster.md:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | description: Comprehensive reference for Taskmaster MCP tools and CLI commands.
  3 | globs: **/*
  4 | alwaysApply: true
  5 | ---
  6 | # Taskmaster Tool & Command Reference
  7 | 
  8 | This document provides a detailed reference for interacting with Taskmaster, covering both the recommended MCP tools, suitable for integrations like Roo Code, and the corresponding `task-master` CLI commands, designed for direct user interaction or fallback.
  9 | 
 10 | **Note:** For interacting with Taskmaster programmatically or via integrated tools, using the **MCP tools is strongly recommended** due to better performance, structured data, and error handling. The CLI commands serve as a user-friendly alternative and fallback. 
 11 | 
 12 | **Important:** Several MCP tools involve AI processing... The AI-powered tools include `parse_prd`, `analyze_project_complexity`, `update_subtask`, `update_task`, `update`, `expand_all`, `expand_task`, and `add_task`.
 13 | 
 14 | ---
 15 | 
 16 | ## Initialization & Setup
 17 | 
 18 | ### 1. Initialize Project (`init`)
 19 | 
 20 | *   **MCP Tool:** `initialize_project`
 21 | *   **CLI Command:** `task-master init [options]`
 22 | *   **Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project.`
 23 | *   **Key CLI Options:**
 24 |     *   `--name <name>`: `Set the name for your project in Taskmaster's configuration.`
 25 |     *   `--description <text>`: `Provide a brief description for your project.`
 26 |     *   `--version <version>`: `Set the initial version for your project, e.g., '0.1.0'.`
 27 |     *   `-y, --yes`: `Initialize Taskmaster quickly using default settings without interactive prompts.`
 28 | *   **Usage:** Run this once at the beginning of a new project.
 29 | *   **MCP Variant Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project by running the 'task-master init' command.`
 30 | *   **Key MCP Parameters/Options:**
 31 |     *   `projectName`: `Set the name for your project.` (CLI: `--name <name>`)
 32 |     *   `projectDescription`: `Provide a brief description for your project.` (CLI: `--description <text>`)
 33 |     *   `projectVersion`: `Set the initial version for your project, e.g., '0.1.0'.` (CLI: `--version <version>`)
 34 |     *   `authorName`: `Author name.` (CLI: `--author <author>`)
 35 |     *   `skipInstall`: `Skip installing dependencies. Default is false.` (CLI: `--skip-install`)
 36 |     *   `addAliases`: `Add shell aliases tm and taskmaster. Default is false.` (CLI: `--aliases`)
 37 |     *   `yes`: `Skip prompts and use defaults/provided arguments. Default is false.` (CLI: `-y, --yes`)
 38 | *   **Usage:** Run this once at the beginning of a new project, typically via an integrated tool like Roo Code. Operates on the current working directory of the MCP server. 
 39 | *   **Important:** Once complete, you *MUST* parse a prd in order to generate tasks. There will be no tasks files until then. The next step after initializing should be to create a PRD using the example PRD in scripts/example_prd.txt. 
 40 | 
 41 | ### 2. Parse PRD (`parse_prd`)
 42 | 
 43 | *   **MCP Tool:** `parse_prd`
 44 | *   **CLI Command:** `task-master parse-prd [file] [options]`
 45 | *   **Description:** `Parse a Product Requirements Document, PRD, or text file with Taskmaster to automatically generate an initial set of tasks in tasks.json.`
 46 | *   **Key Parameters/Options:**
 47 |     *   `input`: `Path to your PRD or requirements text file that Taskmaster should parse for tasks.` (CLI: `[file]` positional or `-i, --input <file>`)
 48 |     *   `output`: `Specify where Taskmaster should save the generated 'tasks.json' file. Defaults to 'tasks/tasks.json'.` (CLI: `-o, --output <file>`)
 49 |     *   `numTasks`: `Approximate number of top-level tasks Taskmaster should aim to generate from the document.` (CLI: `-n, --num-tasks <number>`)
 50 |     *   `force`: `Use this to allow Taskmaster to overwrite an existing 'tasks.json' without asking for confirmation.` (CLI: `-f, --force`)
 51 | *   **Usage:** Useful for bootstrapping a project from an existing requirements document.
 52 | *   **Notes:** Task Master will strictly adhere to any specific requirements mentioned in the PRD, such as libraries, database schemas, frameworks, tech stacks, etc., while filling in any gaps where the PRD isn't fully specified. Tasks are designed to provide the most direct implementation path while avoiding over-engineering.
 53 | *   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. If the user does not have a PRD, suggest discussing their idea and then use the example PRD in `scripts/example_prd.txt` as a template for creating the PRD based on their idea, for use with `parse-prd`.
 54 | 
 55 | ---
 56 | 
 57 | ## AI Model Configuration
 58 | 
 59 | ### 2. Manage Models (`models`)
 60 | *   **MCP Tool:** `models`
 61 | *   **CLI Command:** `task-master models [options]`
 62 | *   **Description:** `View the current AI model configuration or set specific models for different roles (main, research, fallback). Allows setting custom model IDs for Ollama and OpenRouter.`
 63 | *   **Key MCP Parameters/Options:**
 64 |     *   `setMain <model_id>`: `Set the primary model ID for task generation/updates.` (CLI: `--set-main <model_id>`)
 65 |     *   `setResearch <model_id>`: `Set the model ID for research-backed operations.` (CLI: `--set-research <model_id>`)
 66 |     *   `setFallback <model_id>`: `Set the model ID to use if the primary fails.` (CLI: `--set-fallback <model_id>`)
 67 |     *   `ollama <boolean>`: `Indicates the set model ID is a custom Ollama model.` (CLI: `--ollama`)
 68 |     *   `openrouter <boolean>`: `Indicates the set model ID is a custom OpenRouter model.` (CLI: `--openrouter`)
 69 |     *   `listAvailableModels <boolean>`: `If true, lists available models not currently assigned to a role.` (CLI: No direct equivalent; CLI lists available automatically)
 70 |     *   `projectRoot <string>`: `Optional. Absolute path to the project root directory.` (CLI: Determined automatically)
 71 | *   **Key CLI Options:**
 72 |     *   `--set-main <model_id>`: `Set the primary model.`
 73 |     *   `--set-research <model_id>`: `Set the research model.`
 74 |     *   `--set-fallback <model_id>`: `Set the fallback model.`
 75 |     *   `--ollama`: `Specify that the provided model ID is for Ollama (use with --set-*).`
 76 |     *   `--openrouter`: `Specify that the provided model ID is for OpenRouter (use with --set-*). Validates against OpenRouter API.`
 77 |     *   `--setup`: `Run interactive setup to configure models, including custom Ollama/OpenRouter IDs.`
 78 | *   **Usage (MCP):** Call without set flags to get current config. Use `setMain`, `setResearch`, or `setFallback` with a valid model ID to update the configuration. Use `listAvailableModels: true` to get a list of unassigned models. To set a custom model, provide the model ID and set `ollama: true` or `openrouter: true`.
 79 | *   **Usage (CLI):** Run without flags to view current configuration and available models. Use set flags to update specific roles. Use `--setup` for guided configuration, including custom models. To set a custom model via flags, use `--set-<role>=<model_id>` along with either `--ollama` or `--openrouter`.
 80 | *   **Notes:** Configuration is stored in `.taskmasterconfig` in the project root. This command/tool modifies that file. Use `listAvailableModels` or `task-master models` to see internally supported models. OpenRouter custom models are validated against their live API. Ollama custom models are not validated live.
 81 | *   **API note:** API keys for selected AI providers (based on their model) need to exist in the mcp.json file to be accessible in MCP context. The API keys must be present in the local .env file for the CLI to be able to read them.
 82 | *   **Model costs:** The costs in supported models are expressed in dollars. An input/output value of 3 is $3.00. A value of 0.8 is $0.80. 
 83 | *   **Warning:** DO NOT MANUALLY EDIT THE .taskmasterconfig FILE. Use the included commands either in the MCP or CLI format as needed. Always prioritize MCP tools when available and use the CLI as a fallback.
 84 | 
 85 | ---
 86 | 
 87 | ## Task Listing & Viewing
 88 | 
 89 | ### 3. Get Tasks (`get_tasks`)
 90 | 
 91 | *   **MCP Tool:** `get_tasks`
 92 | *   **CLI Command:** `task-master list [options]`
 93 | *   **Description:** `List your Taskmaster tasks, optionally filtering by status and showing subtasks.`
 94 | *   **Key Parameters/Options:**
 95 |     *   `status`: `Show only Taskmaster tasks matching this status, e.g., 'pending' or 'done'.` (CLI: `-s, --status <status>`)
 96 |     *   `withSubtasks`: `Include subtasks indented under their parent tasks in the list.` (CLI: `--with-subtasks`)
 97 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
 98 | *   **Usage:** Get an overview of the project status, often used at the start of a work session.
 99 | 
100 | ### 4. Get Next Task (`next_task`)
101 | 
102 | *   **MCP Tool:** `next_task`
103 | *   **CLI Command:** `task-master next [options]`
104 | *   **Description:** `Ask Taskmaster to show the next available task you can work on, based on status and completed dependencies.`
105 | *   **Key Parameters/Options:**
106 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
107 | *   **Usage:** Identify what to work on next according to the plan.
108 | 
109 | ### 5. Get Task Details (`get_task`)
110 | 
111 | *   **MCP Tool:** `get_task`
112 | *   **CLI Command:** `task-master show [id] [options]`
113 | *   **Description:** `Display detailed information for a specific Taskmaster task or subtask by its ID.`
114 | *   **Key Parameters/Options:**
115 |     *   `id`: `Required. The ID of the Taskmaster task, e.g., '15', or subtask, e.g., '15.2', you want to view.` (CLI: `[id]` positional or `-i, --id <id>`)
116 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
117 | *   **Usage:** Understand the full details, implementation notes, and test strategy for a specific task before starting work.
118 | 
119 | ---
120 | 
121 | ## Task Creation & Modification
122 | 
123 | ### 6. Add Task (`add_task`)
124 | 
125 | *   **MCP Tool:** `add_task`
126 | *   **CLI Command:** `task-master add-task [options]`
127 | *   **Description:** `Add a new task to Taskmaster by describing it; AI will structure it.`
128 | *   **Key Parameters/Options:**
129 |     *   `prompt`: `Required. Describe the new task you want Taskmaster to create, e.g., "Implement user authentication using JWT".` (CLI: `-p, --prompt <text>`)
130 |     *   `dependencies`: `Specify the IDs of any Taskmaster tasks that must be completed before this new one can start, e.g., '12,14'.` (CLI: `-d, --dependencies <ids>`)
131 |     *   `priority`: `Set the priority for the new task: 'high', 'medium', or 'low'. Default is 'medium'.` (CLI: `--priority <priority>`)
132 |     *   `research`: `Enable Taskmaster to use the research role for potentially more informed task creation.` (CLI: `-r, --research`)
133 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
134 | *   **Usage:** Quickly add newly identified tasks during development.
135 | *   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
136 | 
137 | ### 7. Add Subtask (`add_subtask`)
138 | 
139 | *   **MCP Tool:** `add_subtask`
140 | *   **CLI Command:** `task-master add-subtask [options]`
141 | *   **Description:** `Add a new subtask to a Taskmaster parent task, or convert an existing task into a subtask.`
142 | *   **Key Parameters/Options:**
143 |     *   `id` / `parent`: `Required. The ID of the Taskmaster task that will be the parent.` (MCP: `id`, CLI: `-p, --parent <id>`)
144 |     *   `taskId`: `Use this if you want to convert an existing top-level Taskmaster task into a subtask of the specified parent.` (CLI: `-i, --task-id <id>`)
145 |     *   `title`: `Required if not using taskId. The title for the new subtask Taskmaster should create.` (CLI: `-t, --title <title>`)
146 |     *   `description`: `A brief description for the new subtask.` (CLI: `-d, --description <text>`)
147 |     *   `details`: `Provide implementation notes or details for the new subtask.` (CLI: `--details <text>`)
148 |     *   `dependencies`: `Specify IDs of other tasks or subtasks, e.g., '15' or '16.1', that must be done before this new subtask.` (CLI: `--dependencies <ids>`)
149 |     *   `status`: `Set the initial status for the new subtask. Default is 'pending'.` (CLI: `-s, --status <status>`)
150 |     *   `skipGenerate`: `Prevent Taskmaster from automatically regenerating markdown task files after adding the subtask.` (CLI: `--skip-generate`)
151 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
152 | *   **Usage:** Break down tasks manually or reorganize existing tasks.
153 | 
154 | ### 8. Update Tasks (`update`)
155 | 
156 | *   **MCP Tool:** `update`
157 | *   **CLI Command:** `task-master update [options]`
158 | *   **Description:** `Update multiple upcoming tasks in Taskmaster based on new context or changes, starting from a specific task ID.`
159 | *   **Key Parameters/Options:**
160 |     *   `from`: `Required. The ID of the first task Taskmaster should update. All tasks with this ID or higher that are not 'done' will be considered.` (CLI: `--from <id>`)
161 |     *   `prompt`: `Required. Explain the change or new context for Taskmaster to apply to the tasks, e.g., "We are now using React Query instead of Redux Toolkit for data fetching".` (CLI: `-p, --prompt <text>`)
162 |     *   `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
163 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
164 | *   **Usage:** Handle significant implementation changes or pivots that affect multiple future tasks. Example CLI: `task-master update --from='18' --prompt='Switching to React Query.\nNeed to refactor data fetching...'`
165 | *   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
166 | 
167 | ### 9. Update Task (`update_task`)
168 | 
169 | *   **MCP Tool:** `update_task`
170 | *   **CLI Command:** `task-master update-task [options]`
171 | *   **Description:** `Modify a specific Taskmaster task or subtask by its ID, incorporating new information or changes.`
172 | *   **Key Parameters/Options:**
173 |     *   `id`: `Required. The specific ID of the Taskmaster task, e.g., '15', or subtask, e.g., '15.2', you want to update.` (CLI: `-i, --id <id>`)
174 |     *   `prompt`: `Required. Explain the specific changes or provide the new information Taskmaster should incorporate into this task.` (CLI: `-p, --prompt <text>`)
175 |     *   `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
176 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
177 | *   **Usage:** Refine a specific task based on new understanding or feedback. Example CLI: `task-master update-task --id='15' --prompt='Clarification: Use PostgreSQL instead of MySQL.\nUpdate schema details...'`
178 | *   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
179 | 
180 | ### 10. Update Subtask (`update_subtask`)
181 | 
182 | *   **MCP Tool:** `update_subtask`
183 | *   **CLI Command:** `task-master update-subtask [options]`
184 | *   **Description:** `Append timestamped notes or details to a specific Taskmaster subtask without overwriting existing content. Intended for iterative implementation logging.`
185 | *   **Key Parameters/Options:**
186 |     *   `id`: `Required. The specific ID of the Taskmaster subtask, e.g., '15.2', you want to add information to.` (CLI: `-i, --id <id>`)
187 |     *   `prompt`: `Required. Provide the information or notes Taskmaster should append to the subtask's details. Ensure this adds *new* information not already present.` (CLI: `-p, --prompt <text>`)
188 |     *   `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
189 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
190 | *   **Usage:** Add implementation notes, code snippets, or clarifications to a subtask during development. Before calling, review the subtask's current details to append only fresh insights, helping to build a detailed log of the implementation journey and avoid redundancy. Example CLI: `task-master update-subtask --id='15.2' --prompt='Discovered that the API requires header X.\nImplementation needs adjustment...'`
191 | *   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
192 | 
193 | ### 11. Set Task Status (`set_task_status`)
194 | 
195 | *   **MCP Tool:** `set_task_status`
196 | *   **CLI Command:** `task-master set-status [options]`
197 | *   **Description:** `Update the status of one or more Taskmaster tasks or subtasks, e.g., 'pending', 'in-progress', 'done'.`
198 | *   **Key Parameters/Options:**
199 |     *   `id`: `Required. The ID(s) of the Taskmaster task(s) or subtask(s), e.g., '15', '15.2', or '16,17.1', to update.` (CLI: `-i, --id <id>`)
200 |     *   `status`: `Required. The new status to set, e.g., 'done', 'pending', 'in-progress', 'review', 'cancelled'.` (CLI: `-s, --status <status>`)
201 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
202 | *   **Usage:** Mark progress as tasks move through the development cycle.
203 | 
204 | ### 12. Remove Task (`remove_task`)
205 | 
206 | *   **MCP Tool:** `remove_task`
207 | *   **CLI Command:** `task-master remove-task [options]`
208 | *   **Description:** `Permanently remove a task or subtask from the Taskmaster tasks list.`
209 | *   **Key Parameters/Options:**
210 |     *   `id`: `Required. The ID of the Taskmaster task, e.g., '5', or subtask, e.g., '5.2', to permanently remove.` (CLI: `-i, --id <id>`)
211 |     *   `yes`: `Skip the confirmation prompt and immediately delete the task.` (CLI: `-y, --yes`)
212 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
213 | *   **Usage:** Permanently delete tasks or subtasks that are no longer needed in the project.
214 | *   **Notes:** Use with caution as this operation cannot be undone. Consider using 'blocked', 'cancelled', or 'deferred' status instead if you just want to exclude a task from active planning but keep it for reference. The command automatically cleans up dependency references in other tasks.
215 | 
216 | ---
217 | 
218 | ## Task Structure & Breakdown
219 | 
220 | ### 13. Expand Task (`expand_task`)
221 | 
222 | *   **MCP Tool:** `expand_task`
223 | *   **CLI Command:** `task-master expand [options]`
224 | *   **Description:** `Use Taskmaster's AI to break down a complex task into smaller, manageable subtasks. Appends subtasks by default.`
225 | *   **Key Parameters/Options:**
226 |     *   `id`: `The ID of the specific Taskmaster task you want to break down into subtasks.` (CLI: `-i, --id <id>`)
227 |     *   `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create. Uses complexity analysis/defaults otherwise.` (CLI: `-n, --num <number>`)
228 |     *   `research`: `Enable Taskmaster to use the research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`)
229 |     *   `prompt`: `Optional: Provide extra context or specific instructions to Taskmaster for generating the subtasks.` (CLI: `-p, --prompt <text>`)
230 |     *   `force`: `Optional: If true, clear existing subtasks before generating new ones. Default is false (append).` (CLI: `--force`)
231 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
232 | *   **Usage:** Generate a detailed implementation plan for a complex task before starting coding. Automatically uses complexity report recommendations if available and `num` is not specified.
233 | *   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
234 | 
235 | ### 14. Expand All Tasks (`expand_all`)
236 | 
237 | *   **MCP Tool:** `expand_all`
238 | *   **CLI Command:** `task-master expand --all [options]` (Note: CLI uses the `expand` command with the `--all` flag)
239 | *   **Description:** `Tell Taskmaster to automatically expand all eligible pending/in-progress tasks based on complexity analysis or defaults. Appends subtasks by default.`
240 | *   **Key Parameters/Options:**
241 |     *   `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create per task.` (CLI: `-n, --num <number>`)
242 |     *   `research`: `Enable research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`)
243 |     *   `prompt`: `Optional: Provide extra context for Taskmaster to apply generally during expansion.` (CLI: `-p, --prompt <text>`)
244 |     *   `force`: `Optional: If true, clear existing subtasks before generating new ones for each eligible task. Default is false (append).` (CLI: `--force`)
245 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
246 | *   **Usage:** Useful after initial task generation or complexity analysis to break down multiple tasks at once.
247 | *   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
248 | 
249 | ### 15. Clear Subtasks (`clear_subtasks`)
250 | 
251 | *   **MCP Tool:** `clear_subtasks`
252 | *   **CLI Command:** `task-master clear-subtasks [options]`
253 | *   **Description:** `Remove all subtasks from one or more specified Taskmaster parent tasks.`
254 | *   **Key Parameters/Options:**
255 |     *   `id`: `The ID(s) of the Taskmaster parent task(s) whose subtasks you want to remove, e.g., '15' or '16,18'. Required unless using `all`.) (CLI: `-i, --id <ids>`)
256 |     *   `all`: `Tell Taskmaster to remove subtasks from all parent tasks.` (CLI: `--all`)
257 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
258 | *   **Usage:** Used before regenerating subtasks with `expand_task` if the previous breakdown needs replacement.
259 | 
260 | ### 16. Remove Subtask (`remove_subtask`)
261 | 
262 | *   **MCP Tool:** `remove_subtask`
263 | *   **CLI Command:** `task-master remove-subtask [options]`
264 | *   **Description:** `Remove a subtask from its Taskmaster parent, optionally converting it into a standalone task.`
265 | *   **Key Parameters/Options:**
266 |     *   `id`: `Required. The ID(s) of the Taskmaster subtask(s) to remove, e.g., '15.2' or '16.1,16.3'.` (CLI: `-i, --id <id>`)
267 |     *   `convert`: `If used, Taskmaster will turn the subtask into a regular top-level task instead of deleting it.` (CLI: `-c, --convert`)
268 |     *   `skipGenerate`: `Prevent Taskmaster from automatically regenerating markdown task files after removing the subtask.` (CLI: `--skip-generate`)
269 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
270 | *   **Usage:** Delete unnecessary subtasks or promote a subtask to a top-level task.
271 | 
272 | ### 17. Move Task (`move_task`)
273 | 
274 | *   **MCP Tool:** `move_task`
275 | *   **CLI Command:** `task-master move [options]`
276 | *   **Description:** `Move a task or subtask to a new position within the task hierarchy.`
277 | *   **Key Parameters/Options:**
278 |     *   `from`: `Required. ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated for multiple tasks.` (CLI: `--from <id>`)
279 |     *   `to`: `Required. ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated.` (CLI: `--to <id>`)
280 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
281 | *   **Usage:** Reorganize tasks by moving them within the hierarchy. Supports various scenarios like:
282 |     *   Moving a task to become a subtask
283 |     *   Moving a subtask to become a standalone task
284 |     *   Moving a subtask to a different parent
285 |     *   Reordering subtasks within the same parent
286 |     *   Moving a task to a new, non-existent ID (automatically creates placeholders)
287 |     *   Moving multiple tasks at once with comma-separated IDs
288 | *   **Validation Features:**
289 |     *   Allows moving tasks to non-existent destination IDs (creates placeholder tasks)
290 |     *   Prevents moving to existing task IDs that already have content (to avoid overwriting)
291 |     *   Validates that source tasks exist before attempting to move them
292 |     *   Maintains proper parent-child relationships
293 | *   **Example CLI:** `task-master move --from=5.2 --to=7.3` to move subtask 5.2 to become subtask 7.3.
294 | *   **Example Multi-Move:** `task-master move --from=10,11,12 --to=16,17,18` to move multiple tasks to new positions.
295 | *   **Common Use:** Resolving merge conflicts in tasks.json when multiple team members create tasks on different branches.
296 | 
297 | ---
298 | 
299 | ## Dependency Management
300 | 
301 | ### 18. Add Dependency (`add_dependency`)
302 | 
303 | *   **MCP Tool:** `add_dependency`
304 | *   **CLI Command:** `task-master add-dependency [options]`
305 | *   **Description:** `Define a dependency in Taskmaster, making one task a prerequisite for another.`
306 | *   **Key Parameters/Options:**
307 |     *   `id`: `Required. The ID of the Taskmaster task that will depend on another.` (CLI: `-i, --id <id>`)
308 |     *   `dependsOn`: `Required. The ID of the Taskmaster task that must be completed first, the prerequisite.` (CLI: `-d, --depends-on <id>`)
309 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <path>`)
310 | *   **Usage:** Establish the correct order of execution between tasks.
311 | 
312 | ### 19. Remove Dependency (`remove_dependency`)
313 | 
314 | *   **MCP Tool:** `remove_dependency`
315 | *   **CLI Command:** `task-master remove-dependency [options]`
316 | *   **Description:** `Remove a dependency relationship between two Taskmaster tasks.`
317 | *   **Key Parameters/Options:**
318 |     *   `id`: `Required. The ID of the Taskmaster task you want to remove a prerequisite from.` (CLI: `-i, --id <id>`)
319 |     *   `dependsOn`: `Required. The ID of the Taskmaster task that should no longer be a prerequisite.` (CLI: `-d, --depends-on <id>`)
320 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
321 | *   **Usage:** Update task relationships when the order of execution changes.
322 | 
323 | ### 20. Validate Dependencies (`validate_dependencies`)
324 | 
325 | *   **MCP Tool:** `validate_dependencies`
326 | *   **CLI Command:** `task-master validate-dependencies [options]`
327 | *   **Description:** `Check your Taskmaster tasks for dependency issues (like circular references or links to non-existent tasks) without making changes.`
328 | *   **Key Parameters/Options:**
329 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
330 | *   **Usage:** Audit the integrity of your task dependencies.
331 | 
332 | ### 21. Fix Dependencies (`fix_dependencies`)
333 | 
334 | *   **MCP Tool:** `fix_dependencies`
335 | *   **CLI Command:** `task-master fix-dependencies [options]`
336 | *   **Description:** `Automatically fix dependency issues (like circular references or links to non-existent tasks) in your Taskmaster tasks.`
337 | *   **Key Parameters/Options:**
338 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
339 | *   **Usage:** Clean up dependency errors automatically.
340 | 
341 | ---
342 | 
343 | ## Analysis & Reporting
344 | 
345 | ### 22. Analyze Project Complexity (`analyze_project_complexity`)
346 | 
347 | *   **MCP Tool:** `analyze_project_complexity`
348 | *   **CLI Command:** `task-master analyze-complexity [options]`
349 | *   **Description:** `Have Taskmaster analyze your tasks to determine their complexity and suggest which ones need to be broken down further.`
350 | *   **Key Parameters/Options:**
351 |     *   `output`: `Where to save the complexity analysis report (default: 'scripts/task-complexity-report.json').` (CLI: `-o, --output <file>`)
352 |     *   `threshold`: `The minimum complexity score (1-10) that should trigger a recommendation to expand a task.` (CLI: `-t, --threshold <number>`)
353 |     *   `research`: `Enable research role for more accurate complexity analysis. Requires appropriate API key.` (CLI: `-r, --research`)
354 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
355 | *   **Usage:** Used before breaking down tasks to identify which ones need the most attention.
356 | *   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
357 | 
358 | ### 23. View Complexity Report (`complexity_report`)
359 | 
360 | *   **MCP Tool:** `complexity_report`
361 | *   **CLI Command:** `task-master complexity-report [options]`
362 | *   **Description:** `Display the task complexity analysis report in a readable format.`
363 | *   **Key Parameters/Options:**
364 |     *   `file`: `Path to the complexity report (default: 'scripts/task-complexity-report.json').` (CLI: `-f, --file <file>`)
365 | *   **Usage:** Review and understand the complexity analysis results after running analyze-complexity.
366 | 
367 | ---
368 | 
369 | ## File Management
370 | 
371 | ### 24. Generate Task Files (`generate`)
372 | 
373 | *   **MCP Tool:** `generate`
374 | *   **CLI Command:** `task-master generate [options]`
375 | *   **Description:** `Create or update individual Markdown files for each task based on your tasks.json.`
376 | *   **Key Parameters/Options:**
377 |     *   `output`: `The directory where Taskmaster should save the task files (default: in a 'tasks' directory).` (CLI: `-o, --output <directory>`)
378 |     *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
379 | *   **Usage:** Run this after making changes to tasks.json to keep individual task files up to date.
380 | 
381 | ---
382 | 
383 | ## Environment Variables Configuration (Updated)
384 | 
385 | Taskmaster primarily uses the **`.taskmasterconfig`** file (in project root) for configuration (models, parameters, logging level, etc.), managed via `task-master models --setup`.
386 | 
387 | Environment variables are used **only** for sensitive API keys related to AI providers and specific overrides like the Ollama base URL:
388 | 
389 | *   **API Keys (Required for corresponding provider):**
390 |     *   `ANTHROPIC_API_KEY`
391 |     *   `PERPLEXITY_API_KEY`
392 |     *   `OPENAI_API_KEY`
393 |     *   `GOOGLE_API_KEY`
394 |     *   `MISTRAL_API_KEY`
395 |     *   `AZURE_OPENAI_API_KEY` (Requires `AZURE_OPENAI_ENDPOINT` too)
396 |     *   `OPENROUTER_API_KEY`
397 |     *   `XAI_API_KEY`
398 |     *   `OLLANA_API_KEY` (Requires `OLLAMA_BASE_URL` too)
399 | *   **Endpoints (Optional/Provider Specific inside .taskmasterconfig):**
400 |     *   `AZURE_OPENAI_ENDPOINT`
401 |     *   `OLLAMA_BASE_URL` (Default: `http://localhost:11434/api`)
402 | 
403 | **Set API keys** in your **`.env`** file in the project root (for CLI use) or within the `env` section of your **`.roo/mcp.json`** file (for MCP/Roo Code integration). All other settings (model choice, max tokens, temperature, log level, custom endpoints) are managed in `.taskmasterconfig` via `task-master models` command or `models` MCP tool.
404 | 
405 | ---
406 | 
407 | For details on how these commands fit into the development process, see the [Development Workflow Guide](mdc:.roo/rules/dev_workflow.md).
408 | 
```

--------------------------------------------------------------------------------
/src/tools/conversation-tools.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { z } from 'zod';
   2 | import { CursorDatabaseReader } from '../database/reader.js';
   3 | import { ConversationParser } from '../database/parser.js';
   4 | import type { ConversationFilters, ConversationSummary, ConversationSearchResult, BubbleMessage } from '../database/types.js';
   5 | import { detectCursorDatabasePath } from '../utils/database-utils.js';
   6 | 
   7 | // Input schema for list_conversations tool
   8 | export const listConversationsSchema = z.object({
   9 |   limit: z.number().min(1).max(1000).optional(),
  10 |   minLength: z.number().min(0).optional(),
  11 |   keywords: z.array(z.string()).optional(),
  12 |   hasCodeBlocks: z.boolean().optional(),
  13 |   format: z.enum(['legacy', 'modern', 'both']).optional(),
  14 |   includeEmpty: z.boolean().optional(),
  15 |   projectPath: z.string().optional(),
  16 |   filePattern: z.string().optional(),
  17 |   relevantFiles: z.array(z.string()).optional(),
  18 |   startDate: z.string().optional(),
  19 |   endDate: z.string().optional(),
  20 |   includeAiSummaries: z.boolean().optional().default(true)
  21 | });
  22 | 
  23 | export type ListConversationsInput = z.infer<typeof listConversationsSchema>;
  24 | 
  25 | // Output type for list_conversations tool
  26 | export interface ListConversationsOutput {
  27 |   conversations: Array<{
  28 |     composerId: string;
  29 |     format: 'legacy' | 'modern';
  30 |     messageCount: number;
  31 |     hasCodeBlocks: boolean;
  32 |     relevantFiles: string[];
  33 |     attachedFolders: string[];
  34 |     firstMessage?: string;
  35 |     title?: string;
  36 |     aiGeneratedSummary?: string;
  37 |     size: number;
  38 |   }>;
  39 |   totalFound: number;
  40 |   filters: {
  41 |     limit: number;
  42 |     minLength: number;
  43 |     format: string;
  44 |     hasCodeBlocks?: boolean;
  45 |     keywords?: string[];
  46 |     projectPath?: string;
  47 |     filePattern?: string;
  48 |     relevantFiles?: string[];
  49 |     includeAiSummaries?: boolean;
  50 |   };
  51 | }
  52 | 
  53 | /**
  54 |  * List Cursor conversations with optional filters and ROWID-based ordering
  55 |  */
  56 | export async function listConversations(input: ListConversationsInput): Promise<ListConversationsOutput> {
  57 |   const validatedInput = listConversationsSchema.parse(input);
  58 | 
  59 |   const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath();
  60 |   const reader = new CursorDatabaseReader({ dbPath });
  61 | 
  62 |   try {
  63 |     await reader.connect();
  64 | 
  65 |     const filters: ConversationFilters = {
  66 |       minLength: validatedInput.minLength,
  67 |       format: validatedInput.format,
  68 |       hasCodeBlocks: validatedInput.hasCodeBlocks,
  69 |       keywords: validatedInput.keywords,
  70 |       projectPath: validatedInput.projectPath,
  71 |       filePattern: validatedInput.filePattern,
  72 |       relevantFiles: validatedInput.relevantFiles
  73 |     };
  74 | 
  75 |     // Add date range filter if provided
  76 |     if (validatedInput.startDate || validatedInput.endDate) {
  77 |       const start = validatedInput.startDate ? new Date(validatedInput.startDate) : new Date('1970-01-01');
  78 |       const end = validatedInput.endDate ? new Date(validatedInput.endDate) : new Date();
  79 |       filters.dateRange = { start, end };
  80 |     }
  81 | 
  82 |     const conversationIds = await reader.getConversationIds(filters);
  83 |     let limitedIds = conversationIds.slice(0, validatedInput.limit);
  84 | 
  85 |     // Apply date filtering if specified (post-query filtering due to unreliable timestamps)
  86 |     if (validatedInput.startDate || validatedInput.endDate) {
  87 |       const filteredIds = [];
  88 |       for (const composerId of limitedIds) {
  89 |         try {
  90 |           const conversation = await reader.getConversationById(composerId);
  91 |           if (!conversation) continue;
  92 | 
  93 |           const hasValidDate = checkConversationDateRange(
  94 |             conversation,
  95 |             validatedInput.startDate,
  96 |             validatedInput.endDate
  97 |           );
  98 | 
  99 |           if (hasValidDate) {
 100 |             filteredIds.push(composerId);
 101 |           }
 102 |         } catch (error) {
 103 |           // Skip conversations that can't be processed
 104 |           continue;
 105 |         }
 106 |       }
 107 |       limitedIds = filteredIds;
 108 |     }
 109 | 
 110 |     const conversations = [];
 111 |     for (const composerId of limitedIds) {
 112 |       try {
 113 |         const summary = await reader.getConversationSummary(composerId, {
 114 |           includeFirstMessage: true,
 115 |           maxFirstMessageLength: 150,
 116 |           includeTitle: true,
 117 |           includeAIGeneratedSummary: validatedInput.includeAiSummaries
 118 |         });
 119 | 
 120 |         if (summary) {
 121 |           conversations.push({
 122 |             composerId: summary.composerId,
 123 |             format: summary.format,
 124 |             messageCount: summary.messageCount,
 125 |             hasCodeBlocks: summary.hasCodeBlocks,
 126 |             relevantFiles: summary.relevantFiles || [],
 127 |             attachedFolders: summary.attachedFolders || [],
 128 |             firstMessage: summary.firstMessage,
 129 |             title: summary.title,
 130 |             aiGeneratedSummary: summary.aiGeneratedSummary,
 131 |             size: summary.conversationSize
 132 |           });
 133 |         }
 134 |       } catch (error) {
 135 |         console.error(`Failed to get summary for conversation ${composerId}:`, error);
 136 |       }
 137 |     }
 138 | 
 139 |     return {
 140 |       conversations,
 141 |       totalFound: conversationIds.length,
 142 |       filters: {
 143 |         limit: validatedInput.limit ?? 10,
 144 |         minLength: validatedInput.minLength ?? 100,
 145 |         format: validatedInput.format ?? 'both',
 146 |         hasCodeBlocks: validatedInput.hasCodeBlocks,
 147 |         keywords: validatedInput.keywords,
 148 |         projectPath: validatedInput.projectPath,
 149 |         filePattern: validatedInput.filePattern,
 150 |         relevantFiles: validatedInput.relevantFiles,
 151 |         includeAiSummaries: validatedInput.includeAiSummaries
 152 |       }
 153 |     };
 154 | 
 155 |   } finally {
 156 |     // Always close the database connection
 157 |     reader.close();
 158 |   }
 159 | }
 160 | 
 161 | // Input schema for get_conversation tool
 162 | export const getConversationSchema = z.object({
 163 |   conversationId: z.string().min(1),
 164 |   includeCodeBlocks: z.boolean().optional().default(true),
 165 |   includeFileReferences: z.boolean().optional().default(true),
 166 |   includeMetadata: z.boolean().optional().default(false),
 167 |   resolveBubbles: z.boolean().optional().default(true),
 168 |   summaryOnly: z.boolean().optional().default(false)
 169 | });
 170 | 
 171 | export type GetConversationInput = z.infer<typeof getConversationSchema>;
 172 | 
 173 | // Output type for get_conversation tool
 174 | export interface GetConversationOutput {
 175 |   conversation: {
 176 |     composerId: string;
 177 |     format: 'legacy' | 'modern';
 178 |     messageCount: number;
 179 |     title?: string;
 180 |     aiGeneratedSummary?: string;
 181 |     messages?: Array<{
 182 |       type: number;
 183 |       text: string;
 184 |       bubbleId: string;
 185 |       relevantFiles?: string[];
 186 |       attachedFolders?: string[];
 187 |       codeBlocks?: Array<{
 188 |         language: string;
 189 |         code: string;
 190 |         filename?: string;
 191 |       }>;
 192 |     }>;
 193 |     codeBlocks?: Array<{
 194 |       language: string;
 195 |       code: string;
 196 |       filename?: string;
 197 |     }>;
 198 |     relevantFiles?: string[];
 199 |     attachedFolders?: string[];
 200 |     metadata?: {
 201 |       hasLoaded: boolean;
 202 |       storedSummary?: string;
 203 |       storedRichText?: string;
 204 |       size: number;
 205 |     };
 206 |   } | null;
 207 | }
 208 | 
 209 | /**
 210 |  * Get a specific conversation by ID with full content
 211 |  */
 212 | export async function getConversation(input: GetConversationInput): Promise<GetConversationOutput> {
 213 |   // Validate input
 214 |   const validatedInput = getConversationSchema.parse(input);
 215 | 
 216 |   // Create database reader
 217 |   const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath();
 218 |   const reader = new CursorDatabaseReader({ dbPath });
 219 | 
 220 |   try {
 221 |     // Connect to database
 222 |     await reader.connect();
 223 | 
 224 |     // If summaryOnly is requested, return enhanced summary without full content
 225 |     if (validatedInput.summaryOnly) {
 226 |       const summary = await reader.getConversationSummary(validatedInput.conversationId, {
 227 |         includeTitle: true,
 228 |         includeAIGeneratedSummary: true,
 229 |         includeFirstMessage: true,
 230 |         includeLastMessage: true,
 231 |         maxFirstMessageLength: 200,
 232 |         maxLastMessageLength: 200
 233 |       });
 234 | 
 235 |       if (!summary) {
 236 |         return { conversation: null };
 237 |       }
 238 | 
 239 |       return {
 240 |         conversation: {
 241 |           composerId: summary.composerId,
 242 |           format: summary.format,
 243 |           messageCount: summary.messageCount,
 244 |           title: summary.title,
 245 |           aiGeneratedSummary: summary.aiGeneratedSummary,
 246 |           relevantFiles: validatedInput.includeFileReferences ? summary.relevantFiles : undefined,
 247 |           attachedFolders: validatedInput.includeFileReferences ? summary.attachedFolders : undefined,
 248 |           metadata: validatedInput.includeMetadata ? {
 249 |             hasLoaded: true,
 250 |             storedSummary: summary.storedSummary,
 251 |             storedRichText: summary.storedRichText,
 252 |             size: summary.conversationSize
 253 |           } : undefined
 254 |         }
 255 |       };
 256 |     }
 257 | 
 258 |     // Get conversation
 259 |     const conversation = await reader.getConversationById(validatedInput.conversationId);
 260 | 
 261 |     if (!conversation) {
 262 |       return { conversation: null };
 263 |     }
 264 | 
 265 |     // Get conversation summary to extract title and AI summary
 266 |     const summary = await reader.getConversationSummary(validatedInput.conversationId, {
 267 |       includeTitle: true,
 268 |       includeAIGeneratedSummary: true
 269 |     });
 270 | 
 271 |     // Determine format
 272 |     const format = conversation.hasOwnProperty('_v') ? 'modern' : 'legacy';
 273 | 
 274 |     // Build response based on format
 275 |     if (format === 'legacy') {
 276 |       const legacyConv = conversation as any;
 277 |       const messages = legacyConv.conversation || [];
 278 | 
 279 |       // Extract data
 280 |       let allCodeBlocks: any[] = [];
 281 |       let allRelevantFiles: string[] = [];
 282 |       let allAttachedFolders: string[] = [];
 283 | 
 284 |       const processedMessages = messages.map((msg: any) => {
 285 |         if (validatedInput.includeCodeBlocks && msg.suggestedCodeBlocks) {
 286 |           allCodeBlocks.push(...msg.suggestedCodeBlocks);
 287 |         }
 288 | 
 289 |         if (validatedInput.includeFileReferences) {
 290 |           if (msg.relevantFiles) allRelevantFiles.push(...msg.relevantFiles);
 291 |           if (msg.attachedFoldersNew) allAttachedFolders.push(...msg.attachedFoldersNew);
 292 |         }
 293 | 
 294 |         return {
 295 |           type: msg.type,
 296 |           text: msg.text,
 297 |           bubbleId: msg.bubbleId,
 298 |           relevantFiles: validatedInput.includeFileReferences ? msg.relevantFiles : undefined,
 299 |           attachedFolders: validatedInput.includeFileReferences ? msg.attachedFoldersNew : undefined,
 300 |           codeBlocks: validatedInput.includeCodeBlocks ? msg.suggestedCodeBlocks : undefined
 301 |         };
 302 |       });
 303 | 
 304 |       allRelevantFiles = Array.from(new Set(allRelevantFiles));
 305 |       allAttachedFolders = Array.from(new Set(allAttachedFolders));
 306 | 
 307 |       return {
 308 |         conversation: {
 309 |           composerId: legacyConv.composerId,
 310 |           format: 'legacy',
 311 |           messageCount: messages.length,
 312 |           title: summary?.title,
 313 |           aiGeneratedSummary: summary?.aiGeneratedSummary,
 314 |           messages: processedMessages,
 315 |           codeBlocks: validatedInput.includeCodeBlocks ? allCodeBlocks : undefined,
 316 |           relevantFiles: validatedInput.includeFileReferences ? allRelevantFiles : undefined,
 317 |           attachedFolders: validatedInput.includeFileReferences ? allAttachedFolders : undefined,
 318 |           metadata: validatedInput.includeMetadata ? {
 319 |             hasLoaded: true,
 320 |             storedSummary: legacyConv.storedSummary,
 321 |             storedRichText: legacyConv.storedRichText,
 322 |             size: JSON.stringify(conversation).length
 323 |           } : undefined
 324 |         }
 325 |       };
 326 |     } else {
 327 |       const modernConv = conversation as any;
 328 |       const headers = modernConv.fullConversationHeadersOnly || [];
 329 | 
 330 |       if (validatedInput.resolveBubbles) {
 331 |         const resolvedMessages = [];
 332 |         for (const header of headers.slice(0, 10)) {
 333 |           try {
 334 |             const bubbleMessage = await reader.getBubbleMessage(modernConv.composerId, header.bubbleId);
 335 |             if (bubbleMessage) {
 336 |               resolvedMessages.push({
 337 |                 type: header.type,
 338 |                 text: bubbleMessage.text,
 339 |                 bubbleId: header.bubbleId,
 340 |                 relevantFiles: validatedInput.includeFileReferences ? bubbleMessage.relevantFiles : undefined,
 341 |                 attachedFolders: validatedInput.includeFileReferences ? bubbleMessage.attachedFoldersNew : undefined,
 342 |                 codeBlocks: validatedInput.includeCodeBlocks ? bubbleMessage.suggestedCodeBlocks : undefined
 343 |               });
 344 |             }
 345 |           } catch (error) {
 346 |             console.error(`Failed to resolve bubble ${header.bubbleId}:`, error);
 347 |           }
 348 |         }
 349 | 
 350 |         return {
 351 |           conversation: {
 352 |             composerId: modernConv.composerId,
 353 |             format: 'modern',
 354 |             messageCount: headers.length,
 355 |             title: summary?.title,
 356 |             aiGeneratedSummary: summary?.aiGeneratedSummary,
 357 |             messages: resolvedMessages,
 358 |             metadata: validatedInput.includeMetadata ? {
 359 |               hasLoaded: true,
 360 |               storedSummary: modernConv.storedSummary,
 361 |               storedRichText: modernConv.storedRichText,
 362 |               size: JSON.stringify(conversation).length
 363 |             } : undefined
 364 |           }
 365 |         };
 366 |       } else {
 367 |         return {
 368 |           conversation: {
 369 |             composerId: modernConv.composerId,
 370 |             format: 'modern',
 371 |             messageCount: headers.length,
 372 |             title: summary?.title,
 373 |             aiGeneratedSummary: summary?.aiGeneratedSummary,
 374 |             metadata: validatedInput.includeMetadata ? {
 375 |               hasLoaded: true,
 376 |               storedSummary: modernConv.storedSummary,
 377 |               storedRichText: modernConv.storedRichText,
 378 |               size: JSON.stringify(conversation).length
 379 |             } : undefined
 380 |           }
 381 |         };
 382 |       }
 383 |     }
 384 | 
 385 |   } finally {
 386 |     // Always close the database connection
 387 |     reader.close();
 388 |   }
 389 | }
 390 | 
 391 | // Input schema for get_conversation_summary tool
 392 | export const getConversationSummarySchema = z.object({
 393 |   conversationId: z.string().min(1),
 394 |   includeFirstMessage: z.boolean().optional().default(false),
 395 |   includeLastMessage: z.boolean().optional().default(false),
 396 |   maxFirstMessageLength: z.number().min(1).max(1000).optional().default(200),
 397 |   maxLastMessageLength: z.number().min(1).max(1000).optional().default(200),
 398 |   includeMetadata: z.boolean().optional().default(false)
 399 | });
 400 | 
 401 | export type GetConversationSummaryInput = z.infer<typeof getConversationSummarySchema>;
 402 | 
 403 | // Output type for get_conversation_summary tool
 404 | export interface GetConversationSummaryOutput {
 405 |   summary: {
 406 |     composerId: string;
 407 |     format: 'legacy' | 'modern';
 408 |     messageCount: number;
 409 |     hasCodeBlocks: boolean;
 410 |     codeBlockCount?: number;
 411 |     conversationSize: number;
 412 |     firstMessage?: string;
 413 |     lastMessage?: string;
 414 |     storedSummary?: string;
 415 |     storedRichText?: string;
 416 |     relevantFiles?: string[];
 417 |     attachedFolders?: string[];
 418 |     metadata?: {
 419 |       totalCharacters: number;
 420 |       averageMessageLength: number;
 421 |     };
 422 |   } | null;
 423 | }
 424 | 
 425 | /**
 426 |  * Get conversation summary with optional first/last message content
 427 |  */
 428 | export async function getConversationSummary(input: GetConversationSummaryInput): Promise<GetConversationSummaryOutput> {
 429 |   const validatedInput = getConversationSummarySchema.parse(input);
 430 |   const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath();
 431 |   const reader = new CursorDatabaseReader({ dbPath });
 432 | 
 433 |   try {
 434 |     await reader.connect();
 435 | 
 436 |     const summary = await reader.getConversationSummary(validatedInput.conversationId, {
 437 |       includeFirstMessage: validatedInput.includeFirstMessage,
 438 |       includeLastMessage: validatedInput.includeLastMessage,
 439 |       maxFirstMessageLength: validatedInput.maxFirstMessageLength,
 440 |       maxLastMessageLength: validatedInput.maxLastMessageLength
 441 |     });
 442 | 
 443 |     if (!summary) {
 444 |       return { summary: null };
 445 |     }
 446 | 
 447 |     return {
 448 |       summary: {
 449 |         composerId: summary.composerId,
 450 |         format: summary.format,
 451 |         messageCount: summary.messageCount,
 452 |         hasCodeBlocks: summary.hasCodeBlocks,
 453 |         codeBlockCount: summary.codeBlockCount,
 454 |         conversationSize: summary.conversationSize,
 455 |         firstMessage: summary.firstMessage,
 456 |         lastMessage: summary.lastMessage,
 457 |         storedSummary: summary.storedSummary,
 458 |         storedRichText: summary.storedRichText,
 459 |         relevantFiles: summary.relevantFiles,
 460 |         attachedFolders: summary.attachedFolders,
 461 |         metadata: validatedInput.includeMetadata ? {
 462 |           totalCharacters: summary.conversationSize,
 463 |           averageMessageLength: Math.round(summary.conversationSize / summary.messageCount)
 464 |         } : undefined
 465 |       }
 466 |     };
 467 | 
 468 |   } finally {
 469 |     reader.close();
 470 |   }
 471 | }
 472 | 
 473 | // Input schema for search_conversations tool
 474 | export const searchConversationsSchema = z.object({
 475 |   // Simple query (existing - backward compatible)
 476 |   query: z.string().optional(),
 477 | 
 478 |   // Multi-keyword search
 479 |   keywords: z.array(z.string().min(1)).optional(),
 480 |   keywordOperator: z.enum(['AND', 'OR']).optional().default('OR'),
 481 | 
 482 |   // LIKE pattern search (database-level)
 483 |   likePattern: z.string().optional(),
 484 | 
 485 |   // Date filtering
 486 |   startDate: z.string().optional(),
 487 |   endDate: z.string().optional(),
 488 | 
 489 |   // Existing options
 490 |   includeCode: z.boolean().optional().default(true),
 491 |   contextLines: z.number().min(0).max(10).optional().default(2),
 492 |   maxResults: z.number().min(1).max(100).optional().default(10),
 493 |   searchBubbles: z.boolean().optional().default(true),
 494 |   searchType: z.enum(['all', 'summarization', 'code', 'files', 'project']).optional().default('all'),
 495 |   format: z.enum(['legacy', 'modern', 'both']).optional().default('both'),
 496 |   highlightMatches: z.boolean().optional().default(true),
 497 |   projectSearch: z.boolean().optional().default(false),
 498 |   fuzzyMatch: z.boolean().optional().default(false),
 499 |   includePartialPaths: z.boolean().optional().default(true),
 500 |   includeFileContent: z.boolean().optional().default(false),
 501 |   minRelevanceScore: z.number().min(0).max(1).optional().default(0.1),
 502 |   orderBy: z.enum(['relevance', 'recency']).optional().default('relevance')
 503 | }).refine(
 504 |   (data) => {
 505 |     const hasSearchCriteria = (data.query && data.query.trim() !== '' && data.query.trim() !== '?') || data.keywords || data.likePattern;
 506 |     const hasDateFilter = data.startDate || data.endDate;
 507 |     const hasOtherFilters = data.searchType !== 'all';
 508 |     return hasSearchCriteria || hasDateFilter || hasOtherFilters;
 509 |   },
 510 |   { message: "At least one search criteria (query, keywords, likePattern), date filter (startDate, endDate), or search type filter must be provided" }
 511 | );
 512 | 
 513 | export type SearchConversationsInput = z.infer<typeof searchConversationsSchema>;
 514 | 
 515 | // Output type for search_conversations tool
 516 | export interface SearchConversationsOutput {
 517 |   conversations: Array<{
 518 |     composerId: string;
 519 |     format: 'legacy' | 'modern';
 520 |     messageCount: number;
 521 |     hasCodeBlocks: boolean;
 522 |     relevantFiles: string[];
 523 |     attachedFolders: string[];
 524 |     firstMessage?: string;
 525 |     title?: string;
 526 |     aiGeneratedSummary?: string;
 527 |     size: number;
 528 |     relevanceScore?: number;
 529 |     matchDetails?: {
 530 |       exactPathMatch: boolean;
 531 |       partialPathMatch: boolean;
 532 |       filePathMatch: boolean;
 533 |       fuzzyMatch: boolean;
 534 |       matchedPaths: string[];
 535 |       matchedFiles: string[];
 536 |     };
 537 |   }>;
 538 |   totalResults: number;
 539 |   query: string;
 540 |   searchOptions: {
 541 |     includeCode: boolean;
 542 |     contextLines: number;
 543 |     maxResults: number;
 544 |     searchBubbles: boolean;
 545 |     searchType: 'all' | 'summarization' | 'code' | 'files' | 'project';
 546 |     format: 'legacy' | 'modern' | 'both';
 547 |     highlightMatches: boolean;
 548 |     projectSearch?: boolean;
 549 |     fuzzyMatch?: boolean;
 550 |     includePartialPaths?: boolean;
 551 |     includeFileContent?: boolean;
 552 |     minRelevanceScore?: number;
 553 |     orderBy?: 'relevance' | 'recency';
 554 |   };
 555 |   debugInfo?: {
 556 |     totalConversationsScanned: number;
 557 |     averageRelevanceScore: number;
 558 |     matchTypeDistribution: {
 559 |       exactPath: number;
 560 |       partialPath: number;
 561 |       filePath: number;
 562 |       fuzzy: number;
 563 |     };
 564 |   };
 565 | }
 566 | 
 567 | /**
 568 |  * Search conversations with enhanced multi-keyword and LIKE pattern support
 569 |  */
 570 | export async function searchConversations(input: SearchConversationsInput): Promise<SearchConversationsOutput> {
 571 |   const validatedInput = searchConversationsSchema.parse(input);
 572 |   const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath();
 573 |   const reader = new CursorDatabaseReader({ dbPath });
 574 | 
 575 |   try {
 576 |     await reader.connect();
 577 | 
 578 |     // Determine the search query for display purposes
 579 |     const displayQuery = validatedInput.query ||
 580 |                         (validatedInput.keywords ? validatedInput.keywords.join(` ${validatedInput.keywordOperator} `) : '') ||
 581 |                         validatedInput.likePattern ||
 582 |                         'advanced search';
 583 | 
 584 |     if (validatedInput.projectSearch && validatedInput.query) {
 585 |       // Handle project search (existing logic)
 586 |       const searchOptions = {
 587 |         fuzzyMatch: validatedInput.fuzzyMatch,
 588 |         includePartialPaths: validatedInput.includePartialPaths,
 589 |         includeFileContent: validatedInput.includeFileContent,
 590 |         minRelevanceScore: validatedInput.minRelevanceScore,
 591 |         orderBy: validatedInput.orderBy,
 592 |         limit: validatedInput.maxResults
 593 |       };
 594 | 
 595 |       const conversationIds = await reader.getConversationIds({
 596 |         format: validatedInput.format,
 597 |         projectPath: validatedInput.query
 598 |       });
 599 | 
 600 |       const conversations = [];
 601 |       const matchTypeDistribution = {
 602 |         exactPath: 0,
 603 |         partialPath: 0,
 604 |         filePath: 0,
 605 |         fuzzy: 0
 606 |       };
 607 | 
 608 |       let totalConversationsScanned = 0;
 609 |       let totalRelevanceScore = 0;
 610 | 
 611 |       for (const composerId of conversationIds.slice(0, validatedInput.maxResults * 2)) {
 612 |         try {
 613 |           totalConversationsScanned++;
 614 |           const conversation = await reader.getConversationById(composerId);
 615 |           if (!conversation) continue;
 616 | 
 617 |           const format = conversation.hasOwnProperty('_v') ? 'modern' : 'legacy';
 618 | 
 619 |           if (format === 'modern') {
 620 |             const modernConv = conversation as any;
 621 |             const headers = modernConv.fullConversationHeadersOnly || [];
 622 | 
 623 |             for (const header of headers.slice(0, 5)) {
 624 |               try {
 625 |                 const bubbleMessage = await reader.getBubbleMessage(modernConv.composerId, header.bubbleId);
 626 |                 if (bubbleMessage) {
 627 |                   (conversation as any).resolvedMessages = (conversation as any).resolvedMessages || [];
 628 |                   (conversation as any).resolvedMessages.push(bubbleMessage);
 629 |                 }
 630 |               } catch (error) {
 631 |                 continue;
 632 |               }
 633 |             }
 634 |           }
 635 | 
 636 |           const relevanceResult = calculateEnhancedProjectRelevance(
 637 |             conversation,
 638 |             validatedInput.query,
 639 |             {
 640 |               fuzzyMatch: validatedInput.fuzzyMatch || false,
 641 |               includePartialPaths: validatedInput.includePartialPaths || false,
 642 |               includeFileContent: validatedInput.includeFileContent || false
 643 |             }
 644 |           );
 645 | 
 646 |           if (relevanceResult.score >= (validatedInput.minRelevanceScore || 0.1)) {
 647 |             const summary = await reader.getConversationSummary(composerId, {
 648 |               includeFirstMessage: true,
 649 |               maxFirstMessageLength: 150
 650 |             });
 651 | 
 652 |             if (summary) {
 653 |               conversations.push({
 654 |                 composerId: summary.composerId,
 655 |                 format: summary.format,
 656 |                 messageCount: summary.messageCount,
 657 |                 hasCodeBlocks: summary.hasCodeBlocks,
 658 |                 relevantFiles: summary.relevantFiles || [],
 659 |                 attachedFolders: summary.attachedFolders || [],
 660 |                 firstMessage: summary.firstMessage,
 661 |                 size: summary.conversationSize,
 662 |                 relevanceScore: relevanceResult.score,
 663 |                 matchDetails: relevanceResult.details
 664 |               });
 665 | 
 666 |               totalRelevanceScore += relevanceResult.score;
 667 | 
 668 |               if (relevanceResult.details.exactPathMatch) matchTypeDistribution.exactPath++;
 669 |               if (relevanceResult.details.partialPathMatch) matchTypeDistribution.partialPath++;
 670 |               if (relevanceResult.details.filePathMatch) matchTypeDistribution.filePath++;
 671 |               if (relevanceResult.details.fuzzyMatch) matchTypeDistribution.fuzzy++;
 672 |             }
 673 |           }
 674 |         } catch (error) {
 675 |           continue;
 676 |         }
 677 |       }
 678 | 
 679 |       if (validatedInput.orderBy === 'relevance') {
 680 |         conversations.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
 681 |       }
 682 | 
 683 |       return {
 684 |         conversations: conversations.slice(0, validatedInput.maxResults),
 685 |         totalResults: conversations.length,
 686 |         query: displayQuery,
 687 |         searchOptions: {
 688 |           includeCode: validatedInput.includeCode,
 689 |           contextLines: validatedInput.contextLines,
 690 |           maxResults: validatedInput.maxResults,
 691 |           searchBubbles: validatedInput.searchBubbles,
 692 |           searchType: validatedInput.searchType,
 693 |           format: validatedInput.format,
 694 |           highlightMatches: validatedInput.highlightMatches,
 695 |           projectSearch: validatedInput.projectSearch,
 696 |           fuzzyMatch: validatedInput.fuzzyMatch,
 697 |           includePartialPaths: validatedInput.includePartialPaths,
 698 |           includeFileContent: validatedInput.includeFileContent,
 699 |           minRelevanceScore: validatedInput.minRelevanceScore,
 700 |           orderBy: validatedInput.orderBy
 701 |         },
 702 |         debugInfo: {
 703 |           totalConversationsScanned,
 704 |           averageRelevanceScore: totalConversationsScanned > 0 ? totalRelevanceScore / totalConversationsScanned : 0,
 705 |           matchTypeDistribution
 706 |         }
 707 |       };
 708 |     } else {
 709 |       const hasSearchCriteria = (validatedInput.query && validatedInput.query.trim() !== '' && validatedInput.query.trim() !== '?') || validatedInput.keywords || validatedInput.likePattern;
 710 | 
 711 |       if (!hasSearchCriteria && (validatedInput.startDate || validatedInput.endDate)) {
 712 |         // Date-only search: get all conversations and filter by date
 713 |         const allConversationIds = await reader.getConversationIds({
 714 |           format: validatedInput.format
 715 |         });
 716 | 
 717 |         const conversations = [];
 718 |         for (const composerId of allConversationIds.slice(0, validatedInput.maxResults * 2)) {
 719 |           try {
 720 |             const conversation = await reader.getConversationById(composerId);
 721 |             if (!conversation) continue;
 722 | 
 723 |             // Apply date filtering
 724 |             const hasValidDate = checkConversationDateRange(
 725 |               conversation,
 726 |               validatedInput.startDate,
 727 |               validatedInput.endDate
 728 |             );
 729 | 
 730 |             if (!hasValidDate) continue;
 731 | 
 732 |             const summary = await reader.getConversationSummary(composerId, {
 733 |               includeFirstMessage: true,
 734 |               maxFirstMessageLength: 150,
 735 |               includeTitle: true,
 736 |               includeAIGeneratedSummary: true
 737 |             });
 738 | 
 739 |             if (summary) {
 740 |               conversations.push({
 741 |                 composerId: summary.composerId,
 742 |                 format: summary.format,
 743 |                 messageCount: summary.messageCount,
 744 |                 hasCodeBlocks: summary.hasCodeBlocks,
 745 |                 relevantFiles: summary.relevantFiles || [],
 746 |                 attachedFolders: summary.attachedFolders || [],
 747 |                 firstMessage: summary.firstMessage,
 748 |                 title: summary.title,
 749 |                 aiGeneratedSummary: summary.aiGeneratedSummary,
 750 |                 size: summary.conversationSize
 751 |               });
 752 | 
 753 |               if (conversations.length >= validatedInput.maxResults) break;
 754 |             }
 755 |           } catch (error) {
 756 |             console.error(`Failed to process conversation ${composerId}:`, error);
 757 |           }
 758 |         }
 759 | 
 760 |         return {
 761 |           conversations,
 762 |           totalResults: conversations.length,
 763 |           query: displayQuery,
 764 |           searchOptions: {
 765 |             includeCode: validatedInput.includeCode,
 766 |             contextLines: validatedInput.contextLines,
 767 |             maxResults: validatedInput.maxResults,
 768 |             searchBubbles: validatedInput.searchBubbles,
 769 |             searchType: validatedInput.searchType,
 770 |             format: validatedInput.format,
 771 |             highlightMatches: validatedInput.highlightMatches
 772 |           }
 773 |         };
 774 |       }
 775 | 
 776 |       // Handle enhanced search with keywords, LIKE patterns, or simple query
 777 |       const searchResults = await reader.searchConversationsEnhanced({
 778 |         query: validatedInput.query,
 779 |         keywords: validatedInput.keywords,
 780 |         keywordOperator: validatedInput.keywordOperator,
 781 |         likePattern: validatedInput.likePattern,
 782 |         includeCode: validatedInput.includeCode,
 783 |         contextLines: validatedInput.contextLines,
 784 |         maxResults: validatedInput.maxResults,
 785 |         searchBubbles: validatedInput.searchBubbles,
 786 |         searchType: validatedInput.searchType === 'project' ? 'all' : validatedInput.searchType,
 787 |         format: validatedInput.format,
 788 |         startDate: validatedInput.startDate,
 789 |         endDate: validatedInput.endDate
 790 |       });
 791 | 
 792 |       // Convert search results to conversation summaries for consistency
 793 |       const conversations = [];
 794 |       for (const result of searchResults) {
 795 |         try {
 796 |           // Apply date filtering if specified (post-query filtering due to unreliable timestamps)
 797 |           if (validatedInput.startDate || validatedInput.endDate) {
 798 |             const conversation = await reader.getConversationById(result.composerId);
 799 |             if (!conversation) continue;
 800 | 
 801 |             const hasValidDate = checkConversationDateRange(
 802 |               conversation,
 803 |               validatedInput.startDate,
 804 |               validatedInput.endDate
 805 |             );
 806 | 
 807 |             if (!hasValidDate) continue;
 808 |           }
 809 | 
 810 |           const summary = await reader.getConversationSummary(result.composerId, {
 811 |             includeFirstMessage: true,
 812 |             maxFirstMessageLength: 150,
 813 |             includeTitle: true,
 814 |             includeAIGeneratedSummary: true
 815 |           });
 816 | 
 817 |           if (summary) {
 818 |             conversations.push({
 819 |               composerId: summary.composerId,
 820 |               format: summary.format,
 821 |               messageCount: summary.messageCount,
 822 |               hasCodeBlocks: summary.hasCodeBlocks,
 823 |               relevantFiles: summary.relevantFiles || [],
 824 |               attachedFolders: summary.attachedFolders || [],
 825 |               firstMessage: summary.firstMessage,
 826 |               title: summary.title,
 827 |               aiGeneratedSummary: summary.aiGeneratedSummary,
 828 |               size: summary.conversationSize
 829 |             });
 830 |           }
 831 |         } catch (error) {
 832 |           console.error(`Failed to get summary for conversation ${result.composerId}:`, error);
 833 |         }
 834 |       }
 835 | 
 836 |       return {
 837 |         conversations,
 838 |         totalResults: conversations.length,
 839 |         query: displayQuery,
 840 |         searchOptions: {
 841 |           includeCode: validatedInput.includeCode,
 842 |           contextLines: validatedInput.contextLines,
 843 |           maxResults: validatedInput.maxResults,
 844 |           searchBubbles: validatedInput.searchBubbles,
 845 |           searchType: validatedInput.searchType,
 846 |           format: validatedInput.format,
 847 |           highlightMatches: validatedInput.highlightMatches
 848 |         }
 849 |       };
 850 |     }
 851 | 
 852 |   } finally {
 853 |     reader.close();
 854 |   }
 855 | }
 856 | 
 857 | // Get bubble message tool schema and types
 858 | export const getBubbleMessageSchema = z.object({
 859 |   composerId: z.string().min(1).describe('The composer ID of the conversation containing the bubble message'),
 860 |   bubbleId: z.string().min(1).describe('The unique bubble ID of the message to retrieve'),
 861 |   includeMetadata: z.boolean().optional().default(false).describe('Include additional metadata about the bubble message'),
 862 |   includeCodeBlocks: z.boolean().optional().default(true).describe('Include code blocks in the response'),
 863 |   includeFileReferences: z.boolean().optional().default(true).describe('Include file references and attached folders'),
 864 |   resolveReferences: z.boolean().optional().default(false).describe('Attempt to resolve file references to actual content')
 865 | });
 866 | 
 867 | export type GetBubbleMessageInput = z.infer<typeof getBubbleMessageSchema>;
 868 | 
 869 | export interface GetBubbleMessageOutput {
 870 |   bubbleMessage: BubbleMessage | null;
 871 |   metadata?: {
 872 |     composerId: string;
 873 |     bubbleId: string;
 874 |     messageType: 'user' | 'assistant' | 'unknown';
 875 |     hasCodeBlocks: boolean;
 876 |     codeBlockCount: number;
 877 |     hasFileReferences: boolean;
 878 |     fileReferenceCount: number;
 879 |     hasAttachedFolders: boolean;
 880 |     attachedFolderCount: number;
 881 |     messageLength: number;
 882 |     timestamp?: string;
 883 |   };
 884 |   error?: string;
 885 | }
 886 | 
 887 | /**
 888 |  * Get a specific bubble message from a modern format conversation
 889 |  */
 890 | export async function getBubbleMessage(input: GetBubbleMessageInput): Promise<GetBubbleMessageOutput> {
 891 |   // Validate input
 892 |   const validatedInput = getBubbleMessageSchema.parse(input);
 893 | 
 894 |   // Create database reader
 895 |   const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath();
 896 |   const reader = new CursorDatabaseReader({ dbPath });
 897 | 
 898 |   try {
 899 |     // Connect to database
 900 |     await reader.connect();
 901 | 
 902 |     // Get the bubble message
 903 |     const bubbleMessage = await reader.getBubbleMessage(validatedInput.composerId, validatedInput.bubbleId);
 904 | 
 905 |     if (!bubbleMessage) {
 906 |       return {
 907 |         bubbleMessage: null,
 908 |         error: `Bubble message not found: ${validatedInput.bubbleId} in conversation ${validatedInput.composerId}`
 909 |       };
 910 |     }
 911 | 
 912 |     // Build metadata if requested
 913 |     let metadata;
 914 |     if (validatedInput.includeMetadata) {
 915 |       const hasCodeBlocks = !!(bubbleMessage.suggestedCodeBlocks && bubbleMessage.suggestedCodeBlocks.length > 0);
 916 |       const hasFileReferences = !!(bubbleMessage.relevantFiles && bubbleMessage.relevantFiles.length > 0);
 917 |       const hasAttachedFolders = !!(bubbleMessage.attachedFoldersNew && bubbleMessage.attachedFoldersNew.length > 0);
 918 | 
 919 |       const messageType: 'user' | 'assistant' | 'unknown' =
 920 |         bubbleMessage.type === 0 ? 'user' :
 921 |         bubbleMessage.type === 1 ? 'assistant' : 'unknown';
 922 | 
 923 |       metadata = {
 924 |         composerId: validatedInput.composerId,
 925 |         bubbleId: validatedInput.bubbleId,
 926 |         messageType,
 927 |         hasCodeBlocks,
 928 |         codeBlockCount: bubbleMessage.suggestedCodeBlocks?.length || 0,
 929 |         hasFileReferences,
 930 |         fileReferenceCount: bubbleMessage.relevantFiles?.length || 0,
 931 |         hasAttachedFolders,
 932 |         attachedFolderCount: bubbleMessage.attachedFoldersNew?.length || 0,
 933 |         messageLength: bubbleMessage.text.length,
 934 |         timestamp: bubbleMessage.timestamp
 935 |       };
 936 |     }
 937 | 
 938 |     return {
 939 |       bubbleMessage,
 940 |       metadata
 941 |     };
 942 | 
 943 |   } finally {
 944 |     // Always close the database connection
 945 |     reader.close();
 946 |   }
 947 | }
 948 | 
 949 | // Input schema for get_recent_conversations tool
 950 | export const getRecentConversationsSchema = z.object({
 951 |   limit: z.number().min(1).max(100).optional().default(10),
 952 |   includeEmpty: z.boolean().optional().default(false),
 953 |   format: z.enum(['legacy', 'modern', 'both']).optional().default('both'),
 954 |   includeFirstMessage: z.boolean().optional().default(true),
 955 |   maxFirstMessageLength: z.number().min(10).max(500).optional().default(150),
 956 |   includeMetadata: z.boolean().optional().default(false)
 957 | });
 958 | 
 959 | export type GetRecentConversationsInput = z.infer<typeof getRecentConversationsSchema>;
 960 | 
 961 | // Output type for get_recent_conversations tool
 962 | export interface GetRecentConversationsOutput {
 963 |   conversations: Array<{
 964 |     composerId: string;
 965 |     format: 'legacy' | 'modern';
 966 |     messageCount: number;
 967 |     hasCodeBlocks: boolean;
 968 |     relevantFiles: string[];
 969 |     attachedFolders: string[];
 970 |     firstMessage?: string;
 971 |     size: number;
 972 |     metadata?: {
 973 |       hasLoaded: boolean;
 974 |       totalCharacters: number;
 975 |       averageMessageLength: number;
 976 |       codeBlockCount: number;
 977 |       fileReferenceCount: number;
 978 |       attachedFolderCount: number;
 979 |     };
 980 |   }>;
 981 |   totalFound: number;
 982 |   requestedLimit: number;
 983 |   timestamp: string;
 984 | }
 985 | 
 986 | /**
 987 |  * Get recent Cursor conversations ordered by ROWID (most recent first)
 988 |  */
 989 | export async function getRecentConversations(input: GetRecentConversationsInput): Promise<GetRecentConversationsOutput> {
 990 |   // Validate input
 991 |   const validatedInput = getRecentConversationsSchema.parse(input);
 992 | 
 993 |   // Create database reader
 994 |   const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath();
 995 |   const reader = new CursorDatabaseReader({
 996 |     dbPath,
 997 |     minConversationSize: validatedInput.includeEmpty ? 0 : 5000
 998 |   });
 999 | 
1000 |   try {
1001 |     // Connect to database
1002 |     await reader.connect();
1003 | 
1004 |     // Build minimal filters for recent conversations
1005 |     const filters: ConversationFilters = {
1006 |       minLength: validatedInput.includeEmpty ? 0 : 5000,
1007 |       format: validatedInput.format
1008 |     };
1009 | 
1010 |     // Get conversation IDs (already ordered by ROWID DESC)
1011 |     const conversationIds = await reader.getConversationIds(filters);
1012 | 
1013 |     // Limit results
1014 |     const limitedIds = conversationIds.slice(0, validatedInput.limit);
1015 | 
1016 |     // Get conversation summaries
1017 |     const conversations = [];
1018 |     for (const composerId of limitedIds) {
1019 |       try {
1020 |         const summary = await reader.getConversationSummary(composerId, {
1021 |           includeFirstMessage: validatedInput.includeFirstMessage,
1022 |           maxFirstMessageLength: validatedInput.maxFirstMessageLength,
1023 |           includeFileList: true,
1024 |           includeCodeBlockCount: true
1025 |         });
1026 | 
1027 |         if (summary) {
1028 |           const conversationData: any = {
1029 |             composerId: summary.composerId,
1030 |             format: summary.format,
1031 |             messageCount: summary.messageCount,
1032 |             hasCodeBlocks: summary.hasCodeBlocks,
1033 |             relevantFiles: summary.relevantFiles,
1034 |             attachedFolders: summary.attachedFolders,
1035 |             firstMessage: summary.firstMessage,
1036 |             size: summary.conversationSize
1037 |           };
1038 | 
1039 |           // Add metadata if requested
1040 |           if (validatedInput.includeMetadata) {
1041 |             conversationData.metadata = {
1042 |               hasLoaded: true,
1043 |               totalCharacters: summary.conversationSize,
1044 |               averageMessageLength: summary.messageCount > 0 ? Math.round(summary.conversationSize / summary.messageCount) : 0,
1045 |               codeBlockCount: summary.codeBlockCount || 0,
1046 |               fileReferenceCount: summary.relevantFiles.length,
1047 |               attachedFolderCount: summary.attachedFolders.length
1048 |             };
1049 |           }
1050 | 
1051 |           conversations.push(conversationData);
1052 |         }
1053 |       } catch (error) {
1054 |         console.error(`Failed to get summary for conversation ${composerId}:`, error);
1055 |         // Continue with other conversations
1056 |       }
1057 |     }
1058 | 
1059 |     return {
1060 |       conversations,
1061 |       totalFound: conversationIds.length,
1062 |       requestedLimit: validatedInput.limit,
1063 |       timestamp: new Date().toISOString()
1064 |     };
1065 | 
1066 |   } finally {
1067 |     // Always close the database connection
1068 |     reader.close();
1069 |   }
1070 | }
1071 | 
1072 | // Input schema for get_conversations_by_project tool
1073 | export const getConversationsByProjectSchema = z.object({
1074 |   projectPath: z.string().min(1),
1075 |   filePattern: z.string().optional(),
1076 |   exactFilePath: z.string().optional(),
1077 |   orderBy: z.enum(['recency', 'relevance']).optional().default('recency'),
1078 |   limit: z.number().min(1).max(1000).optional().default(50),
1079 |   fuzzyMatch: z.boolean().optional().default(false)
1080 | });
1081 | 
1082 | export type GetConversationsByProjectInput = z.infer<typeof getConversationsByProjectSchema>;
1083 | 
1084 | // Output type for get_conversations_by_project tool
1085 | export interface GetConversationsByProjectOutput {
1086 |   conversations: Array<{
1087 |     composerId: string;
1088 |     format: 'legacy' | 'modern';
1089 |     messageCount: number;
1090 |     hasCodeBlocks: boolean;
1091 |     relevantFiles: string[];
1092 |     attachedFolders: string[];
1093 |     firstMessage?: string;
1094 |     size: number;
1095 |     relevanceScore?: number;
1096 |   }>;
1097 |   totalFound: number;
1098 |   filters: {
1099 |     projectPath: string;
1100 |     filePattern?: string;
1101 |     exactFilePath?: string;
1102 |     orderBy: string;
1103 |     limit: number;
1104 |   };
1105 | }
1106 | 
1107 | /**
1108 |  * Get conversations filtered by project path, attached folders, or relevant files
1109 |  */
1110 | export async function getConversationsByProject(input: GetConversationsByProjectInput): Promise<GetConversationsByProjectOutput> {
1111 |   // Validate input
1112 |   const validatedInput = getConversationsByProjectSchema.parse(input);
1113 | 
1114 |   // Create database reader
1115 |   const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath();
1116 |   const reader = new CursorDatabaseReader({
1117 |     dbPath,
1118 |     minConversationSize: 5000 // Default minimum size for project conversations
1119 |   });
1120 | 
1121 |   try {
1122 |     // Connect to database
1123 |     await reader.connect();
1124 | 
1125 |     // Get conversation IDs with project-specific filtering
1126 |     const conversationResults = await reader.getConversationIdsByProject(
1127 |       validatedInput.projectPath,
1128 |       {
1129 |         filePattern: validatedInput.filePattern,
1130 |         exactFilePath: validatedInput.exactFilePath,
1131 |         orderBy: validatedInput.orderBy,
1132 |         limit: validatedInput.limit,
1133 |         format: 'both', // Support both legacy and modern formats
1134 |         fuzzyMatch: validatedInput.fuzzyMatch
1135 |       }
1136 |     );
1137 | 
1138 |     // Get conversation summaries
1139 |     const conversations = [];
1140 |     for (const result of conversationResults) {
1141 |       try {
1142 |         const summary = await reader.getConversationSummary(result.composerId, {
1143 |           includeFirstMessage: true,
1144 |           maxFirstMessageLength: 100,
1145 |           includeFileList: true,
1146 |           includeCodeBlockCount: true
1147 |         });
1148 | 
1149 |         if (summary) {
1150 |           conversations.push({
1151 |             composerId: summary.composerId,
1152 |             format: summary.format,
1153 |             messageCount: summary.messageCount,
1154 |             hasCodeBlocks: summary.hasCodeBlocks,
1155 |             relevantFiles: summary.relevantFiles,
1156 |             attachedFolders: summary.attachedFolders,
1157 |             firstMessage: summary.firstMessage,
1158 |             size: summary.conversationSize,
1159 |             relevanceScore: result.relevanceScore
1160 |           });
1161 |         }
1162 |       } catch (error) {
1163 |         console.error(`Failed to get summary for conversation ${result.composerId}:`, error);
1164 |         // Continue with other conversations
1165 |       }
1166 |     }
1167 | 
1168 |     return {
1169 |       conversations,
1170 |       totalFound: conversationResults.length,
1171 |       filters: {
1172 |         projectPath: validatedInput.projectPath,
1173 |         filePattern: validatedInput.filePattern,
1174 |         exactFilePath: validatedInput.exactFilePath,
1175 |         orderBy: validatedInput.orderBy,
1176 |         limit: validatedInput.limit
1177 |       }
1178 |     };
1179 | 
1180 |   } finally {
1181 |     // Always close the database connection
1182 |     reader.close();
1183 |   }
1184 | }
1185 | 
1186 | // Input schema for search_conversations_by_project tool (improved project search)
1187 | export const searchConversationsByProjectSchema = z.object({
1188 |   projectQuery: z.string().min(1),
1189 |   fuzzyMatch: z.boolean().optional().default(true),
1190 |   includePartialPaths: z.boolean().optional().default(true),
1191 |   includeFileContent: z.boolean().optional().default(false),
1192 |   minRelevanceScore: z.number().min(0).max(10).optional().default(1),
1193 |   orderBy: z.enum(['relevance', 'recency']).optional().default('relevance'),
1194 |   limit: z.number().min(1).max(1000).optional().default(50),
1195 |   includeDebugInfo: z.boolean().optional().default(false)
1196 | });
1197 | 
1198 | export type SearchConversationsByProjectInput = z.infer<typeof searchConversationsByProjectSchema>;
1199 | 
1200 | // Output type for search_conversations_by_project tool
1201 | export interface SearchConversationsByProjectOutput {
1202 |   conversations: Array<{
1203 |     composerId: string;
1204 |     format: 'legacy' | 'modern';
1205 |     messageCount: number;
1206 |     hasCodeBlocks: boolean;
1207 |     relevantFiles: string[];
1208 |     attachedFolders: string[];
1209 |     firstMessage?: string;
1210 |     size: number;
1211 |     relevanceScore: number;
1212 |     matchDetails?: {
1213 |       exactPathMatch: boolean;
1214 |       partialPathMatch: boolean;
1215 |       filePathMatch: boolean;
1216 |       fuzzyMatch: boolean;
1217 |       matchedPaths: string[];
1218 |       matchedFiles: string[];
1219 |     };
1220 |   }>;
1221 |   totalFound: number;
1222 |   searchQuery: string;
1223 |   searchOptions: {
1224 |     fuzzyMatch: boolean;
1225 |     includePartialPaths: boolean;
1226 |     includeFileContent: boolean;
1227 |     minRelevanceScore: number;
1228 |     orderBy: string;
1229 |     limit: number;
1230 |   };
1231 |   debugInfo?: {
1232 |     totalConversationsScanned: number;
1233 |     averageRelevanceScore: number;
1234 |     matchTypeDistribution: {
1235 |       exactPath: number;
1236 |       partialPath: number;
1237 |       filePath: number;
1238 |       fuzzy: number;
1239 |     };
1240 |   };
1241 | }
1242 | 
1243 | /**
1244 |  * Enhanced project search with fuzzy matching and flexible path matching
1245 |  */
1246 | export async function searchConversationsByProject(input: SearchConversationsByProjectInput): Promise<SearchConversationsByProjectOutput> {
1247 |   // Validate input
1248 |   const validatedInput = searchConversationsByProjectSchema.parse(input);
1249 | 
1250 |   // Create database reader
1251 |   const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath();
1252 |   const reader = new CursorDatabaseReader({
1253 |     dbPath,
1254 |     minConversationSize: 1000 // Lower threshold for broader search
1255 |   });
1256 | 
1257 |   try {
1258 |     // Connect to database
1259 |     await reader.connect();
1260 | 
1261 |     // Get all conversations for flexible searching
1262 |     const allConversationIds = await reader.getConversationIds({
1263 |       format: 'both',
1264 |       minLength: 1000
1265 |     });
1266 | 
1267 |     const results: Array<{
1268 |       composerId: string;
1269 |       format: 'legacy' | 'modern';
1270 |       messageCount: number;
1271 |       hasCodeBlocks: boolean;
1272 |       relevantFiles: string[];
1273 |       attachedFolders: string[];
1274 |       firstMessage?: string;
1275 |       size: number;
1276 |       relevanceScore: number;
1277 |       matchDetails?: any;
1278 |     }> = [];
1279 | 
1280 |     let totalScanned = 0;
1281 |     let matchTypeDistribution = {
1282 |       exactPath: 0,
1283 |       partialPath: 0,
1284 |       filePath: 0,
1285 |       fuzzy: 0
1286 |     };
1287 | 
1288 |     // Process conversations in batches to avoid memory issues
1289 |     const batchSize = 100;
1290 |     for (let i = 0; i < allConversationIds.length; i += batchSize) {
1291 |       const batch = allConversationIds.slice(i, i + batchSize);
1292 | 
1293 |       for (const composerId of batch) {
1294 |         totalScanned++;
1295 | 
1296 |         try {
1297 |           const conversation = await reader.getConversationById(composerId);
1298 |           if (!conversation) continue;
1299 | 
1300 |           // For modern format conversations, we need to resolve bubble messages to get file paths
1301 |           let enrichedConversation = conversation as any;
1302 | 
1303 |           if (conversation.hasOwnProperty('_v')) {
1304 |             // Modern format - resolve bubble messages
1305 |             const headers = (conversation as any).fullConversationHeadersOnly || [];
1306 |             const bubbleMessages: any[] = [];
1307 | 
1308 |             // Resolve a few bubble messages to get file paths (limit to avoid performance issues)
1309 |             const maxBubblesToResolve = Math.min(headers.length, 10);
1310 |             for (let i = 0; i < maxBubblesToResolve; i++) {
1311 |               const header = headers[i];
1312 |               try {
1313 |                 const bubbleMessage = await reader.getBubbleMessage(composerId, header.bubbleId);
1314 |                 if (bubbleMessage) {
1315 |                   bubbleMessages.push(bubbleMessage);
1316 |                 }
1317 |               } catch (error) {
1318 |                 // Continue with other bubbles if one fails
1319 |                 console.error(`Failed to resolve bubble ${header.bubbleId}:`, error);
1320 |               }
1321 |             }
1322 | 
1323 |             // Add resolved messages to the conversation object for matching
1324 |             enrichedConversation = {
1325 |               ...conversation,
1326 |               messages: bubbleMessages
1327 |             };
1328 |           }
1329 | 
1330 |           const matchResult = calculateEnhancedProjectRelevance(
1331 |             enrichedConversation,
1332 |             validatedInput.projectQuery,
1333 |             {
1334 |               fuzzyMatch: validatedInput.fuzzyMatch,
1335 |               includePartialPaths: validatedInput.includePartialPaths,
1336 |               includeFileContent: validatedInput.includeFileContent
1337 |             }
1338 |           );
1339 | 
1340 |           if (matchResult.score >= validatedInput.minRelevanceScore) {
1341 |             const summary = await reader.getConversationSummary(composerId, {
1342 |               includeFirstMessage: true,
1343 |               maxFirstMessageLength: 100,
1344 |               includeFileList: true,
1345 |               includeCodeBlockCount: true
1346 |             });
1347 | 
1348 |             if (summary) {
1349 |               // Update match type distribution
1350 |               if (matchResult.details.exactPathMatch) matchTypeDistribution.exactPath++;
1351 |               if (matchResult.details.partialPathMatch) matchTypeDistribution.partialPath++;
1352 |               if (matchResult.details.filePathMatch) matchTypeDistribution.filePath++;
1353 |               if (matchResult.details.fuzzyMatch) matchTypeDistribution.fuzzy++;
1354 | 
1355 |               results.push({
1356 |                 composerId: summary.composerId,
1357 |                 format: summary.format,
1358 |                 messageCount: summary.messageCount,
1359 |                 hasCodeBlocks: summary.hasCodeBlocks,
1360 |                 relevantFiles: summary.relevantFiles,
1361 |                 attachedFolders: summary.attachedFolders,
1362 |                 firstMessage: summary.firstMessage,
1363 |                 size: summary.conversationSize,
1364 |                 relevanceScore: matchResult.score,
1365 |                 matchDetails: validatedInput.includeDebugInfo ? matchResult.details : undefined
1366 |               });
1367 |             }
1368 |           }
1369 |         } catch (error) {
1370 |           console.error(`Failed to process conversation ${composerId}:`, error);
1371 |           // Continue with other conversations
1372 |         }
1373 |       }
1374 |     }
1375 | 
1376 |     // Sort by relevance or recency
1377 |     if (validatedInput.orderBy === 'relevance') {
1378 |       results.sort((a, b) => b.relevanceScore - a.relevanceScore);
1379 |     } else {
1380 |       // For recency, we rely on the original ROWID order from getConversationIds
1381 |       // which is already in descending order (most recent first)
1382 |     }
1383 | 
1384 |     const limitedResults = results.slice(0, validatedInput.limit);
1385 | 
1386 |     const debugInfo = validatedInput.includeDebugInfo ? {
1387 |       totalConversationsScanned: totalScanned,
1388 |       averageRelevanceScore: results.length > 0 ? results.reduce((sum, r) => sum + r.relevanceScore, 0) / results.length : 0,
1389 |       matchTypeDistribution
1390 |     } : undefined;
1391 | 
1392 |     return {
1393 |       conversations: limitedResults,
1394 |       totalFound: results.length,
1395 |       searchQuery: validatedInput.projectQuery,
1396 |       searchOptions: {
1397 |         fuzzyMatch: validatedInput.fuzzyMatch,
1398 |         includePartialPaths: validatedInput.includePartialPaths,
1399 |         includeFileContent: validatedInput.includeFileContent,
1400 |         minRelevanceScore: validatedInput.minRelevanceScore,
1401 |         orderBy: validatedInput.orderBy,
1402 |         limit: validatedInput.limit
1403 |       },
1404 |       debugInfo
1405 |     };
1406 | 
1407 |   } finally {
1408 |     // Always close the database connection
1409 |     reader.close();
1410 |   }
1411 | }
1412 | 
1413 | /**
1414 |  * Calculate enhanced project relevance with fuzzy matching and flexible path matching
1415 |  */
1416 | function calculateEnhancedProjectRelevance(
1417 |   conversation: any,
1418 |   projectQuery: string,
1419 |   options: {
1420 |     fuzzyMatch: boolean;
1421 |     includePartialPaths: boolean;
1422 |     includeFileContent: boolean;
1423 |   }
1424 | ): {
1425 |   score: number;
1426 |   details: {
1427 |     exactPathMatch: boolean;
1428 |     partialPathMatch: boolean;
1429 |     filePathMatch: boolean;
1430 |     fuzzyMatch: boolean;
1431 |     matchedPaths: string[];
1432 |     matchedFiles: string[];
1433 |   };
1434 | } {
1435 |   let score = 0;
1436 |   const details = {
1437 |     exactPathMatch: false,
1438 |     partialPathMatch: false,
1439 |     filePathMatch: false,
1440 |     fuzzyMatch: false,
1441 |     matchedPaths: [] as string[],
1442 |     matchedFiles: [] as string[]
1443 |   };
1444 | 
1445 |   const queryLower = projectQuery.toLowerCase();
1446 |   const queryParts = queryLower.split(/[-_\s]+/); // Split on common separators
1447 | 
1448 |   // Helper function for fuzzy matching
1449 |   const fuzzyMatch = (text: string, query: string): number => {
1450 |     const textLower = text.toLowerCase();
1451 | 
1452 |     // Exact match
1453 |     if (textLower.includes(query)) return 10;
1454 | 
1455 |     // Check if all query parts are present
1456 |     const allPartsPresent = queryParts.every(part => textLower.includes(part));
1457 |     if (allPartsPresent) return 8;
1458 | 
1459 |     // Check for partial matches
1460 |     const partialMatches = queryParts.filter(part => textLower.includes(part)).length;
1461 |     if (partialMatches > 0) return (partialMatches / queryParts.length) * 6;
1462 | 
1463 |     // Levenshtein-like similarity for very fuzzy matching
1464 |     const similarity = calculateSimilarity(textLower, query);
1465 |     if (similarity > 0.6) return similarity * 4;
1466 | 
1467 |     return 0;
1468 |   };
1469 | 
1470 |   // Helper function to process files and folders
1471 |   const processFiles = (files: string[], scoreMultiplier: number = 1) => {
1472 |     if (!files || !Array.isArray(files)) return;
1473 | 
1474 |     for (const file of files) {
1475 |       if (typeof file === 'string') {
1476 |         const fileName = file.split('/').pop() || file;
1477 |         const filePath = file.toLowerCase();
1478 |         const fileNameLower = fileName.toLowerCase();
1479 | 
1480 |         // Check if file path contains project query
1481 |         if (filePath.includes(queryLower)) {
1482 |           score += 10 * scoreMultiplier;
1483 |           details.filePathMatch = true;
1484 |           details.matchedFiles.push(file);
1485 |         }
1486 |         // Check file name
1487 |         else if (fileNameLower.includes(queryLower)) {
1488 |           score += 8 * scoreMultiplier;
1489 |           details.filePathMatch = true;
1490 |           details.matchedFiles.push(file);
1491 |         }
1492 |         // Fuzzy match on file paths
1493 |         else if (options.fuzzyMatch) {
1494 |           const fuzzyScore = Math.max(
1495 |             fuzzyMatch(file, queryLower),
1496 |             fuzzyMatch(fileName, queryLower)
1497 |           );
1498 |           if (fuzzyScore > 0) {
1499 |             score += fuzzyScore * 0.5 * scoreMultiplier; // Lower weight for file matches
1500 |             details.fuzzyMatch = true;
1501 |             details.matchedFiles.push(file);
1502 |           }
1503 |         }
1504 |       }
1505 |     }
1506 |   };
1507 | 
1508 |   const processFolders = (folders: string[], scoreMultiplier: number = 1) => {
1509 |     if (!folders || !Array.isArray(folders)) return;
1510 | 
1511 |     for (const folder of folders) {
1512 |       if (typeof folder === 'string') {
1513 |         const folderName = folder.split('/').pop() || folder; // Get last part of path
1514 |         const folderLower = folder.toLowerCase();
1515 | 
1516 |         // Exact path match
1517 |         if (folderLower === queryLower || folderName.toLowerCase() === queryLower) {
1518 |           score += 20 * scoreMultiplier;
1519 |           details.exactPathMatch = true;
1520 |           details.matchedPaths.push(folder);
1521 |         }
1522 |         // Partial path match
1523 |         else if (options.includePartialPaths && (folderLower.includes(queryLower) || folderName.toLowerCase().includes(queryLower))) {
1524 |           score += 15 * scoreMultiplier;
1525 |           details.partialPathMatch = true;
1526 |           details.matchedPaths.push(folder);
1527 |         }
1528 |         // Fuzzy match
1529 |         else if (options.fuzzyMatch) {
1530 |           const fuzzyScore = Math.max(
1531 |             fuzzyMatch(folder, queryLower),
1532 |             fuzzyMatch(folderName, queryLower)
1533 |           );
1534 |           if (fuzzyScore > 0) {
1535 |             score += fuzzyScore * scoreMultiplier;
1536 |             details.fuzzyMatch = true;
1537 |             details.matchedPaths.push(folder);
1538 |           }
1539 |         }
1540 |       }
1541 |     }
1542 |   };
1543 | 
1544 |   // Check top-level attachedFoldersNew and relevantFiles (legacy format)
1545 |   processFolders(conversation.attachedFoldersNew);
1546 |   processFiles(conversation.relevantFiles);
1547 | 
1548 |   // Check legacy conversation messages
1549 |   if (conversation.conversation && Array.isArray(conversation.conversation)) {
1550 |     for (const message of conversation.conversation) {
1551 |       processFolders(message.attachedFoldersNew, 0.8);
1552 |       processFiles(message.relevantFiles, 0.8);
1553 | 
1554 |       // Check message content if enabled
1555 |       if (options.includeFileContent && message.text && typeof message.text === 'string') {
1556 |         const textLower = message.text.toLowerCase();
1557 |         if (textLower.includes(queryLower)) {
1558 |           score += 2; // Lower weight for content matches
1559 |         }
1560 |       }
1561 |     }
1562 |   }
1563 | 
1564 |   // Check modern format messages (this is the key fix!)
1565 |   if (conversation.messages && Array.isArray(conversation.messages)) {
1566 |     for (const message of conversation.messages) {
1567 |       processFolders(message.attachedFolders, 0.8);
1568 |       processFiles(message.relevantFiles, 0.8);
1569 | 
1570 |       // Check message content if enabled
1571 |       if (options.includeFileContent && message.text && typeof message.text === 'string') {
1572 |         const textLower = message.text.toLowerCase();
1573 |         if (textLower.includes(queryLower)) {
1574 |           score += 2; // Lower weight for content matches
1575 |         }
1576 |       }
1577 |     }
1578 |   }
1579 | 
1580 |   // Check modern format bubbles for additional context
1581 |   if (conversation._v && conversation.bubbles && Array.isArray(conversation.bubbles)) {
1582 |     for (const bubble of conversation.bubbles) {
1583 |       processFolders(bubble.attachedFoldersNew, 0.5);
1584 |       processFiles(bubble.relevantFiles, 0.5);
1585 |     }
1586 |   }
1587 | 
1588 |   return {
1589 |     score: Math.max(score, 0),
1590 |     details
1591 |   };
1592 | }
1593 | 
1594 | /**
1595 |  * Calculate string similarity (simplified Levenshtein-based)
1596 |  */
1597 | function calculateSimilarity(str1: string, str2: string): number {
1598 |   const longer = str1.length > str2.length ? str1 : str2;
1599 |   const shorter = str1.length > str2.length ? str2 : str1;
1600 | 
1601 |   if (longer.length === 0) return 1.0;
1602 | 
1603 |   const editDistance = levenshteinDistance(longer, shorter);
1604 |   return (longer.length - editDistance) / longer.length;
1605 | }
1606 | 
1607 | /**
1608 |  * Calculate Levenshtein distance between two strings
1609 |  */
1610 | function levenshteinDistance(str1: string, str2: string): number {
1611 |   const matrix = [];
1612 | 
1613 |   for (let i = 0; i <= str2.length; i++) {
1614 |     matrix[i] = [i];
1615 |   }
1616 | 
1617 |   for (let j = 0; j <= str1.length; j++) {
1618 |     matrix[0][j] = j;
1619 |   }
1620 | 
1621 |   for (let i = 1; i <= str2.length; i++) {
1622 |     for (let j = 1; j <= str1.length; j++) {
1623 |       if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
1624 |         matrix[i][j] = matrix[i - 1][j - 1];
1625 |       } else {
1626 |         matrix[i][j] = Math.min(
1627 |           matrix[i - 1][j - 1] + 1,
1628 |           matrix[i][j - 1] + 1,
1629 |           matrix[i - 1][j] + 1
1630 |         );
1631 |       }
1632 |     }
1633 |   }
1634 | 
1635 |   return matrix[str2.length][str1.length];
1636 | }
1637 | 
1638 | /**
1639 |  * Check if a conversation falls within the specified date range
1640 |  */
1641 | function checkConversationDateRange(conversation: any, startDate?: string, endDate?: string): boolean {
1642 |   if (!startDate && !endDate) return true;
1643 | 
1644 |   const start = startDate ? new Date(startDate) : new Date('1970-01-01');
1645 |   const end = endDate ? new Date(endDate) : new Date();
1646 | 
1647 |   // Check if conversation is legacy or modern format
1648 |   const isLegacy = conversation.conversation && Array.isArray(conversation.conversation);
1649 | 
1650 |   if (isLegacy) {
1651 |     // Legacy format: check timestamps in conversation.conversation array
1652 |     for (const message of conversation.conversation) {
1653 |       if (message.timestamp) {
1654 |         const messageDate = new Date(message.timestamp);
1655 |         if (messageDate >= start && messageDate <= end) {
1656 |           return true;
1657 |         }
1658 |       }
1659 |     }
1660 |   } else {
1661 |     // Modern format: would need to resolve bubble messages to check timestamps
1662 |     // For now, return true to include all modern conversations when date filtering
1663 |     // since resolving all bubble messages would be too expensive
1664 |     return true;
1665 |   }
1666 | 
1667 |   // If no valid timestamps found, include the conversation
1668 |   return true;
1669 | }
```

--------------------------------------------------------------------------------
/src/database/reader.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import Database from 'better-sqlite3';
   2 | import type {
   3 |   CursorConversation,
   4 |   LegacyCursorConversation,
   5 |   ModernCursorConversation,
   6 |   BubbleMessage,
   7 |   ConversationSummary,
   8 |   ConversationSearchResult,
   9 |   ConversationStats,
  10 |   ConversationFilters,
  11 |   SummaryOptions,
  12 |   DatabaseConfig,
  13 |   SearchMatch
  14 | } from './types.js';
  15 | import {
  16 |   isLegacyConversation,
  17 |   isModernConversation
  18 | } from './types.js';
  19 | import {
  20 |   validateDatabasePath,
  21 |   createDefaultDatabaseConfig,
  22 |   extractComposerIdFromKey,
  23 |   generateBubbleIdKey,
  24 |   sanitizeMinConversationSize,
  25 |   sanitizeLimit,
  26 |   createFilePatternLike,
  27 |   sanitizeSearchQuery
  28 | } from '../utils/database-utils.js';
  29 | import {
  30 |   DatabaseError,
  31 |   DatabaseConnectionError,
  32 |   ConversationNotFoundError,
  33 |   BubbleMessageNotFoundError,
  34 |   ConversationParseError,
  35 |   SearchError,
  36 |   ValidationError
  37 | } from '../utils/errors.js';
  38 | 
  39 | export class CursorDatabaseReader {
  40 |   private db: Database.Database | null = null;
  41 |   private config: DatabaseConfig;
  42 |   private cache: Map<string, any> = new Map();
  43 | 
  44 |   constructor(config?: Partial<DatabaseConfig>) {
  45 |     this.config = { ...createDefaultDatabaseConfig(), ...config };
  46 |   }
  47 | 
  48 |   /**
  49 |    * Initialize database connection
  50 |    */
  51 |   async connect(): Promise<void> {
  52 |     if (this.db) {
  53 |       return;
  54 |     }
  55 | 
  56 |     try {
  57 |       this.db = new Database(this.config.dbPath, { readonly: true });
  58 | 
  59 |       const testQuery = this.db.prepare('SELECT COUNT(*) as count FROM cursorDiskKV LIMIT 1');
  60 |       testQuery.get();
  61 |     } catch (error) {
  62 |       throw new DatabaseConnectionError(
  63 |         this.config.dbPath,
  64 |         error instanceof Error ? error : new Error(String(error))
  65 |       );
  66 |     }
  67 |   }
  68 | 
  69 |   /**
  70 |    * Close database connection
  71 |    */
  72 |   close(): void {
  73 |     if (this.db) {
  74 |       this.db.close();
  75 |       this.db = null;
  76 |     }
  77 |     this.cache.clear();
  78 |   }
  79 | 
  80 |   /**
  81 |    * Ensure database is connected
  82 |    */
  83 |   private ensureConnected(): void {
  84 |     if (!this.db) {
  85 |       throw new DatabaseError('Database not connected. Call connect() first.');
  86 |     }
  87 |   }
  88 | 
  89 |   /**
  90 |    * Get conversation IDs with optional filters (ordered by recency using ROWID)
  91 |    */
  92 |   async getConversationIds(filters?: ConversationFilters): Promise<string[]> {
  93 |     this.ensureConnected();
  94 | 
  95 |     try {
  96 |       const minLength = sanitizeMinConversationSize(filters?.minLength);
  97 |       const limit = sanitizeLimit(undefined, this.config.maxConversations);
  98 | 
  99 |       let whereConditions: string[] = [];
 100 |       let params: any[] = [];
 101 | 
 102 |       whereConditions.push("key LIKE 'composerData:%'");
 103 |       whereConditions.push('length(value) > ?');
 104 |       params.push(this.config.minConversationSize || 100);
 105 | 
 106 |       if (filters?.format && filters.format !== 'both') {
 107 |         if (filters.format === 'legacy') {
 108 |           whereConditions.push("value NOT LIKE '%\"_v\":%'");
 109 |         } else if (filters.format === 'modern') {
 110 |           whereConditions.push("value LIKE '%\"_v\":%'");
 111 |         }
 112 |       }
 113 | 
 114 |       if (filters?.projectPath) {
 115 |         // Check if it's a full path or just a project name
 116 |         const isFullPath = filters.projectPath.startsWith('/');
 117 | 
 118 |         if (isFullPath) {
 119 |           // For full paths, search in all three places
 120 |           whereConditions.push("(value LIKE ? OR value LIKE ? OR value LIKE ?)");
 121 |           params.push(`%"attachedFoldersNew":[%"${filters.projectPath}%`);
 122 |           params.push(`%"relevantFiles":[%"${filters.projectPath}%`);
 123 |           params.push(`%"fsPath":"${filters.projectPath}%`);
 124 |         } else {
 125 |           // For project names, we need to search for the project name in paths
 126 |           whereConditions.push("(value LIKE ? OR value LIKE ? OR value LIKE ?)");
 127 |           params.push(`%"attachedFoldersNew":[%"${filters.projectPath}%`);
 128 |           params.push(`%"relevantFiles":[%"${filters.projectPath}%`);
 129 |           params.push(`%"fsPath":"%/${filters.projectPath}/%`);
 130 |         }
 131 |       }
 132 | 
 133 |       if (filters?.filePattern) {
 134 |         whereConditions.push("value LIKE ?");
 135 |         params.push(`%"relevantFiles":[%"${filters.filePattern}%`);
 136 |       }
 137 | 
 138 |       if (filters?.relevantFiles && filters.relevantFiles.length > 0) {
 139 |         const fileConditions = filters.relevantFiles.map(() => "value LIKE ?");
 140 |         whereConditions.push(`(${fileConditions.join(' OR ')})`);
 141 |         filters.relevantFiles.forEach(file => {
 142 |           params.push(`%"relevantFiles":[%"${file}"%`);
 143 |         });
 144 |       }
 145 | 
 146 |       if (filters?.hasCodeBlocks) {
 147 |         whereConditions.push("value LIKE '%\"suggestedCodeBlocks\":[%'");
 148 |       }
 149 | 
 150 |       if (filters?.keywords && filters.keywords.length > 0) {
 151 |         const keywordConditions = filters.keywords.map(() => "value LIKE ?");
 152 |         whereConditions.push(`(${keywordConditions.join(' OR ')})`);
 153 |         filters.keywords.forEach(keyword => {
 154 |           params.push(`%${keyword}%`);
 155 |         });
 156 |       }
 157 | 
 158 |       const sql = `
 159 |         SELECT key FROM cursorDiskKV
 160 |         WHERE ${whereConditions.join(' AND ')}
 161 |         ORDER BY ROWID DESC
 162 |         LIMIT ?
 163 |       `;
 164 |       params.push(limit);
 165 | 
 166 |       const stmt = this.db!.prepare(sql);
 167 |       const rows = stmt.all(...params) as Array<{ key: string }>;
 168 | 
 169 |       return rows.map(row => extractComposerIdFromKey(row.key)).filter(Boolean) as string[];
 170 |     } catch (error) {
 171 |       throw new DatabaseError(`Failed to get conversation IDs: ${error instanceof Error ? error.message : 'Unknown error'}`);
 172 |     }
 173 |   }
 174 | 
 175 |   /**
 176 |    * Get conversation IDs filtered by project path with more precise JSON querying
 177 |    */
 178 |   async getConversationIdsByProject(
 179 |     projectPath: string,
 180 |     options?: {
 181 |       filePattern?: string;
 182 |       exactFilePath?: string;
 183 |       orderBy?: 'recency' | 'relevance';
 184 |       limit?: number;
 185 |       format?: 'legacy' | 'modern' | 'both';
 186 |       fuzzyMatch?: boolean;
 187 |     }
 188 |   ): Promise<Array<{ composerId: string; relevanceScore?: number }>> {
 189 |     this.ensureConnected();
 190 | 
 191 |     const limit = sanitizeLimit(options?.limit, 1000);
 192 |     const orderBy = options?.orderBy || 'recency';
 193 |     const fuzzyMatch = options?.fuzzyMatch ?? false;
 194 | 
 195 |     let sql = `
 196 |       SELECT key, value FROM cursorDiskKV
 197 |       WHERE key LIKE 'composerData:%'
 198 |       AND length(value) > ?
 199 |     `;
 200 | 
 201 |     const params: any[] = [this.config.minConversationSize || 5000];
 202 | 
 203 |     if (options?.format && options.format !== 'both') {
 204 |       if (options.format === 'legacy') {
 205 |         sql += ` AND value NOT LIKE '%"_v":%'`;
 206 |       } else if (options.format === 'modern') {
 207 |         sql += ` AND value LIKE '%"_v":%'`;
 208 |       }
 209 |     }
 210 | 
 211 |     if (fuzzyMatch) {
 212 |       sql += ` AND (
 213 |         (value LIKE '%"attachedFoldersNew":%' AND (
 214 |           value LIKE ? OR
 215 |           value LIKE ? OR
 216 |           value LIKE ?
 217 |         )) OR
 218 |         (value LIKE '%"context":%' AND value LIKE '%"fsPath":%' AND (
 219 |           value LIKE ? OR
 220 |           value LIKE ? OR
 221 |           value LIKE ?
 222 |         ))
 223 |       )`;
 224 | 
 225 |       const projectLower = projectPath.toLowerCase();
 226 |       const escapedProjectPath = projectPath.replace(/"/g, '\\"');
 227 |       const escapedProjectLower = projectLower.replace(/"/g, '\\"');
 228 | 
 229 |       // For attachedFoldersNew
 230 |       params.push(`%"${escapedProjectPath}"%`);
 231 |       params.push(`%"${escapedProjectLower}"%`);
 232 |       params.push(`%${escapedProjectPath}%`);
 233 | 
 234 |       // For context.fileSelections.uri.fsPath
 235 |       params.push(`%"fsPath":"%/${escapedProjectPath}/%`);
 236 |       params.push(`%"fsPath":"%/${escapedProjectLower}/%`);
 237 |       params.push(`%"fsPath":"%${escapedProjectPath}%`);
 238 |     } else {
 239 |       sql += ` AND (
 240 |         (value LIKE '%"attachedFoldersNew":%' AND (
 241 |           value LIKE ? OR
 242 |           value LIKE ?
 243 |         )) OR
 244 |         (value LIKE '%"context":%' AND value LIKE '%"fsPath":%' AND (
 245 |           value LIKE ? OR
 246 |           value LIKE ?
 247 |         ))
 248 |       )`;
 249 | 
 250 |       const escapedProjectPath = projectPath.replace(/"/g, '\\"');
 251 | 
 252 |       // For attachedFoldersNew
 253 |       params.push(`%"${escapedProjectPath}"%`);
 254 |       params.push(`%"${escapedProjectPath}/%"`);
 255 | 
 256 |       // For context.fileSelections.uri.fsPath
 257 |       params.push(`%"fsPath":"%/${escapedProjectPath}/%`);
 258 |       params.push(`%"fsPath":"%/${escapedProjectPath}/%`);
 259 |     }
 260 | 
 261 |     if (options?.filePattern) {
 262 |       const pattern = createFilePatternLike(options.filePattern);
 263 |       sql += ` AND value LIKE '%"relevantFiles":%' AND value LIKE ?`;
 264 |       params.push(`%${pattern}%`);
 265 |     }
 266 | 
 267 |     if (options?.exactFilePath) {
 268 |       const escapedFilePath = options.exactFilePath.replace(/"/g, '\\"');
 269 |       sql += ` AND value LIKE '%"relevantFiles":%' AND value LIKE ?`;
 270 |       params.push(`%"${escapedFilePath}"%`);
 271 |     }
 272 | 
 273 |     if (orderBy === 'recency') {
 274 |       sql += ` ORDER BY ROWID DESC`;
 275 |     } else {
 276 |       sql += ` ORDER BY ROWID DESC`;
 277 |     }
 278 | 
 279 |     sql += ` LIMIT ?`;
 280 |     params.push(limit);
 281 | 
 282 |     const stmt = this.db!.prepare(sql);
 283 |     const rows = stmt.all(...params) as Array<{ key: string; value: string }>;
 284 | 
 285 |     const results = rows.map(row => {
 286 |       const composerId = extractComposerIdFromKey(row.key);
 287 |       if (!composerId) return null;
 288 | 
 289 |       let relevanceScore = 1;
 290 | 
 291 |       if (orderBy === 'relevance') {
 292 |         try {
 293 |           const conversation = JSON.parse(row.value);
 294 |           relevanceScore = this.calculateProjectRelevanceScore(conversation, projectPath, options);
 295 |         } catch (error) {
 296 |           relevanceScore = 1;
 297 |         }
 298 |       }
 299 | 
 300 |       return {
 301 |         composerId,
 302 |         relevanceScore: orderBy === 'relevance' ? relevanceScore : undefined
 303 |       };
 304 |     }).filter(Boolean) as Array<{ composerId: string; relevanceScore?: number }>;
 305 | 
 306 |     if (orderBy === 'relevance') {
 307 |       results.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
 308 |     }
 309 | 
 310 |     return results;
 311 |   }
 312 | 
 313 |   /**
 314 |    * Extract project paths from conversation context field
 315 |    */
 316 |   private extractProjectPathsFromContext(conversation: any): string[] {
 317 |     const projectPaths = new Set<string>();
 318 | 
 319 |     // Check top-level context
 320 |     if (conversation.context?.fileSelections) {
 321 |       for (const selection of conversation.context.fileSelections) {
 322 |         const fsPath = selection.uri?.fsPath || selection.uri?.path;
 323 |         if (fsPath) {
 324 |           const projectName = this.extractProjectName(fsPath);
 325 |           if (projectName) {
 326 |             projectPaths.add(projectName);
 327 |             projectPaths.add(fsPath); // Also add full path for exact matching
 328 |           }
 329 |         }
 330 |       }
 331 |     }
 332 | 
 333 |     // Check message-level context for legacy format
 334 |     if (conversation.conversation && Array.isArray(conversation.conversation)) {
 335 |       for (const message of conversation.conversation) {
 336 |         if (message.context?.fileSelections) {
 337 |           for (const selection of message.context.fileSelections) {
 338 |             const fsPath = selection.uri?.fsPath || selection.uri?.path;
 339 |             if (fsPath) {
 340 |               const projectName = this.extractProjectName(fsPath);
 341 |               if (projectName) {
 342 |                 projectPaths.add(projectName);
 343 |                 projectPaths.add(fsPath); // Also add full path for exact matching
 344 |               }
 345 |             }
 346 |           }
 347 |         }
 348 |       }
 349 |     }
 350 | 
 351 |     return Array.from(projectPaths);
 352 |   }
 353 | 
 354 |     /**
 355 |    * Extract project name from file path
 356 |    */
 357 |   private extractProjectName(filePath: string): string {
 358 |     // Extract project name from path like "/Users/vladta/Projects/editor-elements/file.ts"
 359 |     const parts = filePath.split('/').filter(Boolean); // Remove empty parts
 360 | 
 361 |     // Look for "Projects" folder (case-insensitive)
 362 |     const projectsIndex = parts.findIndex(part => part.toLowerCase() === 'projects');
 363 |     if (projectsIndex >= 0 && projectsIndex < parts.length - 1) {
 364 |       return parts[projectsIndex + 1];
 365 |     }
 366 | 
 367 |     // Fallback: try to find common workspace patterns
 368 |     const workspacePatterns = ['workspace', 'repos', 'code', 'dev', 'development', 'src', 'work'];
 369 |     for (const pattern of workspacePatterns) {
 370 |       const patternIndex = parts.findIndex(part => part.toLowerCase() === pattern);
 371 |       if (patternIndex >= 0 && patternIndex < parts.length - 1) {
 372 |         return parts[patternIndex + 1];
 373 |       }
 374 |     }
 375 | 
 376 |     // For paths like /Users/username/project-name/..., take the project name
 377 |     // Skip common user directory patterns
 378 |     const skipPatterns = ['users', 'home', 'documents', 'desktop', 'downloads'];
 379 |     let candidateIndex = -1;
 380 | 
 381 |     for (let i = 0; i < parts.length - 1; i++) {
 382 |       const part = parts[i].toLowerCase();
 383 |       if (!skipPatterns.includes(part) && part.length > 1) {
 384 |         // This could be a project name if it's not a common system directory
 385 |         candidateIndex = i;
 386 |         break;
 387 |       }
 388 |     }
 389 | 
 390 |     if (candidateIndex >= 0 && candidateIndex < parts.length - 1) {
 391 |       // Take the next part after the candidate (likely the project name)
 392 |       return parts[candidateIndex + 1];
 393 |     }
 394 | 
 395 |     // Last resort: if we have at least 3 parts, take the one that's most likely a project
 396 |     if (parts.length >= 3) {
 397 |       // Skip the first two parts (usually /Users/username) and take the third
 398 |       return parts[2] || '';
 399 |     }
 400 | 
 401 |     return '';
 402 |   }
 403 | 
 404 |   /**
 405 |    * Calculate relevance score for project-based filtering
 406 |    */
 407 |   private calculateProjectRelevanceScore(
 408 |     conversation: any,
 409 |     projectPath: string,
 410 |     options?: {
 411 |       filePattern?: string;
 412 |       exactFilePath?: string;
 413 |     }
 414 |   ): number {
 415 |     let score = 0;
 416 | 
 417 |     // NEW: Check context field for project paths (highest priority)
 418 |     const contextProjectPaths = this.extractProjectPathsFromContext(conversation);
 419 |     for (const contextPath of contextProjectPaths) {
 420 |       if (contextPath === projectPath) {
 421 |         score += 15; // Highest score for exact context match
 422 |       } else if (contextPath.includes(projectPath) || projectPath.includes(contextPath)) {
 423 |         score += 10; // High score for partial context match
 424 |       }
 425 |     }
 426 | 
 427 |     // Check attachedFoldersNew for exact matches and path prefixes
 428 |     if (conversation.attachedFoldersNew && Array.isArray(conversation.attachedFoldersNew)) {
 429 |       for (const folder of conversation.attachedFoldersNew) {
 430 |         if (typeof folder === 'string') {
 431 |           if (folder === projectPath) {
 432 |             score += 10; // Exact match
 433 |           } else if (folder.startsWith(projectPath + '/')) {
 434 |             score += 5; // Subfolder match
 435 |           } else if (projectPath.startsWith(folder + '/')) {
 436 |             score += 3; // Parent folder match
 437 |           }
 438 |         }
 439 |       }
 440 |     }
 441 | 
 442 |     // Check relevantFiles for matches
 443 |     if (conversation.relevantFiles && Array.isArray(conversation.relevantFiles)) {
 444 |       for (const file of conversation.relevantFiles) {
 445 |         if (typeof file === 'string') {
 446 |           if (options?.exactFilePath && file === options.exactFilePath) {
 447 |             score += 8; // Exact file match
 448 |           } else if (file.startsWith(projectPath + '/')) {
 449 |             score += 2; // File in project
 450 |           }
 451 | 
 452 |           // File pattern matching
 453 |           if (options?.filePattern) {
 454 |             const pattern = options.filePattern.replace(/\*/g, '.*').replace(/\?/g, '.');
 455 |             const regex = new RegExp(pattern);
 456 |             if (regex.test(file)) {
 457 |               score += 1;
 458 |             }
 459 |           }
 460 |         }
 461 |       }
 462 |     }
 463 | 
 464 |     // Check legacy conversation messages for attachedFoldersNew and relevantFiles
 465 |     if (conversation.conversation && Array.isArray(conversation.conversation)) {
 466 |       for (const message of conversation.conversation) {
 467 |         if (message.attachedFoldersNew && Array.isArray(message.attachedFoldersNew)) {
 468 |           for (const folder of message.attachedFoldersNew) {
 469 |             if (typeof folder === 'string' && folder.startsWith(projectPath)) {
 470 |               score += 1;
 471 |             }
 472 |           }
 473 |         }
 474 |         if (message.relevantFiles && Array.isArray(message.relevantFiles)) {
 475 |           for (const file of message.relevantFiles) {
 476 |             if (typeof file === 'string' && file.startsWith(projectPath + '/')) {
 477 |               score += 1;
 478 |             }
 479 |           }
 480 |         }
 481 |       }
 482 |     }
 483 | 
 484 |     return Math.max(score, 1); // Minimum score of 1
 485 |   }
 486 | 
 487 |   /**
 488 |    * Get conversation by ID (handles both legacy and modern formats)
 489 |    */
 490 |   async getConversationById(composerId: string): Promise<CursorConversation | null> {
 491 |     this.ensureConnected();
 492 | 
 493 |     try {
 494 |       const cacheKey = `conversation:${composerId}`;
 495 |       if (this.config.cacheEnabled && this.cache.has(cacheKey)) {
 496 |         return this.cache.get(cacheKey);
 497 |       }
 498 | 
 499 |       const stmt = this.db!.prepare('SELECT value FROM cursorDiskKV WHERE key = ?');
 500 |       const row = stmt.get(`composerData:${composerId}`) as { value: string } | undefined;
 501 | 
 502 |       if (!row) {
 503 |         return null;
 504 |       }
 505 | 
 506 |       try {
 507 |         const conversation = JSON.parse(row.value) as CursorConversation;
 508 | 
 509 |         if (this.config.cacheEnabled) {
 510 |           this.cache.set(cacheKey, conversation);
 511 |         }
 512 | 
 513 |         return conversation;
 514 |       } catch (parseError) {
 515 |         throw new ConversationParseError(`Failed to parse conversation data`, composerId, parseError instanceof Error ? parseError : new Error(String(parseError)));
 516 |       }
 517 |     } catch (error) {
 518 |       if (error instanceof ConversationParseError) {
 519 |         throw error;
 520 |       }
 521 |       throw new DatabaseError(`Failed to get conversation ${composerId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
 522 |     }
 523 |   }
 524 | 
 525 |   /**
 526 |    * Get individual message by bubble ID (for modern format)
 527 |    */
 528 |   async getBubbleMessage(composerId: string, bubbleId: string): Promise<BubbleMessage | null> {
 529 |     this.ensureConnected();
 530 | 
 531 |     try {
 532 |       const cacheKey = `bubble:${composerId}:${bubbleId}`;
 533 |       if (this.config.cacheEnabled && this.cache.has(cacheKey)) {
 534 |         return this.cache.get(cacheKey);
 535 |       }
 536 | 
 537 |       const key = generateBubbleIdKey(composerId, bubbleId);
 538 |       const stmt = this.db!.prepare('SELECT value FROM cursorDiskKV WHERE key = ?');
 539 |       const row = stmt.get(key) as { value: string } | undefined;
 540 | 
 541 |       if (!row) {
 542 |         return null;
 543 |       }
 544 | 
 545 |       try {
 546 |         const message = JSON.parse(row.value) as BubbleMessage;
 547 | 
 548 |         if (this.config.cacheEnabled) {
 549 |           this.cache.set(cacheKey, message);
 550 |         }
 551 | 
 552 |         return message;
 553 |       } catch (parseError) {
 554 |         throw new ConversationParseError(`Failed to parse bubble message data`, composerId, parseError instanceof Error ? parseError : new Error(String(parseError)));
 555 |       }
 556 |     } catch (error) {
 557 |       if (error instanceof ConversationParseError) {
 558 |         throw error;
 559 |       }
 560 |       throw new DatabaseError(`Failed to get bubble message ${bubbleId} from conversation ${composerId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
 561 |     }
 562 |   }
 563 | 
 564 |   /**
 565 |    * Get conversation summary without full content
 566 |    */
 567 |   async getConversationSummary(composerId: string, options?: SummaryOptions): Promise<ConversationSummary | null> {
 568 |     this.ensureConnected();
 569 | 
 570 |     const conversation = await this.getConversationById(composerId);
 571 |     if (!conversation) {
 572 |       return null;
 573 |     }
 574 | 
 575 |     const format = isModernConversation(conversation) ? 'modern' : 'legacy';
 576 |     let messageCount = 0;
 577 |     let hasCodeBlocks = false;
 578 |     let codeBlockCount = 0;
 579 |     const relevantFiles = new Set<string>();
 580 |     const attachedFolders = new Set<string>();
 581 |     let firstMessage: string | undefined;
 582 |     let lastMessage: string | undefined;
 583 |     const conversationSize = JSON.stringify(conversation).length;
 584 |     let title: string | undefined;
 585 |     let aiGeneratedSummary: string | undefined;
 586 | 
 587 |     if (format === 'legacy') {
 588 |       const legacyConvo = conversation as LegacyCursorConversation;
 589 |       messageCount = legacyConvo.conversation.length;
 590 |       legacyConvo.conversation.forEach((msg, index) => {
 591 |         if (msg.suggestedCodeBlocks && msg.suggestedCodeBlocks.length > 0) {
 592 |           hasCodeBlocks = true;
 593 |           codeBlockCount += msg.suggestedCodeBlocks.length;
 594 |         }
 595 |         msg.relevantFiles?.forEach(file => relevantFiles.add(file));
 596 |         msg.attachedFoldersNew?.forEach(folder => attachedFolders.add(folder));
 597 |         if (index === 0) {
 598 |           firstMessage = msg.text;
 599 |         }
 600 |         lastMessage = msg.text;
 601 |       });
 602 |     } else {
 603 |       const modernConvo = conversation as ModernCursorConversation;
 604 |       messageCount = modernConvo.fullConversationHeadersOnly.length;
 605 |       title = modernConvo.name;
 606 |       aiGeneratedSummary = modernConvo.latestConversationSummary?.summary?.summary;
 607 | 
 608 |       // For modern conversations, we need to resolve bubbles to get details
 609 |       // This can be slow, so we only do it if necessary based on options
 610 |       const needsBubbleResolution = options?.includeFirstMessage || options?.includeLastMessage || options?.includeCodeBlockCount || options?.includeFileList;
 611 | 
 612 |       if (needsBubbleResolution && this.config.resolveBubblesAutomatically) {
 613 |         for (const header of modernConvo.fullConversationHeadersOnly) {
 614 |           const bubble = await this.getBubbleMessage(composerId, header.bubbleId);
 615 |           if (bubble) {
 616 |             if (bubble.suggestedCodeBlocks && bubble.suggestedCodeBlocks.length > 0) {
 617 |               hasCodeBlocks = true;
 618 |               codeBlockCount += bubble.suggestedCodeBlocks.length;
 619 |             }
 620 |             bubble.relevantFiles?.forEach(file => relevantFiles.add(file));
 621 |             bubble.attachedFoldersNew?.forEach(folder => attachedFolders.add(folder));
 622 | 
 623 |             if (!firstMessage) {
 624 |               firstMessage = bubble.text;
 625 |             }
 626 |             lastMessage = bubble.text;
 627 |           }
 628 |         }
 629 |       }
 630 |     }
 631 | 
 632 |     // Truncate messages if requested
 633 |     if (options?.includeFirstMessage && firstMessage) {
 634 |       firstMessage = firstMessage.substring(0, options.maxFirstMessageLength || 150);
 635 |     } else {
 636 |       firstMessage = undefined;
 637 |     }
 638 | 
 639 |     if (options?.includeLastMessage && lastMessage) {
 640 |       lastMessage = lastMessage.substring(0, options.maxLastMessageLength || 150);
 641 |     } else {
 642 |       lastMessage = undefined;
 643 |     }
 644 | 
 645 |     const summary: ConversationSummary = {
 646 |       composerId,
 647 |       format,
 648 |       messageCount,
 649 |       hasCodeBlocks,
 650 |       codeBlockCount: options?.includeCodeBlockCount ? codeBlockCount : 0,
 651 |       relevantFiles: options?.includeFileList ? Array.from(relevantFiles) : [],
 652 |       attachedFolders: options?.includeAttachedFolders ? Array.from(attachedFolders) : [],
 653 |       firstMessage,
 654 |       lastMessage,
 655 |       storedSummary: options?.includeStoredSummary ? conversation.text : undefined,
 656 |       storedRichText: options?.includeStoredSummary ? conversation.richText : undefined,
 657 |       title: options?.includeTitle ? title : undefined,
 658 |       aiGeneratedSummary: options?.includeAIGeneratedSummary ? aiGeneratedSummary : undefined,
 659 |       conversationSize
 660 |     };
 661 | 
 662 |     return summary;
 663 |   }
 664 | 
 665 |   /**
 666 |    * Search conversations by content (original method)
 667 |    */
 668 |   async searchConversations(query: string, options?: {
 669 |     includeCode?: boolean;
 670 |     contextLines?: number;
 671 |     maxResults?: number;
 672 |     searchBubbles?: boolean;
 673 |     searchType?: 'all' | 'summarization' | 'code' | 'files';
 674 |     format?: 'legacy' | 'modern' | 'both';
 675 |   }): Promise<ConversationSearchResult[]> {
 676 |     this.ensureConnected();
 677 | 
 678 |     const sanitizedQuery = sanitizeSearchQuery(query);
 679 |     const maxResults = sanitizeLimit(options?.maxResults, 100);
 680 |     const format = options?.format || 'both';
 681 | 
 682 |     // Build search patterns based on search type
 683 |     let searchPatterns: string[] = [];
 684 | 
 685 |     switch (options?.searchType) {
 686 |       case 'summarization':
 687 |         searchPatterns = ['%summarization%', '%summarize%', '%summary%'];
 688 |         break;
 689 |       case 'code':
 690 |         searchPatterns = ['%suggestedCodeBlocks%', '%```%'];
 691 |         break;
 692 |       case 'files':
 693 |         searchPatterns = ['%relevantFiles%', '%attachedFoldersNew%'];
 694 |         break;
 695 |       default:
 696 |         searchPatterns = [`%${sanitizedQuery}%`];
 697 |     }
 698 | 
 699 |     let sql = `
 700 |       SELECT key, value FROM cursorDiskKV
 701 |       WHERE key LIKE 'composerData:%'
 702 |       AND length(value) > ?
 703 |       AND (${searchPatterns.map(() => 'value LIKE ?').join(' OR ')})
 704 |     `;
 705 | 
 706 |     const params: any[] = [
 707 |       this.config.minConversationSize || 5000,
 708 |       ...searchPatterns
 709 |     ];
 710 | 
 711 |     // Add format filter
 712 |     if (format === 'legacy') {
 713 |       sql += ` AND value NOT LIKE '%"_v":%'`;
 714 |     } else if (format === 'modern') {
 715 |       sql += ` AND value LIKE '%"_v":%'`;
 716 |     }
 717 | 
 718 |     sql += ` ORDER BY ROWID DESC LIMIT ?`;
 719 |     params.push(maxResults);
 720 | 
 721 |     const stmt = this.db!.prepare(sql);
 722 |     const rows = stmt.all(...params) as Array<{ key: string; value: string }>;
 723 | 
 724 |     const results: ConversationSearchResult[] = [];
 725 | 
 726 |     for (const row of rows) {
 727 |       const composerId = extractComposerIdFromKey(row.key);
 728 |       if (!composerId) continue;
 729 | 
 730 |       try {
 731 |         const conversation = JSON.parse(row.value) as CursorConversation;
 732 |         const conversationFormat = isLegacyConversation(conversation) ? 'legacy' : 'modern';
 733 |         const matches: SearchMatch[] = [];
 734 | 
 735 |         if (isLegacyConversation(conversation)) {
 736 |           // Search in legacy format messages
 737 |           conversation.conversation.forEach((message, index) => {
 738 |             if (message.text.toLowerCase().includes(sanitizedQuery.toLowerCase())) {
 739 |               matches.push({
 740 |                 messageIndex: index,
 741 |                 text: message.text,
 742 |                 context: this.extractContext(message.text, sanitizedQuery, options?.contextLines || 3),
 743 |                 type: message.type
 744 |               });
 745 |             }
 746 |           });
 747 |         } else if (isModernConversation(conversation) && options?.searchBubbles) {
 748 |           // Search in modern format bubble messages
 749 |           const headers = conversation.fullConversationHeadersOnly || [];
 750 | 
 751 |           for (let index = 0; index < headers.length; index++) {
 752 |             const header = headers[index];
 753 |             try {
 754 |               const bubbleMessage = await this.getBubbleMessage(composerId, header.bubbleId);
 755 |               if (bubbleMessage && bubbleMessage.text.toLowerCase().includes(sanitizedQuery.toLowerCase())) {
 756 |                 matches.push({
 757 |                   messageIndex: index,
 758 |                   bubbleId: header.bubbleId,
 759 |                   text: bubbleMessage.text,
 760 |                   context: this.extractContext(bubbleMessage.text, sanitizedQuery, options?.contextLines || 3),
 761 |                   type: bubbleMessage.type
 762 |                 });
 763 |               }
 764 |             } catch (error) {
 765 |               console.error(`Failed to resolve bubble ${header.bubbleId} during search:`, error);
 766 |             }
 767 |           }
 768 |         }
 769 | 
 770 |         if (matches.length > 0) {
 771 |           let relevantFiles: string[] = [];
 772 |           let attachedFolders: string[] = [];
 773 | 
 774 |           if (isLegacyConversation(conversation)) {
 775 |             for (const message of conversation.conversation) {
 776 |               if (message.relevantFiles) relevantFiles.push(...message.relevantFiles);
 777 |               if (message.attachedFoldersNew) attachedFolders.push(...message.attachedFoldersNew);
 778 |             }
 779 |           } else if (isModernConversation(conversation) && options?.searchBubbles) {
 780 |             // For modern format, collect files from bubble messages
 781 |             const headers = conversation.fullConversationHeadersOnly || [];
 782 |             for (const header of headers) {
 783 |               try {
 784 |                 const bubbleMessage = await this.getBubbleMessage(composerId, header.bubbleId);
 785 |                 if (bubbleMessage) {
 786 |                   if (bubbleMessage.relevantFiles) relevantFiles.push(...bubbleMessage.relevantFiles);
 787 |                   if (bubbleMessage.attachedFoldersNew) attachedFolders.push(...bubbleMessage.attachedFoldersNew);
 788 |                 }
 789 |               } catch (error) {
 790 |                 console.error(`Failed to resolve bubble ${header.bubbleId} for file extraction:`, error);
 791 |               }
 792 |             }
 793 |           }
 794 | 
 795 |           results.push({
 796 |             composerId,
 797 |             format: conversationFormat,
 798 |             matches,
 799 |             relevantFiles: Array.from(new Set(relevantFiles)),
 800 |             attachedFolders: Array.from(new Set(attachedFolders))
 801 |           });
 802 |         }
 803 |       } catch (error) {
 804 |         console.error(`Failed to parse conversation ${composerId} during search:`, error);
 805 |       }
 806 |     }
 807 | 
 808 |     return results;
 809 |   }
 810 | 
 811 |   /**
 812 |    * Enhanced search with multi-keyword and LIKE pattern support
 813 |    */
 814 |   async searchConversationsEnhanced(options: {
 815 |     query?: string;
 816 |     keywords?: string[];
 817 |     keywordOperator?: 'AND' | 'OR';
 818 |     likePattern?: string;
 819 |     startDate?: string;
 820 |     endDate?: string;
 821 |     includeCode?: boolean;
 822 |     contextLines?: number;
 823 |     maxResults?: number;
 824 |     searchBubbles?: boolean;
 825 |     searchType?: 'all' | 'summarization' | 'code' | 'files';
 826 |     format?: 'legacy' | 'modern' | 'both';
 827 |   }): Promise<ConversationSearchResult[]> {
 828 |     this.ensureConnected();
 829 | 
 830 |     const maxResults = sanitizeLimit(options?.maxResults, 100);
 831 |     const format = options?.format || 'both';
 832 | 
 833 |     // Build search conditions for SQL
 834 |     let searchConditions: string[] = [];
 835 |     let searchParams: any[] = [];
 836 | 
 837 |     // Handle simple query
 838 |     if (options.query) {
 839 |       const sanitizedQuery = sanitizeSearchQuery(options.query);
 840 | 
 841 |       switch (options?.searchType) {
 842 |         case 'summarization':
 843 |           searchConditions.push('(value LIKE ? OR value LIKE ? OR value LIKE ?)');
 844 |           searchParams.push('%summarization%', '%summarize%', '%summary%');
 845 |           break;
 846 |         case 'code':
 847 |           searchConditions.push('(value LIKE ? OR value LIKE ?)');
 848 |           searchParams.push('%suggestedCodeBlocks%', '%```%');
 849 |           break;
 850 |         case 'files':
 851 |           searchConditions.push('(value LIKE ? OR value LIKE ?)');
 852 |           searchParams.push('%relevantFiles%', '%attachedFoldersNew%');
 853 |           break;
 854 |         default:
 855 |           searchConditions.push('value LIKE ?');
 856 |           searchParams.push(`%${sanitizedQuery}%`);
 857 |       }
 858 |     }
 859 | 
 860 |     // Handle multi-keyword search
 861 |     if (options.keywords && options.keywords.length > 0) {
 862 |       const keywordConditions = options.keywords.map(() => 'value LIKE ?');
 863 |       const operator = options.keywordOperator === 'AND' ? ' AND ' : ' OR ';
 864 |       searchConditions.push(`(${keywordConditions.join(operator)})`);
 865 | 
 866 |       options.keywords.forEach(keyword => {
 867 |         const sanitizedKeyword = sanitizeSearchQuery(keyword);
 868 |         searchParams.push(`%${sanitizedKeyword}%`);
 869 |       });
 870 |     }
 871 | 
 872 |     // Handle LIKE pattern search
 873 |     if (options.likePattern) {
 874 |       searchConditions.push('value LIKE ?');
 875 |       searchParams.push(options.likePattern);
 876 |     }
 877 | 
 878 |     // If no search conditions, return empty results
 879 |     if (searchConditions.length === 0) {
 880 |       return [];
 881 |     }
 882 | 
 883 |     // Build the complete SQL query
 884 |     let sql = `
 885 |       SELECT key, value FROM cursorDiskKV
 886 |       WHERE key LIKE 'composerData:%'
 887 |       AND length(value) > ?
 888 |       AND (${searchConditions.join(' OR ')})
 889 |     `;
 890 | 
 891 |     const params: any[] = [
 892 |       this.config.minConversationSize || 5000,
 893 |       ...searchParams
 894 |     ];
 895 | 
 896 |     // Add format filter
 897 |     if (format === 'legacy') {
 898 |       sql += ` AND value NOT LIKE '%"_v":%'`;
 899 |     } else if (format === 'modern') {
 900 |       sql += ` AND value LIKE '%"_v":%'`;
 901 |     }
 902 | 
 903 |     sql += ` ORDER BY ROWID DESC LIMIT ?`;
 904 |     params.push(maxResults);
 905 | 
 906 |     const stmt = this.db!.prepare(sql);
 907 |     const rows = stmt.all(...params) as Array<{ key: string; value: string }>;
 908 | 
 909 |     const results: ConversationSearchResult[] = [];
 910 | 
 911 |     // Process each conversation
 912 |     for (const row of rows) {
 913 |       const composerId = extractComposerIdFromKey(row.key);
 914 |       if (!composerId) continue;
 915 | 
 916 |       try {
 917 |         const conversation = JSON.parse(row.value) as CursorConversation;
 918 |         const conversationFormat = isLegacyConversation(conversation) ? 'legacy' : 'modern';
 919 |         const matches: SearchMatch[] = [];
 920 | 
 921 |         // For message-level search, we need to check individual messages
 922 |         if (options.query || (options.keywords && options.keywords.length > 0)) {
 923 |           const searchTerms: string[] = [];
 924 |           if (options.query) searchTerms.push(options.query);
 925 |           if (options.keywords) searchTerms.push(...options.keywords);
 926 | 
 927 |           if (isLegacyConversation(conversation)) {
 928 |             // Search in legacy format messages
 929 |             conversation.conversation.forEach((message, index) => {
 930 |               const messageText = message.text.toLowerCase();
 931 | 
 932 |               for (const term of searchTerms) {
 933 |                 const sanitizedTerm = sanitizeSearchQuery(term).toLowerCase();
 934 |                 if (messageText.includes(sanitizedTerm)) {
 935 |                   matches.push({
 936 |                     messageIndex: index,
 937 |                     text: message.text,
 938 |                     context: this.extractContext(message.text, term, options?.contextLines || 3),
 939 |                     type: message.type
 940 |                   });
 941 |                   break; // Only add one match per message
 942 |                 }
 943 |               }
 944 |             });
 945 |           } else if (isModernConversation(conversation) && options?.searchBubbles) {
 946 |             // Search in modern format bubble messages
 947 |             const headers = conversation.fullConversationHeadersOnly || [];
 948 | 
 949 |             for (let index = 0; index < headers.length; index++) {
 950 |               const header = headers[index];
 951 |               try {
 952 |                 const bubbleMessage = await this.getBubbleMessage(composerId, header.bubbleId);
 953 |                 if (bubbleMessage) {
 954 |                   const messageText = bubbleMessage.text.toLowerCase();
 955 | 
 956 |                   for (const term of searchTerms) {
 957 |                     const sanitizedTerm = sanitizeSearchQuery(term).toLowerCase();
 958 |                     if (messageText.includes(sanitizedTerm)) {
 959 |                       matches.push({
 960 |                         messageIndex: index,
 961 |                         bubbleId: header.bubbleId,
 962 |                         text: bubbleMessage.text,
 963 |                         context: this.extractContext(bubbleMessage.text, term, options?.contextLines || 3),
 964 |                         type: bubbleMessage.type
 965 |                       });
 966 |                       break; // Only add one match per message
 967 |                     }
 968 |                   }
 969 |                 }
 970 |               } catch (error) {
 971 |                 console.error(`Failed to resolve bubble ${header.bubbleId} during search:`, error);
 972 |               }
 973 |             }
 974 |           }
 975 |         } else {
 976 |           // For LIKE pattern only, we already filtered at SQL level, so include all
 977 |           matches.push({
 978 |             messageIndex: 0,
 979 |             text: 'Pattern match found in conversation data',
 980 |             context: 'LIKE pattern matched conversation content',
 981 |             type: 1
 982 |           });
 983 |         }
 984 | 
 985 |         if (matches.length > 0) {
 986 |           let relevantFiles: string[] = [];
 987 |           let attachedFolders: string[] = [];
 988 | 
 989 |           if (isLegacyConversation(conversation)) {
 990 |             for (const message of conversation.conversation) {
 991 |               if (message.relevantFiles) relevantFiles.push(...message.relevantFiles);
 992 |               if (message.attachedFoldersNew) attachedFolders.push(...message.attachedFoldersNew);
 993 |             }
 994 |           } else if (isModernConversation(conversation) && options?.searchBubbles) {
 995 |             // For modern format, collect files from bubble messages
 996 |             const headers = conversation.fullConversationHeadersOnly || [];
 997 |             for (const header of headers) {
 998 |               try {
 999 |                 const bubbleMessage = await this.getBubbleMessage(composerId, header.bubbleId);
1000 |                 if (bubbleMessage) {
1001 |                   if (bubbleMessage.relevantFiles) relevantFiles.push(...bubbleMessage.relevantFiles);
1002 |                   if (bubbleMessage.attachedFoldersNew) attachedFolders.push(...bubbleMessage.attachedFoldersNew);
1003 |                 }
1004 |               } catch (error) {
1005 |                 console.error(`Failed to resolve bubble ${header.bubbleId} for file extraction:`, error);
1006 |               }
1007 |             }
1008 |           }
1009 | 
1010 |           results.push({
1011 |             composerId,
1012 |             format: conversationFormat,
1013 |             matches,
1014 |             relevantFiles: Array.from(new Set(relevantFiles)),
1015 |             attachedFolders: Array.from(new Set(attachedFolders))
1016 |           });
1017 |         }
1018 |       } catch (error) {
1019 |         console.error(`Failed to parse conversation ${composerId} during enhanced search:`, error);
1020 |       }
1021 |     }
1022 | 
1023 |     // Apply date filtering if specified (post-query filtering due to unreliable timestamps)
1024 |     if (options.startDate || options.endDate) {
1025 |       const filteredResults = await this.filterResultsByDateRange(results, options.startDate, options.endDate);
1026 |       return filteredResults;
1027 |     }
1028 | 
1029 |     return results;
1030 |   }
1031 | 
1032 |   /**
1033 |    * Get conversation statistics
1034 |    */
1035 |   async getConversationStats(): Promise<ConversationStats> {
1036 |     this.ensureConnected();
1037 | 
1038 |     const sql = `
1039 |       SELECT key, length(value) as size, value FROM cursorDiskKV
1040 |       WHERE key LIKE 'composerData:%'
1041 |       AND length(value) > ?
1042 |     `;
1043 | 
1044 |     const stmt = this.db!.prepare(sql);
1045 |     const rows = stmt.all(this.config.minConversationSize || 5000) as Array<{
1046 |       key: string;
1047 |       size: number;
1048 |       value: string
1049 |     }>;
1050 | 
1051 |     let legacyCount = 0;
1052 |     let modernCount = 0;
1053 |     let totalSize = 0;
1054 |     let conversationsWithCode = 0;
1055 |     const fileCount = new Map<string, number>();
1056 |     const folderCount = new Map<string, number>();
1057 | 
1058 |     for (const row of rows) {
1059 |       totalSize += row.size;
1060 | 
1061 |       try {
1062 |         const conversation = JSON.parse(row.value) as CursorConversation;
1063 | 
1064 |         if (isLegacyConversation(conversation)) {
1065 |           legacyCount++;
1066 | 
1067 |           let hasCode = false;
1068 |           for (const message of conversation.conversation) {
1069 |             if (message.suggestedCodeBlocks && message.suggestedCodeBlocks.length > 0) {
1070 |               hasCode = true;
1071 |             }
1072 | 
1073 |             if (message.relevantFiles) {
1074 |               for (const file of message.relevantFiles) {
1075 |                 fileCount.set(file, (fileCount.get(file) || 0) + 1);
1076 |               }
1077 |             }
1078 | 
1079 |             if (message.attachedFoldersNew) {
1080 |               for (const folder of message.attachedFoldersNew) {
1081 |                 folderCount.set(folder, (folderCount.get(folder) || 0) + 1);
1082 |               }
1083 |             }
1084 |           }
1085 | 
1086 |           if (hasCode) conversationsWithCode++;
1087 |         } else if (isModernConversation(conversation)) {
1088 |           modernCount++;
1089 |           // Note: For modern format, we'd need to resolve bubbles to get accurate stats
1090 |           // This is a simplified version for performance
1091 |         }
1092 |       } catch (error) {
1093 |         console.error(`Failed to parse conversation during stats:`, error);
1094 |       }
1095 |     }
1096 | 
1097 |     const totalConversations = legacyCount + modernCount;
1098 |     const averageSize = totalConversations > 0 ? totalSize / totalConversations : 0;
1099 | 
1100 |     // Get top files and folders
1101 |     const mostCommonFiles = Array.from(fileCount.entries())
1102 |       .sort((a, b) => b[1] - a[1])
1103 |       .slice(0, 10)
1104 |       .map(([file, count]) => ({ file, count }));
1105 | 
1106 |     const mostCommonFolders = Array.from(folderCount.entries())
1107 |       .sort((a, b) => b[1] - a[1])
1108 |       .slice(0, 10)
1109 |       .map(([folder, count]) => ({ folder, count }));
1110 | 
1111 |     return {
1112 |       totalConversations,
1113 |       legacyFormatCount: legacyCount,
1114 |       modernFormatCount: modernCount,
1115 |       averageConversationSize: Math.round(averageSize),
1116 |       totalConversationsWithCode: conversationsWithCode,
1117 |       mostCommonFiles,
1118 |       mostCommonFolders
1119 |     };
1120 |   }
1121 | 
1122 |   /**
1123 |    * Detect conversation format
1124 |    */
1125 |   async detectConversationFormat(composerId: string): Promise<'legacy' | 'modern' | null> {
1126 |     const conversation = await this.getConversationById(composerId);
1127 |     if (!conversation) return null;
1128 | 
1129 |     return isLegacyConversation(conversation) ? 'legacy' : 'modern';
1130 |   }
1131 | 
1132 |   /**
1133 |    * Get conversation summaries for analytics
1134 |    */
1135 |   async getConversationSummariesForAnalytics(
1136 |     conversationIds: string[],
1137 |     options?: { includeCodeBlocks?: boolean }
1138 |   ): Promise<ConversationSummary[]> {
1139 |     this.ensureConnected();
1140 | 
1141 |     const summaries: ConversationSummary[] = [];
1142 | 
1143 |     for (const composerId of conversationIds) {
1144 |       try {
1145 |         const summary = await this.getConversationSummary(composerId, {
1146 |           includeFirstMessage: true,
1147 |           includeCodeBlockCount: true,
1148 |           includeFileList: true,
1149 |           includeAttachedFolders: true,
1150 |           maxFirstMessageLength: 150
1151 |         });
1152 | 
1153 |         if (summary) {
1154 |           summaries.push(summary);
1155 |         }
1156 |       } catch (error) {
1157 |         console.error(`Failed to get summary for conversation ${composerId}:`, error);
1158 |       }
1159 |     }
1160 | 
1161 |     return summaries;
1162 |   }
1163 | 
1164 |   /**
1165 |    * Get conversations with code blocks for language analysis
1166 |    */
1167 |   async getConversationsWithCodeBlocks(
1168 |     conversationIds: string[]
1169 |   ): Promise<Array<{
1170 |     composerId: string;
1171 |     codeBlocks: Array<{ language: string; code: string; filename?: string }>;
1172 |   }>> {
1173 |     this.ensureConnected();
1174 | 
1175 |     const conversationsWithCode: Array<{
1176 |       composerId: string;
1177 |       codeBlocks: Array<{ language: string; code: string; filename?: string }>;
1178 |     }> = [];
1179 | 
1180 |     for (const composerId of conversationIds) {
1181 |       try {
1182 |         const conversation = await this.getConversationById(composerId);
1183 |         if (!conversation) continue;
1184 | 
1185 |         const codeBlocks: Array<{ language: string; code: string; filename?: string }> = [];
1186 | 
1187 |         if (isLegacyConversation(conversation)) {
1188 |           for (const message of conversation.conversation) {
1189 |             if (message.suggestedCodeBlocks) {
1190 |               for (const block of message.suggestedCodeBlocks) {
1191 |                 codeBlocks.push({
1192 |                   language: block.language || 'text',
1193 |                   code: block.code,
1194 |                   filename: block.filename
1195 |                 });
1196 |               }
1197 |             }
1198 |           }
1199 |         } else if (isModernConversation(conversation)) {
1200 |           // For modern format, resolve bubble messages to get code blocks
1201 |           const headers = conversation.fullConversationHeadersOnly || [];
1202 |           for (const header of headers) {
1203 |             try {
1204 |               const bubbleMessage = await this.getBubbleMessage(composerId, header.bubbleId);
1205 |               if (bubbleMessage && bubbleMessage.suggestedCodeBlocks) {
1206 |                 for (const block of bubbleMessage.suggestedCodeBlocks) {
1207 |                   codeBlocks.push({
1208 |                     language: block.language || 'text',
1209 |                     code: block.code,
1210 |                     filename: block.filename
1211 |                   });
1212 |                 }
1213 |               }
1214 |             } catch (error) {
1215 |               console.error(`Failed to resolve bubble ${header.bubbleId} for code blocks:`, error);
1216 |             }
1217 |           }
1218 |         }
1219 | 
1220 |         if (codeBlocks.length > 0) {
1221 |           conversationsWithCode.push({
1222 |             composerId,
1223 |             codeBlocks
1224 |           });
1225 |         }
1226 |       } catch (error) {
1227 |         console.error(`Failed to extract code blocks from conversation ${composerId}:`, error);
1228 |       }
1229 |     }
1230 | 
1231 |     return conversationsWithCode;
1232 |   }
1233 | 
1234 |   /**
1235 |    * Extract elements from conversations for generic extraction
1236 |    */
1237 |   async extractConversationElements(
1238 |     conversationIds: string[],
1239 |     elements: Array<'files' | 'folders' | 'languages' | 'codeblocks' | 'metadata' | 'structure'>,
1240 |     options?: {
1241 |       includeContext?: boolean;
1242 |       filters?: {
1243 |         minCodeLength?: number;
1244 |         fileExtensions?: string[];
1245 |         languages?: string[];
1246 |       };
1247 |     }
1248 |   ): Promise<Array<{
1249 |     composerId: string;
1250 |     format: 'legacy' | 'modern';
1251 |     elements: any;
1252 |   }>> {
1253 |     this.ensureConnected();
1254 | 
1255 |     const results: Array<{
1256 |       composerId: string;
1257 |       format: 'legacy' | 'modern';
1258 |       elements: any;
1259 |     }> = [];
1260 | 
1261 |     for (const composerId of conversationIds) {
1262 |       try {
1263 |         const conversation = await this.getConversationById(composerId);
1264 |         if (!conversation) continue;
1265 | 
1266 |         const format = isLegacyConversation(conversation) ? 'legacy' : 'modern';
1267 |         const extractedElements: any = {};
1268 | 
1269 |         // Extract files
1270 |         if (elements.includes('files')) {
1271 |           extractedElements.files = await this.extractFiles(conversation, options);
1272 |         }
1273 | 
1274 |         // Extract folders
1275 |         if (elements.includes('folders')) {
1276 |           extractedElements.folders = await this.extractFolders(conversation, options);
1277 |         }
1278 | 
1279 |         // Extract languages
1280 |         if (elements.includes('languages')) {
1281 |           extractedElements.languages = await this.extractLanguages(conversation, options);
1282 |         }
1283 | 
1284 |         // Extract code blocks
1285 |         if (elements.includes('codeblocks')) {
1286 |           extractedElements.codeblocks = await this.extractCodeBlocks(conversation, options);
1287 |         }
1288 | 
1289 |         // Extract metadata
1290 |         if (elements.includes('metadata')) {
1291 |           extractedElements.metadata = await this.extractMetadata(conversation);
1292 |         }
1293 | 
1294 |         // Extract structure
1295 |         if (elements.includes('structure')) {
1296 |           extractedElements.structure = await this.extractStructure(conversation);
1297 |         }
1298 | 
1299 |         results.push({
1300 |           composerId,
1301 |           format,
1302 |           elements: extractedElements
1303 |         });
1304 |       } catch (error) {
1305 |         console.error(`Failed to extract elements from conversation ${composerId}:`, error);
1306 |       }
1307 |     }
1308 | 
1309 |     return results;
1310 |   }
1311 | 
1312 |   /**
1313 |    * Extract files from conversation
1314 |    */
1315 |   private async extractFiles(
1316 |     conversation: CursorConversation,
1317 |     options?: { includeContext?: boolean }
1318 |   ): Promise<Array<{
1319 |     path: string;
1320 |     extension: string;
1321 |     context?: string;
1322 |     messageType: 'user' | 'assistant';
1323 |   }>> {
1324 |     const files: Array<{
1325 |       path: string;
1326 |       extension: string;
1327 |       context?: string;
1328 |       messageType: 'user' | 'assistant';
1329 |     }> = [];
1330 | 
1331 |     if (isLegacyConversation(conversation)) {
1332 |       for (const message of conversation.conversation) {
1333 |         if (message.relevantFiles) {
1334 |           for (const file of message.relevantFiles) {
1335 |             files.push({
1336 |               path: file,
1337 |               extension: this.getFileExtension(file),
1338 |               context: options?.includeContext ? message.text.substring(0, 200) : undefined,
1339 |               messageType: message.type === 1 ? 'user' : 'assistant'
1340 |             });
1341 |           }
1342 |         }
1343 |       }
1344 |     } else if (isModernConversation(conversation)) {
1345 |       const headers = conversation.fullConversationHeadersOnly || [];
1346 |       for (const header of headers) {
1347 |         try {
1348 |           const bubbleMessage = await this.getBubbleMessage(conversation.composerId, header.bubbleId);
1349 |           if (bubbleMessage && bubbleMessage.relevantFiles) {
1350 |             for (const file of bubbleMessage.relevantFiles) {
1351 |               files.push({
1352 |                 path: file,
1353 |                 extension: this.getFileExtension(file),
1354 |                 context: options?.includeContext ? bubbleMessage.text.substring(0, 200) : undefined,
1355 |                 messageType: bubbleMessage.type === 1 ? 'user' : 'assistant'
1356 |               });
1357 |             }
1358 |           }
1359 |         } catch (error) {
1360 |           console.error(`Failed to resolve bubble ${header.bubbleId} for files:`, error);
1361 |         }
1362 |       }
1363 |     }
1364 | 
1365 |     return files;
1366 |   }
1367 | 
1368 |   /**
1369 |    * Extract folders from conversation
1370 |    */
1371 |   private async extractFolders(
1372 |     conversation: CursorConversation,
1373 |     options?: { includeContext?: boolean }
1374 |   ): Promise<Array<{
1375 |     path: string;
1376 |     context?: string;
1377 |   }>> {
1378 |     const folders: Array<{
1379 |       path: string;
1380 |       context?: string;
1381 |     }> = [];
1382 | 
1383 |     if (isLegacyConversation(conversation)) {
1384 |       for (const message of conversation.conversation) {
1385 |         if (message.attachedFoldersNew) {
1386 |           for (const folder of message.attachedFoldersNew) {
1387 |             folders.push({
1388 |               path: folder,
1389 |               context: options?.includeContext ? message.text.substring(0, 200) : undefined
1390 |             });
1391 |           }
1392 |         }
1393 |       }
1394 |     } else if (isModernConversation(conversation)) {
1395 |       const headers = conversation.fullConversationHeadersOnly || [];
1396 |       for (const header of headers) {
1397 |         try {
1398 |           const bubbleMessage = await this.getBubbleMessage(conversation.composerId, header.bubbleId);
1399 |           if (bubbleMessage && bubbleMessage.attachedFoldersNew) {
1400 |             for (const folder of bubbleMessage.attachedFoldersNew) {
1401 |               folders.push({
1402 |                 path: folder,
1403 |                 context: options?.includeContext ? bubbleMessage.text.substring(0, 200) : undefined
1404 |               });
1405 |             }
1406 |           }
1407 |         } catch (error) {
1408 |           console.error(`Failed to resolve bubble ${header.bubbleId} for folders:`, error);
1409 |         }
1410 |       }
1411 |     }
1412 | 
1413 |     return folders;
1414 |   }
1415 | 
1416 |   /**
1417 |    * Extract languages from conversation
1418 |    */
1419 |   private async extractLanguages(
1420 |     conversation: CursorConversation,
1421 |     options?: { filters?: { languages?: string[] } }
1422 |   ): Promise<Array<{
1423 |     language: string;
1424 |     codeBlocks: number;
1425 |     totalLines: number;
1426 |     averageLength: number;
1427 |   }>> {
1428 |     const languageMap = new Map<string, { codeBlocks: number; totalLines: number; totalLength: number }>();
1429 | 
1430 |     if (isLegacyConversation(conversation)) {
1431 |       for (const message of conversation.conversation) {
1432 |         if (message.suggestedCodeBlocks) {
1433 |           for (const block of message.suggestedCodeBlocks) {
1434 |             const language = this.normalizeLanguage(block.language || 'text');
1435 |             if (options?.filters?.languages && !options.filters.languages.includes(language)) {
1436 |               continue;
1437 |             }
1438 | 
1439 |             if (!languageMap.has(language)) {
1440 |               languageMap.set(language, { codeBlocks: 0, totalLines: 0, totalLength: 0 });
1441 |             }
1442 | 
1443 |             const entry = languageMap.get(language)!;
1444 |             entry.codeBlocks++;
1445 |             entry.totalLines += block.code.split('\n').length;
1446 |             entry.totalLength += block.code.length;
1447 |           }
1448 |         }
1449 |       }
1450 |     } else if (isModernConversation(conversation)) {
1451 |       const headers = conversation.fullConversationHeadersOnly || [];
1452 |       for (const header of headers) {
1453 |         try {
1454 |           const bubbleMessage = await this.getBubbleMessage(conversation.composerId, header.bubbleId);
1455 |           if (bubbleMessage && bubbleMessage.suggestedCodeBlocks) {
1456 |             for (const block of bubbleMessage.suggestedCodeBlocks) {
1457 |               const language = this.normalizeLanguage(block.language || 'text');
1458 |               if (options?.filters?.languages && !options.filters.languages.includes(language)) {
1459 |                 continue;
1460 |               }
1461 | 
1462 |               if (!languageMap.has(language)) {
1463 |                 languageMap.set(language, { codeBlocks: 0, totalLines: 0, totalLength: 0 });
1464 |               }
1465 | 
1466 |               const entry = languageMap.get(language)!;
1467 |               entry.codeBlocks++;
1468 |               entry.totalLines += block.code.split('\n').length;
1469 |               entry.totalLength += block.code.length;
1470 |             }
1471 |           }
1472 |         } catch (error) {
1473 |           console.error(`Failed to resolve bubble ${header.bubbleId} for languages:`, error);
1474 |         }
1475 |       }
1476 |     }
1477 | 
1478 |     return Array.from(languageMap.entries()).map(([language, data]) => ({
1479 |       language,
1480 |       codeBlocks: data.codeBlocks,
1481 |       totalLines: data.totalLines,
1482 |       averageLength: data.codeBlocks > 0 ? data.totalLength / data.codeBlocks : 0
1483 |     }));
1484 |   }
1485 | 
1486 |   /**
1487 |    * Extract code blocks from conversation
1488 |    */
1489 |   private async extractCodeBlocks(
1490 |     conversation: CursorConversation,
1491 |     options?: {
1492 |       includeContext?: boolean;
1493 |       filters?: {
1494 |         minCodeLength?: number;
1495 |         languages?: string[];
1496 |       };
1497 |     }
1498 |   ): Promise<Array<{
1499 |     language: string;
1500 |     code: string;
1501 |     filename?: string;
1502 |     lineCount: number;
1503 |     messageType: 'user' | 'assistant';
1504 |     context?: string;
1505 |   }>> {
1506 |     const codeBlocks: Array<{
1507 |       language: string;
1508 |       code: string;
1509 |       filename?: string;
1510 |       lineCount: number;
1511 |       messageType: 'user' | 'assistant';
1512 |       context?: string;
1513 |     }> = [];
1514 | 
1515 |     if (isLegacyConversation(conversation)) {
1516 |       for (const message of conversation.conversation) {
1517 |         if (message.suggestedCodeBlocks) {
1518 |           for (const block of message.suggestedCodeBlocks) {
1519 |             const language = this.normalizeLanguage(block.language || 'text');
1520 | 
1521 |             // Apply filters
1522 |             if (options?.filters?.minCodeLength && block.code.length < options.filters.minCodeLength) {
1523 |               continue;
1524 |             }
1525 |             if (options?.filters?.languages && !options.filters.languages.includes(language)) {
1526 |               continue;
1527 |             }
1528 | 
1529 |             codeBlocks.push({
1530 |               language,
1531 |               code: block.code,
1532 |               filename: block.filename,
1533 |               lineCount: block.code.split('\n').length,
1534 |               messageType: message.type === 1 ? 'user' : 'assistant',
1535 |               context: options?.includeContext ? message.text.substring(0, 200) : undefined
1536 |             });
1537 |           }
1538 |         }
1539 |       }
1540 |     } else if (isModernConversation(conversation)) {
1541 |       const headers = conversation.fullConversationHeadersOnly || [];
1542 |       for (const header of headers) {
1543 |         try {
1544 |           const bubbleMessage = await this.getBubbleMessage(conversation.composerId, header.bubbleId);
1545 |           if (bubbleMessage && bubbleMessage.suggestedCodeBlocks) {
1546 |             for (const block of bubbleMessage.suggestedCodeBlocks) {
1547 |               const language = this.normalizeLanguage(block.language || 'text');
1548 | 
1549 |               // Apply filters
1550 |               if (options?.filters?.minCodeLength && block.code.length < options.filters.minCodeLength) {
1551 |                 continue;
1552 |               }
1553 |               if (options?.filters?.languages && !options.filters.languages.includes(language)) {
1554 |                 continue;
1555 |               }
1556 | 
1557 |               codeBlocks.push({
1558 |                 language,
1559 |                 code: block.code,
1560 |                 filename: block.filename,
1561 |                 lineCount: block.code.split('\n').length,
1562 |                 messageType: bubbleMessage.type === 1 ? 'user' : 'assistant',
1563 |                 context: options?.includeContext ? bubbleMessage.text.substring(0, 200) : undefined
1564 |               });
1565 |             }
1566 |           }
1567 |         } catch (error) {
1568 |           console.error(`Failed to resolve bubble ${header.bubbleId} for code blocks:`, error);
1569 |         }
1570 |       }
1571 |     }
1572 | 
1573 |     return codeBlocks;
1574 |   }
1575 | 
1576 |   /**
1577 |    * Extract metadata from conversation
1578 |    */
1579 |   private async extractMetadata(conversation: CursorConversation): Promise<{
1580 |     messageCount: number;
1581 |     size: number;
1582 |     format: 'legacy' | 'modern';
1583 |     userMessages: number;
1584 |     assistantMessages: number;
1585 |     hasCodeBlocks: boolean;
1586 |     hasFileReferences: boolean;
1587 |   }> {
1588 |     let messageCount = 0;
1589 |     let userMessages = 0;
1590 |     let assistantMessages = 0;
1591 |     let hasCodeBlocks = false;
1592 |     let hasFileReferences = false;
1593 | 
1594 |     if (isLegacyConversation(conversation)) {
1595 |       messageCount = conversation.conversation.length;
1596 | 
1597 |       for (const message of conversation.conversation) {
1598 |         if (message.type === 1) userMessages++;
1599 |         else assistantMessages++;
1600 | 
1601 |         if (message.suggestedCodeBlocks && message.suggestedCodeBlocks.length > 0) {
1602 |           hasCodeBlocks = true;
1603 |         }
1604 | 
1605 |         if (message.relevantFiles && message.relevantFiles.length > 0) {
1606 |           hasFileReferences = true;
1607 |         }
1608 |       }
1609 |     } else if (isModernConversation(conversation)) {
1610 |       const headers = conversation.fullConversationHeadersOnly || [];
1611 |       messageCount = headers.length;
1612 | 
1613 |       for (const header of headers) {
1614 |         if (header.type === 1) userMessages++;
1615 |         else assistantMessages++;
1616 | 
1617 |         try {
1618 |           const bubbleMessage = await this.getBubbleMessage(conversation.composerId, header.bubbleId);
1619 |           if (bubbleMessage) {
1620 |             if (bubbleMessage.suggestedCodeBlocks && bubbleMessage.suggestedCodeBlocks.length > 0) {
1621 |               hasCodeBlocks = true;
1622 |             }
1623 | 
1624 |             if (bubbleMessage.relevantFiles && bubbleMessage.relevantFiles.length > 0) {
1625 |               hasFileReferences = true;
1626 |             }
1627 |           }
1628 |         } catch (error) {
1629 |           console.error(`Failed to resolve bubble ${header.bubbleId} for metadata:`, error);
1630 |         }
1631 |       }
1632 |     }
1633 | 
1634 |     return {
1635 |       messageCount,
1636 |       size: JSON.stringify(conversation).length,
1637 |       format: isLegacyConversation(conversation) ? 'legacy' : 'modern',
1638 |       userMessages,
1639 |       assistantMessages,
1640 |       hasCodeBlocks,
1641 |       hasFileReferences
1642 |     };
1643 |   }
1644 | 
1645 |   /**
1646 |    * Extract structure from conversation
1647 |    */
1648 |   private async extractStructure(conversation: CursorConversation): Promise<{
1649 |     messageFlow: Array<{ type: 'user' | 'assistant'; length: number; hasCode: boolean }>;
1650 |     conversationPattern: string;
1651 |     averageMessageLength: number;
1652 |     longestMessage: number;
1653 |   }> {
1654 |     const messageFlow: Array<{ type: 'user' | 'assistant'; length: number; hasCode: boolean }> = [];
1655 |     let totalLength = 0;
1656 |     let longestMessage = 0;
1657 | 
1658 |     if (isLegacyConversation(conversation)) {
1659 |       for (const message of conversation.conversation) {
1660 |         const messageType = message.type === 1 ? 'user' : 'assistant';
1661 |                  const hasCode = !!(message.suggestedCodeBlocks && message.suggestedCodeBlocks.length > 0);
1662 |         const length = message.text.length;
1663 | 
1664 |         messageFlow.push({ type: messageType, length, hasCode });
1665 |         totalLength += length;
1666 |         longestMessage = Math.max(longestMessage, length);
1667 |       }
1668 |     } else if (isModernConversation(conversation)) {
1669 |       const headers = conversation.fullConversationHeadersOnly || [];
1670 | 
1671 |       for (const header of headers) {
1672 |         const messageType = header.type === 1 ? 'user' : 'assistant';
1673 |         let hasCode = false;
1674 |         let length = 0;
1675 | 
1676 |         try {
1677 |           const bubbleMessage = await this.getBubbleMessage(conversation.composerId, header.bubbleId);
1678 |           if (bubbleMessage) {
1679 |             hasCode = !!(bubbleMessage.suggestedCodeBlocks && bubbleMessage.suggestedCodeBlocks.length > 0);
1680 |             length = bubbleMessage.text.length;
1681 |           }
1682 |         } catch (error) {
1683 |           console.error(`Failed to resolve bubble ${header.bubbleId} for structure:`, error);
1684 |         }
1685 | 
1686 |         messageFlow.push({ type: messageType, length, hasCode });
1687 |         totalLength += length;
1688 |         longestMessage = Math.max(longestMessage, length);
1689 |       }
1690 |     }
1691 | 
1692 |     const conversationPattern = messageFlow.map(m => m.type === 'user' ? 'U' : 'A').join('-');
1693 |     const averageMessageLength = messageFlow.length > 0 ? totalLength / messageFlow.length : 0;
1694 | 
1695 |     return {
1696 |       messageFlow,
1697 |       conversationPattern,
1698 |       averageMessageLength,
1699 |       longestMessage
1700 |     };
1701 |   }
1702 | 
1703 |   /**
1704 |    * Get file extension from file path
1705 |    */
1706 |   private getFileExtension(filePath: string): string {
1707 |     const lastDot = filePath.lastIndexOf('.');
1708 |     const lastSlash = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
1709 | 
1710 |     if (lastDot > lastSlash && lastDot !== -1) {
1711 |       return filePath.substring(lastDot + 1).toLowerCase();
1712 |     }
1713 | 
1714 |     return '';
1715 |   }
1716 | 
1717 |   /**
1718 |    * Normalize language names for consistency
1719 |    */
1720 |   private normalizeLanguage(language: string): string {
1721 |     const normalized = language.toLowerCase().trim();
1722 | 
1723 |     // Common language mappings
1724 |     const mappings: Record<string, string> = {
1725 |       'js': 'javascript',
1726 |       'ts': 'typescript',
1727 |       'jsx': 'javascript',
1728 |       'tsx': 'typescript',
1729 |       'py': 'python',
1730 |       'rb': 'ruby',
1731 |       'sh': 'shell',
1732 |       'bash': 'shell',
1733 |       'zsh': 'shell',
1734 |       'fish': 'shell',
1735 |       'yml': 'yaml',
1736 |       'md': 'markdown',
1737 |       'dockerfile': 'docker'
1738 |     };
1739 | 
1740 |     return mappings[normalized] || normalized;
1741 |   }
1742 | 
1743 |   /**
1744 |    * Extract context around a search match
1745 |    */
1746 |   private extractContext(text: string, query: string, contextLines: number): string {
1747 |     const lines = text.split('\n');
1748 |     const queryLower = query.toLowerCase();
1749 | 
1750 |     for (let i = 0; i < lines.length; i++) {
1751 |       if (lines[i].toLowerCase().includes(queryLower)) {
1752 |         const start = Math.max(0, i - contextLines);
1753 |         const end = Math.min(lines.length, i + contextLines + 1);
1754 |         return lines.slice(start, end).join('\n');
1755 |       }
1756 |     }
1757 | 
1758 |     return text.substring(0, 200) + '...';
1759 |   }
1760 | 
1761 |   /**
1762 |    * Filter results by date range
1763 |    */
1764 |   private async filterResultsByDateRange(results: ConversationSearchResult[], startDate?: string, endDate?: string): Promise<ConversationSearchResult[]> {
1765 |     const filteredResults: ConversationSearchResult[] = [];
1766 | 
1767 |     for (const result of results) {
1768 |       const conversation = await this.getConversationById(result.composerId);
1769 |       if (!conversation) continue;
1770 | 
1771 |       const conversationFormat = isLegacyConversation(conversation) ? 'legacy' : 'modern';
1772 |       const filteredMatches: SearchMatch[] = [];
1773 | 
1774 |       // Check each match for date filtering
1775 |       for (const match of result.matches) {
1776 |         let messageHasValidDate = false;
1777 | 
1778 |         if (conversationFormat === 'legacy') {
1779 |           const legacyConv = conversation as LegacyCursorConversation;
1780 |           if (match.messageIndex !== undefined && legacyConv.conversation[match.messageIndex]) {
1781 |             const message = legacyConv.conversation[match.messageIndex];
1782 |             if (message.timestamp) {
1783 |               const messageDate = new Date(message.timestamp).toISOString().split('T')[0];
1784 |               if ((!startDate || messageDate >= startDate) && (!endDate || messageDate <= endDate)) {
1785 |                 messageHasValidDate = true;
1786 |               }
1787 |             } else {
1788 |               // If no timestamp, include the message (can't filter)
1789 |               messageHasValidDate = true;
1790 |             }
1791 |           }
1792 |         } else if (conversationFormat === 'modern' && match.bubbleId) {
1793 |           try {
1794 |             const bubbleMessage = await this.getBubbleMessage(result.composerId, match.bubbleId);
1795 |             if (bubbleMessage && bubbleMessage.timestamp) {
1796 |               const messageDate = new Date(bubbleMessage.timestamp).toISOString().split('T')[0];
1797 |               if ((!startDate || messageDate >= startDate) && (!endDate || messageDate <= endDate)) {
1798 |                 messageHasValidDate = true;
1799 |               }
1800 |             } else {
1801 |               // If no timestamp, include the message (can't filter)
1802 |               messageHasValidDate = true;
1803 |             }
1804 |           } catch (error) {
1805 |             // If error resolving bubble, include the message
1806 |             messageHasValidDate = true;
1807 |           }
1808 |         } else {
1809 |           // No timestamp available, include the message
1810 |           messageHasValidDate = true;
1811 |         }
1812 | 
1813 |         if (messageHasValidDate) {
1814 |           filteredMatches.push(match);
1815 |         }
1816 |       }
1817 | 
1818 |       // Only include the result if it has valid matches after date filtering
1819 |       if (filteredMatches.length > 0) {
1820 |         filteredResults.push({
1821 |           ...result,
1822 |           matches: filteredMatches
1823 |         });
1824 |       }
1825 |     }
1826 | 
1827 |     return filteredResults;
1828 |   }
1829 | }
```
Page 3/3FirstPrevNextLast