Skip to content

Commit 31c6c30

Browse files
authored
Merge pull request somogyijanos#5 from somogyijanos/pr-noemptyimgs
Pr noemptyimgs
2 parents ec204e9 + 30cba85 commit 31c6c30

File tree

6 files changed

+126
-32
lines changed

6 files changed

+126
-32
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.venv
22
**/__pycache__
3-
output
3+
output
4+
**/.DS_Store

README.md

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ Also see [this](https://forum.cursor.com/t/guide-5-steps-exporting-chats-prompts
88

99
## Features
1010

11-
- **Discover Chats**: Discover all `state.vscdb` files in a directory and print a few lines of dialogue so one can identify which is the workspace (chat) one is searching for. It's also possible to filter by text.
12-
- **Export Chats**: Export chats data for a certain workspace to Markdown files or print it to the command line.
11+
- **Discover Chats**: Discover all chats from all workspaces and print a few lines of dialogue so one can identify which is the workspace (or chat) one is searching for. It's also possible to filter by text.
12+
- **Export Chats**: Export chats for the most recent (or a specific) workspace to Markdown files or print it to the command line.
1313

1414
## Installation
1515

@@ -26,20 +26,44 @@ Also see [this](https://forum.cursor.com/t/guide-5-steps-exporting-chats-prompts
2626

2727
## Usage
2828

29-
Find, where the `state.vscdb` is located in your computer. The table below may help:
29+
First, find, where the `state.vscdb` files are located on your computer. Confirm that corresponding to your system, the right path is set in the [config.yml](./config.yml) file. Update it if not set correctly.
3030

31-
| OS | Path |
32-
|------------------|-----------------------------------------------------------|
33-
| Windows | `%APPDATA%\Cursor\User\workspaceStorage` |
34-
| macOS | `/Users/YOUR_USERNAME/Library/Application Support/Cursor/User/workspaceStorage` |
35-
| Linux | `/home/YOUR_USERNAME/.config/Cursor/User/workspaceStorage` |
31+
Both the `discover` and `export` commands will work with this path by default, but you can also provide a custom path any time.
3632

37-
### Discover Chats of all Workspaces
33+
---
34+
35+
### Discover Chats
3836
```sh
39-
./chat.py discover --search-text "matplotlib" "/Users/myuser/Library/Application Support/Cursor/User/workspaceStorage"
37+
# Help on usage
38+
./chat.py discover --help
39+
40+
# Discover all chats from all workspaces
41+
./chat.py discover
42+
43+
# Apply text filter
44+
./chat.py discover --search-text "matplotlib"
45+
46+
# Discover all chats from all workspaces at a custom path
47+
./chat.py discover "/path/to/workspaces"
4048
```
4149

42-
### Export Chats of a Workspace
50+
---
51+
52+
### Export Chats
53+
See `./chat.py export --help` for general help. Examples:
4354
```sh
44-
./chat.py export --output-dir output "/Users/myuser/Library/Application Support/Cursor/User/workspaceStorage/b989572f2e2186b48b808da2da437416/state.vscdb"
45-
```
55+
# Help on usage
56+
./chat.py export --help
57+
58+
# Print all chats of the most recent workspace to the command line
59+
./chat.py export
60+
61+
# Export all chats of the most recent workspace as Markdown
62+
./chat.py export --output-dir "/path/to/output"
63+
64+
# Export only the latest chat of the most recent workspace
65+
./chat.py export --latest-tab --output-dir "/path/to/output"
66+
67+
# Export all chats of a specifc workspace
68+
./chat.py export --output-dir "/path/to/output" "/path/to/workspaces/workspace-dir/state.vscdb"
69+
```

chat.py

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from rich.markdown import Markdown
1010
from loguru import logger
1111
import json
12+
import yaml
13+
import platform
14+
from pathlib import Path
1215

1316
logger.remove()
1417
logger.add(sys.stderr, level="INFO")
@@ -18,15 +21,18 @@
1821
console = Console()
1922

2023
@app.command()
21-
def export(db_path: str, output_dir: str = None):
24+
def export(
25+
db_path: str = typer.Argument(None, help="The path to the SQLite database file. If not provided, the latest workspace will be used."),
26+
output_dir: str = typer.Option(None, help="The directory where the output markdown files will be saved. If not provided, prints to command line."),
27+
latest_tab: bool = typer.Option(False, "--latest-tab", help="Export only the latest tab. If not set, all tabs will be exported.")
28+
):
2229
"""
2330
Export chat data from the database to markdown files or print it to the command line.
24-
25-
Args:
26-
db_path (str): The path to the SQLite database file.
27-
output_dir (str): The directory where the output markdown files will be saved. If not provided, prints to command line.
2831
"""
29-
image_dir = os.path.join(output_dir, 'images') if output_dir else None
32+
if not db_path:
33+
db_path = get_latest_workspace_db_path()
34+
35+
image_dir = None
3036

3137
try:
3238
# Query the AI chat data from the database
@@ -41,6 +47,17 @@ def export(db_path: str, output_dir: str = None):
4147
# Convert the chat data from JSON string to dictionary
4248
chat_data_dict = json.loads(chat_data[0])
4349

50+
if latest_tab:
51+
# Get the latest tab by timestamp
52+
latest_tab = max(chat_data_dict['tabs'], key=lambda tab: tab.get('timestamp', 0))
53+
chat_data_dict['tabs'] = [latest_tab]
54+
55+
# Check if there are any images in the chat data
56+
has_images = any('image' in bubble for tab in chat_data_dict['tabs'] for bubble in tab.get('bubbles', []))
57+
58+
if has_images and output_dir:
59+
image_dir = os.path.join(output_dir, 'images')
60+
4461
# Format the chat data
4562
formatter = MarkdownChatFormatter()
4663
formatted_chats = formatter.format(chat_data_dict, image_dir)
@@ -57,6 +74,10 @@ def export(db_path: str, output_dir: str = None):
5774
for formatted_data in formatted_chats:
5875
console.print(Markdown(formatted_data))
5976
logger.info("Chat data has been successfully printed to the command line")
77+
except KeyError as e:
78+
error_message = f"KeyError: {e}. The chat data structure is not as expected. Please check the database content."
79+
logger.error(error_message)
80+
raise typer.Exit(code=1)
6081
except json.JSONDecodeError as e:
6182
error_message = f"JSON decode error: {e}"
6283
logger.error(error_message)
@@ -70,16 +91,60 @@ def export(db_path: str, output_dir: str = None):
7091
logger.error(error_message)
7192
raise typer.Exit(code=1)
7293

94+
def get_cursor_workspace_path() -> Path:
95+
config_path = Path("config.yml")
96+
logger.debug(f"Looking for configuration file at: {config_path}")
97+
98+
if not config_path.exists():
99+
error_message = f"Configuration file not found: {config_path}"
100+
logger.error(error_message)
101+
raise FileNotFoundError(error_message)
102+
103+
with open(config_path, 'r') as f:
104+
config = yaml.safe_load(f)
105+
logger.debug("Configuration file loaded successfully")
106+
107+
system = platform.system()
108+
logger.debug(f"Detected operating system: {system}")
109+
110+
if system not in config["default_vscdb_dir_paths"]:
111+
error_message = f"Unsupported operating system: {system}"
112+
logger.error(error_message)
113+
raise ValueError(error_message)
114+
115+
base_path = Path(os.path.expandvars(config["default_vscdb_dir_paths"][system])).expanduser()
116+
logger.debug(f"Resolved base path: {base_path}")
117+
118+
if not base_path.exists():
119+
error_message = f"Cursor workspace storage directory not found: {base_path}"
120+
logger.error(error_message)
121+
raise FileNotFoundError(error_message)
122+
123+
logger.info(f"Cursor workspace storage directory found: {base_path}")
124+
return base_path
125+
126+
def get_latest_workspace_db_path() -> str:
127+
base_path = get_cursor_workspace_path()
128+
workspace_folder = max(base_path.glob("*"), key=os.path.getmtime)
129+
db_path = workspace_folder / "state.vscdb"
130+
131+
if not db_path.exists():
132+
raise FileNotFoundError(f"state.vscdb not found in {workspace_folder}")
133+
134+
return str(db_path)
135+
73136
@app.command()
74-
def discover(directory: str, limit: int = None, search_text: str = None):
137+
def discover(
138+
directory: str = typer.Argument(None, help="The directory to search for state.vscdb files. If not provided, the default Cursor workspace storage directory will be used."),
139+
limit: int = typer.Option(None, help="The maximum number of state.vscdb files to process. Defaults to 10 if search_text is not provided, else -1."),
140+
search_text: str = typer.Option(None, help="The text to search for in the chat history.")
141+
):
75142
"""
76143
Discover all state.vscdb files in a directory and its subdirectories, and print a few lines of dialogue.
77-
78-
Args:
79-
directory (str): The directory to search for state.vscdb files.
80-
limit (int): The maximum number of state.vscdb files to process, sorted by most recent edits. Defaults to 10 if search_text is not provided, else -1.
81-
search_text (str): The text to search for in the chat history. If provided, only chat entries containing this text will be printed.
82144
"""
145+
if not directory:
146+
directory = str(get_cursor_workspace_path())
147+
83148
if limit is None:
84149
limit = -1 if search_text else 10
85150

@@ -119,7 +184,6 @@ def discover(directory: str, limit: int = None, search_text: str = None):
119184
for formatted_data in formatted_chats:
120185
filtered_lines = [line for line in formatted_data.splitlines() if search_text.lower() in line.lower()]
121186
if filtered_lines:
122-
# results.append((db_path, "[...]" + " \n[...] \n".join(filtered_lines[:10]) + "[...]"))
123187
results.append((db_path, "\n".join(formatted_data.splitlines()[:10]) + "\n..."))
124188
if not filtered_lines:
125189
logger.debug(f"No chat entries containing '{search_text}' found in {db_path}")
@@ -153,4 +217,4 @@ def discover(directory: str, limit: int = None, search_text: str = None):
153217
raise typer.Exit(code=1)
154218

155219
if __name__ == "__main__":
156-
app()
220+
app()

config.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1+
default_vscdb_dir_paths:
2+
Windows: "%APPDATA%/Cursor/User/workspaceStorage"
3+
Darwin: "~/Library/Application Support/Cursor/User/workspaceStorage"
4+
Linux: "~/.config/Cursor/User/workspaceStorage"
15
aichat_query: "SELECT value FROM ItemTable WHERE [key] IN ('workbench.panel.aichat.view.aichat.chatdata');"

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
loguru
22
pyyaml
33
typer
4+
rich

src/export.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import re
32
import shutil
43
import os
@@ -50,10 +49,12 @@ def format(self, chat_data: dict[str, Any], image_dir: str | None = 'images') ->
5049
shutil.copy(image_path, new_image_path)
5150
formatted_chat.append(f"![User Image]({new_image_path})\n")
5251
elif bubble['type'] == 'ai':
52+
model_type = bubble.get('modelType', 'Unknown')
5353
raw_text = re.sub(r'```python:[^\n]+', '```python', bubble['rawText'])
54-
formatted_chat.append(f"## AI:\n\n{raw_text}\n")
54+
formatted_chat.append(f"## AI ({model_type}):\n\n{raw_text}\n")
5555

5656
formatted_chats.append("\n".join(formatted_chat))
57+
5758
return formatted_chats
5859
except KeyError as e:
5960
logger.error(f"KeyError: {e}")
@@ -126,5 +127,4 @@ def export(self, chat_data: dict[str, Any], output_dir: str, image_dir: str) ->
126127
# formatter = MarkdownChatFormatter()
127128
# saver = MarkdownFileSaver()
128129
# exporter = ChatExporter(formatter, saver)
129-
# exporter.export(chat_data, 'output_folder', 'images')
130-
130+
# exporter.export(chat_data, 'output_folder', 'images')

0 commit comments

Comments
 (0)