cursor-workspace-tool 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cursor_workspace_tool-0.1.0.dist-info/METADATA +494 -0
- cursor_workspace_tool-0.1.0.dist-info/RECORD +7 -0
- cursor_workspace_tool-0.1.0.dist-info/WHEEL +5 -0
- cursor_workspace_tool-0.1.0.dist-info/entry_points.txt +3 -0
- cursor_workspace_tool-0.1.0.dist-info/licenses/LICENSE +674 -0
- cursor_workspace_tool-0.1.0.dist-info/top_level.txt +1 -0
- cursor_workspace_tool.py +2588 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cursor-workspace-tool
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Inspect and manage Cursor workspaces and AI Chat / Composer history across Windows and WSL/Linux.
|
|
5
|
+
Author-email: Aviv Raz <avivraz123@gmail.com>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/aviv-raz/cursor-workspace-tool
|
|
8
|
+
Project-URL: Repository, https://github.com/aviv-raz/cursor-workspace-tool
|
|
9
|
+
Project-URL: Issues, https://github.com/aviv-raz/cursor-workspace-tool/issues
|
|
10
|
+
Keywords: cursor,vscode,workspace,ai-chat,composer,wsl,cli
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# Cursor Workspace Tool
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/cursor-workspace-tool/)
|
|
33
|
+
[](https://pypi.org/project/cursor-workspace-tool/)
|
|
34
|
+
[](LICENSE)
|
|
35
|
+
|
|
36
|
+
A single-file, dependency-free command-line tool for inspecting and managing
|
|
37
|
+
[Cursor](https://cursor.com) editor **workspaces** and their **AI Chat / Composer**
|
|
38
|
+
history — across Windows and WSL/Linux at the same time.
|
|
39
|
+
|
|
40
|
+
Cursor (like any VS Code–based editor) keeps a growing pile of `workspaceStorage`
|
|
41
|
+
entries and chat records that it never cleans up on its own, and it offers no
|
|
42
|
+
built-in way to move a conversation from one project to another. This tool lets
|
|
43
|
+
you see exactly what is stored, move or copy AI chats between workspaces safely,
|
|
44
|
+
and prune the clutter — with an automatic backup before every change.
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **List** every Cursor workspace found across all installations, with a rich
|
|
49
|
+
per-category breakdown of its AI chats (`list`).
|
|
50
|
+
- **Inspect** the non-empty chats and drafts of any workspace, including names,
|
|
51
|
+
IDs, timestamps and message counts (`chats`).
|
|
52
|
+
- **Move or copy** AI Chats/Composers between workspaces — all of them or a
|
|
53
|
+
hand-picked selection — without overwriting anything at the destination
|
|
54
|
+
(`merge`).
|
|
55
|
+
- **Clean up** stale workspace entries or empty chat records, globally or for a
|
|
56
|
+
single workspace, with drafts always protected (`cleanup`).
|
|
57
|
+
- **Restore** any change from an automatic, timestamped backup (`restore`).
|
|
58
|
+
- **Cross-platform:** works whether Cursor runs natively on Linux or on Windows
|
|
59
|
+
with the Remote-WSL extension.
|
|
60
|
+
- **Safe by default:** write commands are dry-run unless you pass `--apply`,
|
|
61
|
+
always create a backup, and refuse to run against a locked database.
|
|
62
|
+
|
|
63
|
+
## How it works
|
|
64
|
+
|
|
65
|
+
The tool automatically discovers Cursor data directories. When run from
|
|
66
|
+
**WSL / native Linux** it scans:
|
|
67
|
+
|
|
68
|
+
- the local Linux Cursor config (`~/.config/Cursor`), and
|
|
69
|
+
- every Windows user profile reachable at
|
|
70
|
+
`/mnt/<drive>/Users/*/AppData/Roaming/Cursor`.
|
|
71
|
+
|
|
72
|
+
The Windows scan matters because when you run Cursor on Windows and connect into
|
|
73
|
+
WSL via the Remote-WSL extension, **all** workspace and chat storage — including
|
|
74
|
+
data for WSL-remote workspaces — actually lives on the Windows side.
|
|
75
|
+
|
|
76
|
+
When run natively on **Windows** (`python.exe`), it uses `%APPDATA%\Cursor`.
|
|
77
|
+
|
|
78
|
+
You can always add extra directories (multiple users, VSCodium, etc.) with
|
|
79
|
+
`--root`.
|
|
80
|
+
|
|
81
|
+
### Where the data lives
|
|
82
|
+
|
|
83
|
+
| Data | Location |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| Per-workspace metadata | `workspaceStorage/<id>/` (`workspace.json`, `state.vscdb`) |
|
|
86
|
+
| AI chat headers & content | `globalStorage/state.vscdb` (`composerHeaders`, `cursorDiskKV`) |
|
|
87
|
+
|
|
88
|
+
Chats are keyed by a globally-unique `composerId` and associated with a
|
|
89
|
+
workspace through a `workspaceId`, which is why chats can be reassigned between
|
|
90
|
+
workspaces without collisions.
|
|
91
|
+
|
|
92
|
+
## Requirements
|
|
93
|
+
|
|
94
|
+
- Python 3.8+ (standard library only — no third-party packages).
|
|
95
|
+
|
|
96
|
+
## Installation
|
|
97
|
+
|
|
98
|
+
### Option 1 — install from PyPI (recommended)
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install cursor-workspace-tool
|
|
102
|
+
# or
|
|
103
|
+
uv tool install cursor-workspace-tool
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
After installation, both of these commands are available and do the same thing:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
cursor-workspace-tool --help
|
|
110
|
+
cwt --help
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Option 2 — install from GitHub
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pip install git+https://github.com/aviv-raz/cursor-workspace-tool.git
|
|
117
|
+
# or
|
|
118
|
+
uv tool install git+https://github.com/aviv-raz/cursor-workspace-tool.git
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Option 3 — install from a local clone
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
git clone https://github.com/aviv-raz/cursor-workspace-tool.git
|
|
125
|
+
cd cursor-workspace-tool
|
|
126
|
+
pip install .
|
|
127
|
+
# or: uv tool install .
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Option 4 — run the script directly
|
|
131
|
+
|
|
132
|
+
No install required (Python 3.8+, standard library only):
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
git clone https://github.com/aviv-raz/cursor-workspace-tool.git
|
|
136
|
+
cd cursor-workspace-tool
|
|
137
|
+
python3 cursor_workspace_tool.py --help
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Quick start
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# See all workspaces and their chat counts
|
|
144
|
+
cwt list --chat-counts
|
|
145
|
+
|
|
146
|
+
# Inspect the chats of a specific workspace
|
|
147
|
+
cwt chats counter-service
|
|
148
|
+
|
|
149
|
+
# Preview moving those chats into another project (dry-run)
|
|
150
|
+
cwt merge counter-service my-other-project
|
|
151
|
+
|
|
152
|
+
# Actually perform the move
|
|
153
|
+
cwt merge counter-service my-other-project --apply
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The full command name `cursor-workspace-tool` works identically in place of `cwt`.
|
|
157
|
+
If you are running the script without installing, replace `cwt` with
|
|
158
|
+
`python3 cursor_workspace_tool.py`.
|
|
159
|
+
|
|
160
|
+
## Contributing
|
|
161
|
+
|
|
162
|
+
Bug reports and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
163
|
+
for setup, safety notes, and pull-request guidelines.
|
|
164
|
+
|
|
165
|
+
## Safety and backups
|
|
166
|
+
|
|
167
|
+
> **Close Cursor completely before any `--apply` or `restore`.**
|
|
168
|
+
> Cursor holds its databases open in the background; writing while it runs can
|
|
169
|
+
> cause data loss or lock errors. The tool checks for a locked database and
|
|
170
|
+
> aborts before making changes if it detects one — but this is a best-effort
|
|
171
|
+
> check, not a substitute for closing Cursor.
|
|
172
|
+
|
|
173
|
+
Every write creates an **automatic, timestamped backup** first (override the
|
|
174
|
+
location with `--backup-dir`) and prints its path when finished. You can always
|
|
175
|
+
roll back with `restore <backup-dir>`.
|
|
176
|
+
|
|
177
|
+
- `merge --apply` backs up the affected `globalStorage/state.vscdb` file(s) — the
|
|
178
|
+
destination's, and the source's too if it belongs to a different installation.
|
|
179
|
+
- `cleanup workspaces --apply` backs up **the entire `workspaceStorage/<id>/`
|
|
180
|
+
folder** of each deleted workspace **and** the relevant `globalStorage/state.vscdb`.
|
|
181
|
+
- `cleanup empty-chats --apply` backs up `globalStorage/state.vscdb` before
|
|
182
|
+
deleting empty chat records.
|
|
183
|
+
|
|
184
|
+
Each backup folder also contains a human-readable `operation-report.md`
|
|
185
|
+
describing exactly what happened:
|
|
186
|
+
|
|
187
|
+
- **`merge`** — source and destination workspaces, `--mode move/copy`, how many
|
|
188
|
+
chats were requested vs. actually transferred, and the full chat list (name,
|
|
189
|
+
ID, type, created/last-updated times, message count). In `copy` mode it also
|
|
190
|
+
records the new IDs created at the destination.
|
|
191
|
+
- **`cleanup workspaces`** — each deleted workspace, the reason, its path and
|
|
192
|
+
size, the chat records purged from it, and the totals actually removed.
|
|
193
|
+
- **`cleanup empty-chats`** — the workspaces scanned, the empty chats deleted,
|
|
194
|
+
and the totals actually removed.
|
|
195
|
+
|
|
196
|
+
If an operation aborts because the database is locked after the backup was made,
|
|
197
|
+
the report is saved with an `Aborted` status. The `manifest.json` in each backup
|
|
198
|
+
folder is the exact machine-readable mapping used by `restore`; the
|
|
199
|
+
`operation-report.md` is for human review.
|
|
200
|
+
|
|
201
|
+
## Commands
|
|
202
|
+
|
|
203
|
+
### `list` — list all workspaces
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
python3 cursor_workspace_tool.py list
|
|
207
|
+
python3 cursor_workspace_tool.py list --chat-counts # + per-workspace chat breakdown
|
|
208
|
+
python3 cursor_workspace_tool.py list --details # + on-disk size (slower)
|
|
209
|
+
python3 cursor_workspace_tool.py list --with-chats # only workspaces whose TOTAL > 0
|
|
210
|
+
python3 cursor_workspace_tool.py list --root "/mnt/d/Users/other/AppData/Roaming/Cursor/User"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Prints a table with: an index (`#`) you can reuse in other commands, the internal
|
|
214
|
+
`ID` (the `workspaceStorage` folder name), the environment `TYPE`, what Cursor
|
|
215
|
+
`OPENED AS`, a `CHATS` breakdown, on-disk `SIZE`, `LAST USED` time, and the
|
|
216
|
+
decoded `PATH`.
|
|
217
|
+
|
|
218
|
+
With `--chat-counts` or `--details`, the `CHATS` column shows a full,
|
|
219
|
+
non-overlapping breakdown that always sums to `TOTAL`, e.g.:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
UI:4 SUB-AGENT:2 ARCHIVED:0 EMPTY:6 DRAFT:0 TOTAL:12
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| Category | Meaning |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| `UI` | Non-empty regular chats shown in the main chat list (not subagent/archived). |
|
|
228
|
+
| `SUB-AGENT` | Non-empty chats created as subagents; usually hidden from the main list. |
|
|
229
|
+
| `ARCHIVED` | Non-empty regular chats marked archived. |
|
|
230
|
+
| `EMPTY` | Zero-message records that are not drafts. |
|
|
231
|
+
| `DRAFT` | Zero-message records flagged `isDraft` **that contain saved unsent text**. |
|
|
232
|
+
| `TOTAL` | Sum of the categories above. |
|
|
233
|
+
|
|
234
|
+
Completely empty drafts (no saved text) are treated as junk: they are **not**
|
|
235
|
+
counted in `DRAFT` and **not** included in `TOTAL`.
|
|
236
|
+
|
|
237
|
+
`--with-chats` filters the table to workspaces whose `TOTAL > 0`. It turns on
|
|
238
|
+
chat counting automatically. `EMPTY` records count as existing chats; fully empty
|
|
239
|
+
drafts do not. A workspace whose database cannot be read is excluded from the
|
|
240
|
+
filter rather than misreported.
|
|
241
|
+
|
|
242
|
+
**`OPENED AS` values**
|
|
243
|
+
|
|
244
|
+
- `Folder` — `PATH` is a single folder opened directly.
|
|
245
|
+
- `Workspace file` — `PATH` points to a `.code-workspace` file, which may define
|
|
246
|
+
one or many folders.
|
|
247
|
+
- `Empty window` — a Cursor window opened with no folder or workspace.
|
|
248
|
+
- `Unknown` — no valid `workspace.json`, so what was opened can't be determined.
|
|
249
|
+
|
|
250
|
+
**`TYPE` values**
|
|
251
|
+
|
|
252
|
+
- `Local (<OS>)` — a local path opened without a Remote connection. A path
|
|
253
|
+
containing a Windows drive letter is reported as `Local (Windows)` even when
|
|
254
|
+
the tool runs inside WSL; other local URIs use the generic name from
|
|
255
|
+
`platform.system()` (e.g. `Local (Linux)`, `Local (Darwin)`).
|
|
256
|
+
- `Remote (<authority>)` — a Remote connection. The value in parentheses is the
|
|
257
|
+
URI's raw `authority`, so current and future remote types display
|
|
258
|
+
automatically — e.g. a Remote-WSL URI `vscode-remote://wsl+Ubuntu/...` shows as
|
|
259
|
+
`Remote (wsl+Ubuntu)`.
|
|
260
|
+
- `Local (Windows with WSL FS: <Distro>)` — Cursor runs locally on Windows and
|
|
261
|
+
reaches files inside a WSL distro via `file://wsl.localhost/<Distro>/...`,
|
|
262
|
+
without a Remote-WSL connection.
|
|
263
|
+
- `Empty window` / `Unknown` — as above.
|
|
264
|
+
|
|
265
|
+
Workspaces whose folder/file no longer exists on disk (from the machine running
|
|
266
|
+
the tool) are marked `(MISSING)`.
|
|
267
|
+
|
|
268
|
+
### `chats [workspace]` — non-empty chats and drafts
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
python3 cursor_workspace_tool.py chats # all workspaces
|
|
272
|
+
python3 cursor_workspace_tool.py chats 4 # by list index
|
|
273
|
+
python3 cursor_workspace_tool.py chats 2199cf1cd03410b8789ae530f51a71cb # by exact id
|
|
274
|
+
python3 cursor_workspace_tool.py chats counter-service # by path substring
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Without an argument, it scans all workspaces and shows those that have non-empty
|
|
278
|
+
chats or drafts **with content**. Under each workspace it lists the non-empty
|
|
279
|
+
chats (`UI`, `SUB-AGENT`, `ARCHIVED`) and any `DRAFT` chats that contain typed
|
|
280
|
+
text. `EMPTY` chats and completely empty drafts (e.g. the throwaway draft created
|
|
281
|
+
when you open a new empty window) are never listed.
|
|
282
|
+
|
|
283
|
+
The output starts with the number of workspaces and the total number of chats
|
|
284
|
+
shown. Each workspace section begins with a `UI / SUB-AGENT / ARCHIVED / DRAFT /
|
|
285
|
+
TOTAL` summary, followed by each chat's name, `composerId`, type, creation time,
|
|
286
|
+
last-updated time and message count. For `DRAFT` chats the unsent draft text is
|
|
287
|
+
shown; the tool extracts it from the chat's saved input (flat text, rich-text
|
|
288
|
+
input tree, or header subtitle).
|
|
289
|
+
|
|
290
|
+
The optional `[workspace]` argument restricts the output to a single workspace.
|
|
291
|
+
|
|
292
|
+
### `merge <source> <dest>` — move or copy chats between workspaces
|
|
293
|
+
|
|
294
|
+
This command **only touches Cursor's own non-empty AI Chats/Composers**
|
|
295
|
+
(`composerHeaders` / `cursorDiskKV` in `globalStorage/state.vscdb`). A chat is
|
|
296
|
+
non-empty when it has at least one message (`bubbleId`) — the same definition
|
|
297
|
+
used by `chats` and the `list` counts. Empty records and drafts are skipped and
|
|
298
|
+
left untouched in the source, in both `move` and `copy` modes.
|
|
299
|
+
|
|
300
|
+
It does **not** touch anything else: not per-workspace `state.vscdb` keys (open
|
|
301
|
+
tabs, UI state, recent files), and not the `chatSessions/` / `chatEditingSessions/`
|
|
302
|
+
folders (those belong to the GitHub Copilot Chat extension, not Cursor's
|
|
303
|
+
Composer).
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
# Dry-run is the default — always preview first:
|
|
307
|
+
python3 cursor_workspace_tool.py merge counter-service my-other-project
|
|
308
|
+
|
|
309
|
+
# Apply the move:
|
|
310
|
+
python3 cursor_workspace_tool.py merge counter-service my-other-project --apply
|
|
311
|
+
|
|
312
|
+
# Copy instead of move (source keeps its own copy):
|
|
313
|
+
python3 cursor_workspace_tool.py merge 3 7 --mode copy --apply
|
|
314
|
+
|
|
315
|
+
# Transfer only specific chats (comma-separated, or repeat the flag):
|
|
316
|
+
python3 cursor_workspace_tool.py merge counter-service my-other-project \
|
|
317
|
+
--chat-id ce5f1fbf-cf28-4fa4-a0c8-ddf616a3aa78,01234567-89ab-cdef-0123-456789abcdef
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Mode (`--mode`, default `move`)**
|
|
321
|
+
|
|
322
|
+
- `--mode move` (default) — the chats are re-associated with the destination and
|
|
323
|
+
removed from the source. This is safe because each chat's ID is globally
|
|
324
|
+
unique, so it can't clobber an existing destination chat.
|
|
325
|
+
- `--mode copy` — the chats are duplicated at the destination under new IDs and
|
|
326
|
+
also remain untouched in the source; nothing is deleted or changed on either
|
|
327
|
+
side.
|
|
328
|
+
|
|
329
|
+
**Selecting specific chats (`--chat-id`)**
|
|
330
|
+
|
|
331
|
+
By default every non-empty source chat is selected. To pick specific ones, pass
|
|
332
|
+
comma-separated IDs (`--chat-id <id-1>,<id-2>`), repeat the flag, or combine both.
|
|
333
|
+
Selection is by `composerId` (shown by `chats`) because chat names are not
|
|
334
|
+
necessarily unique. If an ID doesn't belong to the source, or refers to an
|
|
335
|
+
empty/draft chat, the command stops without making any change.
|
|
336
|
+
|
|
337
|
+
**Overwriting on conflict (`--force`)**
|
|
338
|
+
|
|
339
|
+
During a move between two different Cursor installations, the same `composerId`
|
|
340
|
+
might already exist at the destination. By default such a conflict is skipped and
|
|
341
|
+
nothing is overwritten. `--force` replaces the existing destination chat with the
|
|
342
|
+
source version; the backup is made first, and the source copy is deleted only
|
|
343
|
+
after the destination write succeeds. `--force` is not valid with `--mode copy`,
|
|
344
|
+
since copy always creates a new ID.
|
|
345
|
+
|
|
346
|
+
When source and destination live in **different Cursor installations**, the tool
|
|
347
|
+
performs a full cross-database transfer between the two `globalStorage/state.vscdb`
|
|
348
|
+
files, including message content (`bubbleId`), checkpoints, and related content.
|
|
349
|
+
|
|
350
|
+
### `cleanup workspaces [workspace]` — remove stale workspace entries
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
python3 cursor_workspace_tool.py cleanup workspaces # dry-run, all
|
|
354
|
+
python3 cursor_workspace_tool.py cleanup workspaces --apply # delete all candidates
|
|
355
|
+
python3 cursor_workspace_tool.py cleanup workspaces 6ef46f8f... --apply # one workspace
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Finds workspaces that look like leftovers — `unknown` (missing/unreadable
|
|
359
|
+
`workspace.json`) or `missing` (the folder or `.code-workspace` it pointed to no
|
|
360
|
+
longer exists) — and deletes them **only if they have zero non-empty AI chats**.
|
|
361
|
+
Empty chat records alone do not protect a workspace; a workspace with even one
|
|
362
|
+
real chat is never deleted automatically. If chat counts can't be verified (e.g.
|
|
363
|
+
Cursor is running and the database is locked), the workspace is **skipped**.
|
|
364
|
+
|
|
365
|
+
Passing an explicit `[workspace]` makes it a deletion candidate even if it isn't
|
|
366
|
+
`unknown`/`missing`, but the zero-real-chats protection still applies.
|
|
367
|
+
|
|
368
|
+
**What is deleted (no leftovers):**
|
|
369
|
+
|
|
370
|
+
1. **The whole `workspaceStorage/<id>/` folder** on disk (its per-workspace
|
|
371
|
+
`state.vscdb`, `workspace.json`, and everything else).
|
|
372
|
+
2. **The leftover records in the global database** (`globalStorage/state.vscdb`) —
|
|
373
|
+
the `composerHeaders` and `cursorDiskKV` rows for that workspace's (empty)
|
|
374
|
+
chats — so no orphaned rows remain.
|
|
375
|
+
|
|
376
|
+
> The tool **never touches your actual project files** — only the metadata Cursor
|
|
377
|
+
> stores about the workspace. Shared `composer.content.*` blocks are intentionally
|
|
378
|
+
> preserved because they may be referenced by other chats.
|
|
379
|
+
|
|
380
|
+
Options:
|
|
381
|
+
|
|
382
|
+
- By default `empty-window` (the shared empty-window entry) is **not** touched —
|
|
383
|
+
it's considered a legitimate state, not clutter.
|
|
384
|
+
- `--include-empty-windows` also treats `empty-window` entries as deletion
|
|
385
|
+
candidates (still protected if they contain a non-empty chat).
|
|
386
|
+
|
|
387
|
+
Both the deleted folder and the affected global database are backed up first and
|
|
388
|
+
can be recovered with `restore`.
|
|
389
|
+
|
|
390
|
+
### `cleanup empty-chats [workspace]` — remove empty chat records
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
python3 cursor_workspace_tool.py cleanup empty-chats # dry-run, all
|
|
394
|
+
python3 cursor_workspace_tool.py cleanup empty-chats 6ef46f8f... # dry-run, one workspace
|
|
395
|
+
python3 cursor_workspace_tool.py cleanup empty-chats --apply # delete
|
|
396
|
+
python3 cursor_workspace_tool.py cleanup empty-chats --include-orphaned # + orphaned records
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Deletes only Composer records with zero messages (`bubble_count = 0`), without
|
|
400
|
+
deleting any workspace or project files. Drafts (zero-message records flagged
|
|
401
|
+
`isDraft`) are **always protected** and listed separately at the end with their
|
|
402
|
+
unsent text, so you can see what's in them before deciding. This information is
|
|
403
|
+
also saved in `operation-report.md`.
|
|
404
|
+
|
|
405
|
+
**`--include-orphaned`** — by default only workspaces that still have a
|
|
406
|
+
`workspaceStorage` folder are scanned. This flag also scans the global database
|
|
407
|
+
directly and finds empty chats whose `workspaceId` **has no `workspaceStorage`
|
|
408
|
+
folder** (orphaned records left after a workspace folder was deleted, or created
|
|
409
|
+
by a different installation). Orphans are marked `[orphaned - no workspaceStorage
|
|
410
|
+
folder]` in the output. Only **empty** chats are removed and drafts stay
|
|
411
|
+
protected. The flag is opt-in and is ignored when a specific `[workspace]` is
|
|
412
|
+
given. Workspaces that can't be read (usually because Cursor is running) are
|
|
413
|
+
skipped with a warning.
|
|
414
|
+
|
|
415
|
+
Before `--apply`, the tool verifies the database isn't locked, backs up
|
|
416
|
+
`globalStorage/state.vscdb`, and writes an `operation-report.md`. Dry-run is
|
|
417
|
+
always the default.
|
|
418
|
+
|
|
419
|
+
### `restore <backup-dir>` — roll back a previous change
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
python3 cursor_workspace_tool.py restore ~/.cache/cursor-workspace-tool/backups/20260721_174531
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Shows which files/folders are in the backup and where they will be restored
|
|
426
|
+
(using the exact `manifest.json` saved at backup time — not path guessing), and
|
|
427
|
+
asks for confirmation before copying them back over the originals. Works for both
|
|
428
|
+
`merge` backups (individual files) and `cleanup` backups (whole folders).
|
|
429
|
+
|
|
430
|
+
## Identifying a workspace
|
|
431
|
+
|
|
432
|
+
Anywhere a workspace argument is accepted (`chats`, `merge`, `cleanup`), you can pass:
|
|
433
|
+
|
|
434
|
+
- the **index** (`#`) from the most recent `list` output (cached between runs),
|
|
435
|
+
- the **exact ID** (the folder name under `workspaceStorage/`), or
|
|
436
|
+
- a **path substring**, as long as it's unique (if several match, the tool lists
|
|
437
|
+
them and asks you to be more specific).
|
|
438
|
+
|
|
439
|
+
## FAQ: why are there "weird" values in the `PATH` column?
|
|
440
|
+
|
|
441
|
+
These are all **normal**. Cursor never removes old `workspaceStorage` entries, so
|
|
442
|
+
various leftovers accumulate over time. Importantly, **you don't have to open a
|
|
443
|
+
project "on purpose" for an entry to be created** — each of these creates one:
|
|
444
|
+
|
|
445
|
+
- Opening a single file (e.g. `code file.txt`, or double-clicking a file), not
|
|
446
|
+
just a whole folder.
|
|
447
|
+
- Any empty "New Window" (`Ctrl/Cmd+Shift+N`), even if nothing is saved in it.
|
|
448
|
+
- Extensions/tools that briefly open a Cursor window (some git tools,
|
|
449
|
+
dev-containers, etc.).
|
|
450
|
+
- Moving or renaming a project folder — the **old** entry (with the previous path)
|
|
451
|
+
stays, and a **new** one is created for the new path.
|
|
452
|
+
- Deleting a project or `.code-workspace` file from disk without closing it in
|
|
453
|
+
Cursor first.
|
|
454
|
+
|
|
455
|
+
Specific messages:
|
|
456
|
+
|
|
457
|
+
- **`(unknown - workspace.json missing or unrecognized; ...)`** — the folder has
|
|
458
|
+
no valid `workspace.json`, so the original project can't be determined; likely
|
|
459
|
+
an orphan from a deleted/renamed workspace.
|
|
460
|
+
- **`(MISSING)`** — the tool checked whether the path still exists on disk (from
|
|
461
|
+
the machine running it) and found it gone.
|
|
462
|
+
- **`(empty window - no folder/workspace opened)`** — the single shared
|
|
463
|
+
`empty-window` entry Cursor uses whenever a window opens with nothing loaded.
|
|
464
|
+
- **`/`** — a valid path: a workspace opened on the WSL root directory (`/`)
|
|
465
|
+
itself rather than a specific project.
|
|
466
|
+
|
|
467
|
+
If these entries are no longer relevant (and in particular have no AI chats), use
|
|
468
|
+
the `cleanup` commands above to prune them.
|
|
469
|
+
|
|
470
|
+
## Known limitations
|
|
471
|
+
|
|
472
|
+
- On-disk existence detection is best-effort. For non-WSL remote workspaces
|
|
473
|
+
(SSH, dev-containers) it can't be verified from this machine, so `cleanup` does
|
|
474
|
+
**not** treat them as `missing`; they're deleted only if their `workspace.json`
|
|
475
|
+
is entirely missing/corrupt (`unknown`) **and** they have no non-empty chats.
|
|
476
|
+
- When copying (`--mode copy`) a chat between two different Cursor installations,
|
|
477
|
+
large content blocks stored in content-addressable storage
|
|
478
|
+
(`composer.content.*`) are copied best-effort (by scanning the chat JSON for
|
|
479
|
+
references). In very rare cases of missing content, only that specific
|
|
480
|
+
checkpoint/diff would be inaccessible; the chat text itself is unaffected.
|
|
481
|
+
- The tool does not connect to WSL over `\\wsl$` from Windows. If you run it on
|
|
482
|
+
Windows, use WSL itself (or `--root` with a UNC path) to reach that side.
|
|
483
|
+
|
|
484
|
+
## License
|
|
485
|
+
|
|
486
|
+
Licensed under the **GNU General Public License v3.0** (GPL-3.0). See the
|
|
487
|
+
[LICENSE](LICENSE) file for the full text.
|
|
488
|
+
|
|
489
|
+
This is a copyleft license: you are free to use, modify and redistribute this
|
|
490
|
+
tool, but any distributed derivative work must also be released under the GPL and
|
|
491
|
+
keep the original copyright and license notices.
|
|
492
|
+
|
|
493
|
+
Copyright (C) 2026 Aviv Raz <avivraz123@gmail.com> —
|
|
494
|
+
[https://github.com/aviv-raz](https://github.com/aviv-raz)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
cursor_workspace_tool.py,sha256=QjcOUzwiibk_aBPpvyCoi1pNtBFmoxqyomFppjsBNeM,105148
|
|
2
|
+
cursor_workspace_tool-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
+
cursor_workspace_tool-0.1.0.dist-info/METADATA,sha256=2Uq8uSfxTsPXM7mbVN0Ckne-rJrjKdZfH2LTVfctASI,22573
|
|
4
|
+
cursor_workspace_tool-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
+
cursor_workspace_tool-0.1.0.dist-info/entry_points.txt,sha256=vn2VdvkKsRHef1zJxv5s9S2jNzLGoz-OqGskDkAHABA,102
|
|
6
|
+
cursor_workspace_tool-0.1.0.dist-info/top_level.txt,sha256=4VrPsnGx85QkSKgp-zjQHewFOja3nG1kcYNzQn9xdNY,22
|
|
7
|
+
cursor_workspace_tool-0.1.0.dist-info/RECORD,,
|