ccmv 1.0.0
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.
- package/CLAUDE.md +159 -0
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/bin/ccmv.js +5 -0
- package/docs/ARCHITECTURE.md +438 -0
- package/lib/claude.js +311 -0
- package/lib/cursor.js +582 -0
- package/lib/index.js +352 -0
- package/lib/logger.js +60 -0
- package/lib/utils.js +93 -0
- package/package.json +36 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# Claude Code & Cursor Internal Architecture
|
|
2
|
+
|
|
3
|
+
This document provides detailed documentation of the internal data structures used by Claude Code and Cursor.
|
|
4
|
+
|
|
5
|
+
## Claude Code Data Structure
|
|
6
|
+
|
|
7
|
+
### Directory Layout
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
~/.claude/
|
|
11
|
+
├── projects/ # Per-project session data
|
|
12
|
+
│ └── {encoded-path}/ # Encoded path (see below)
|
|
13
|
+
│ ├── {session-id}.jsonl # Main session file
|
|
14
|
+
│ ├── agent-{agent-id}.jsonl # Sub-agent sessions
|
|
15
|
+
│ └── tool-results/ # Tool execution result cache
|
|
16
|
+
│ └── *.txt
|
|
17
|
+
├── history.jsonl # Global history (all projects)
|
|
18
|
+
├── file-history/ # File change history
|
|
19
|
+
├── backups/ # Backups from ccmv etc.
|
|
20
|
+
├── commands/ # Custom commands
|
|
21
|
+
├── agents/ # Custom agents
|
|
22
|
+
├── cache/ # Various caches
|
|
23
|
+
├── debug/ # Debug logs
|
|
24
|
+
└── downloads/ # Downloaded files
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Path Encoding
|
|
28
|
+
|
|
29
|
+
Claude Code encodes project paths using the following rules:
|
|
30
|
+
|
|
31
|
+
| Original | Encoded |
|
|
32
|
+
|----------|---------|
|
|
33
|
+
| `/` (slash) | `-` |
|
|
34
|
+
| `:` (colon) | `-` |
|
|
35
|
+
| ` ` (space) | `-` |
|
|
36
|
+
|
|
37
|
+
**Example:**
|
|
38
|
+
```
|
|
39
|
+
/Users/jane/Documents/repos/myproject
|
|
40
|
+
→ -Users-jane-Documents-repos-myproject
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Session File Format (*.jsonl)
|
|
44
|
+
|
|
45
|
+
Each line is a single JSON object. Different `type` values per line.
|
|
46
|
+
|
|
47
|
+
#### Record Types
|
|
48
|
+
|
|
49
|
+
| type | Description |
|
|
50
|
+
|------|-------------|
|
|
51
|
+
| `queue-operation` | Queue operations (enqueue/dequeue) |
|
|
52
|
+
| `user` | User messages |
|
|
53
|
+
| `assistant` | Claude's responses |
|
|
54
|
+
| `tool_use` | Tool invocations |
|
|
55
|
+
| `tool_result` | Tool execution results |
|
|
56
|
+
|
|
57
|
+
#### Common Fields
|
|
58
|
+
|
|
59
|
+
Fields present in all records:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"type": "user|assistant|tool_use|...",
|
|
64
|
+
"uuid": "fd49a8a2-68f3-4cdf-81f8-38508beda957",
|
|
65
|
+
"parentUuid": "UUID of previous message (null for first)",
|
|
66
|
+
"sessionId": "fe4c5fad-9703-4485-9e4f-b73a1f638c02",
|
|
67
|
+
"timestamp": "2026-01-07T12:33:48.236Z",
|
|
68
|
+
"cwd": "/Users/jane/project",
|
|
69
|
+
"gitBranch": "main",
|
|
70
|
+
"version": "2.0.72",
|
|
71
|
+
"isSidechain": false,
|
|
72
|
+
"userType": "external"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### User Message
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"type": "user",
|
|
81
|
+
"message": {
|
|
82
|
+
"role": "user",
|
|
83
|
+
"content": "User input text"
|
|
84
|
+
},
|
|
85
|
+
...common fields
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Assistant Message
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"type": "assistant",
|
|
94
|
+
"message": {
|
|
95
|
+
"model": "claude-sonnet-4-5-20250929",
|
|
96
|
+
"id": "msg_014FrefSYxpSnNHJcpNJZugw",
|
|
97
|
+
"type": "message",
|
|
98
|
+
"role": "assistant",
|
|
99
|
+
"content": [
|
|
100
|
+
{ "type": "text", "text": "Response text" },
|
|
101
|
+
{ "type": "tool_use", "id": "toolu_xxx", "name": "Read", "input": {...} }
|
|
102
|
+
],
|
|
103
|
+
"stop_reason": "end_turn|tool_use|null",
|
|
104
|
+
"usage": {
|
|
105
|
+
"input_tokens": 3,
|
|
106
|
+
"cache_creation_input_tokens": 22320,
|
|
107
|
+
"cache_read_input_tokens": 0,
|
|
108
|
+
"output_tokens": 1
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"requestId": "req_011CWt2PcwHgdwPS37vwqRzv",
|
|
112
|
+
...common fields
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Agent Session Files (agent-*.jsonl)
|
|
117
|
+
|
|
118
|
+
Sub-agent sessions (launched via Task tool) are stored in separate files.
|
|
119
|
+
|
|
120
|
+
**Characteristics:**
|
|
121
|
+
- `isSidechain: true` - Indicates branching from main session
|
|
122
|
+
- `agentId: "ae1f9f8"` - Short agent ID (used in filename)
|
|
123
|
+
- Same `sessionId` - Belongs to the same session as parent
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"type": "user",
|
|
128
|
+
"isSidechain": true,
|
|
129
|
+
"agentId": "ae1f9f8",
|
|
130
|
+
"message": { "role": "user", "content": "Warmup" },
|
|
131
|
+
"sessionId": "fe4c5fad-9703-4485-9e4f-b73a1f638c02",
|
|
132
|
+
...
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Global History (history.jsonl)
|
|
137
|
+
|
|
138
|
+
User input history across all projects. Each line records a user input:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"display": "want to check PR, what PR do we have?",
|
|
143
|
+
"pastedContents": {},
|
|
144
|
+
"timestamp": 1759203123612,
|
|
145
|
+
"project": "/Users/jane/Documents/myproject"
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Fields:**
|
|
150
|
+
- `display`: Display text (includes commands like "/context ")
|
|
151
|
+
- `pastedContents`: Pasted content (ID → content map)
|
|
152
|
+
- `timestamp`: Unix timestamp (milliseconds)
|
|
153
|
+
- `project`: Absolute path to project
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Cursor Data Structure
|
|
158
|
+
|
|
159
|
+
### Directory Layout
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
~/Library/Application Support/Cursor/
|
|
163
|
+
└── User/
|
|
164
|
+
├── globalStorage/
|
|
165
|
+
│ ├── storage.json # Global settings and profile associations
|
|
166
|
+
│ └── state.vscdb # Global state (SQLite)
|
|
167
|
+
└── workspaceStorage/
|
|
168
|
+
└── {md5-hash}/ # Per-workspace directory
|
|
169
|
+
├── workspace.json # Workspace configuration
|
|
170
|
+
└── state.vscdb # Workspace state (SQLite, includes chat history)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Path Format
|
|
174
|
+
|
|
175
|
+
Cursor stores paths in file:// URI format:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
/Users/jane/my project
|
|
179
|
+
→ file:///Users/jane/my%20project
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**URL Encoding Rules:**
|
|
183
|
+
- Space → `%20`
|
|
184
|
+
- Multibyte characters (Japanese, etc.) are also URL-encoded
|
|
185
|
+
|
|
186
|
+
### Workspace Hash Calculation
|
|
187
|
+
|
|
188
|
+
Workspace directory names are calculated as **MD5(path + birthtime_ms)**:
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
const fs = require('fs');
|
|
192
|
+
const crypto = require('crypto');
|
|
193
|
+
|
|
194
|
+
const path = '/Users/jane/myproject';
|
|
195
|
+
const stat = fs.statSync(path);
|
|
196
|
+
const hash = crypto.createHash('md5')
|
|
197
|
+
.update(path)
|
|
198
|
+
.update(String(stat.birthtime.getTime())) // birthtime in milliseconds
|
|
199
|
+
.digest('hex');
|
|
200
|
+
// → "a1b2c3d4e5f6..."
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Important Notes:**
|
|
204
|
+
|
|
205
|
+
1. **Node.js Required**: Python's `os.stat().st_birthtime` rounds milliseconds differently (e.g., 583 vs 584), causing hash mismatch
|
|
206
|
+
2. **birthtime Preservation**: `mv` within the same volume preserves birthtime, so the new path's hash is predictable
|
|
207
|
+
3. **Cross-volume Moves**: Moving to a different volume may change birthtime (untested)
|
|
208
|
+
|
|
209
|
+
### storage.json
|
|
210
|
+
|
|
211
|
+
Profile-related settings. Multiple path formats coexist:
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"profileAssociations": {
|
|
216
|
+
"workspaces": {
|
|
217
|
+
"file:///Users/jane/project-a": "profile-id-1",
|
|
218
|
+
"/Users/jane/project-b": "profile-id-2",
|
|
219
|
+
"~/Documents/project-c": "profile-id-3"
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
...
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Path Formats:**
|
|
227
|
+
- `file://` URI
|
|
228
|
+
- Absolute path
|
|
229
|
+
- Tilde-expanded path (`~/...`)
|
|
230
|
+
|
|
231
|
+
### state.vscdb (SQLite)
|
|
232
|
+
|
|
233
|
+
SQLite database. Main tables:
|
|
234
|
+
|
|
235
|
+
#### ItemTable
|
|
236
|
+
|
|
237
|
+
Key-Value store. `value` column contains JSON strings:
|
|
238
|
+
|
|
239
|
+
```sql
|
|
240
|
+
CREATE TABLE ItemTable (
|
|
241
|
+
key TEXT PRIMARY KEY,
|
|
242
|
+
value TEXT
|
|
243
|
+
);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Key Keys:**
|
|
247
|
+
- `history.recentlyOpenedPathsList` - Recently opened projects list
|
|
248
|
+
- `repositoryTracker.paths` - Git repository tracking info
|
|
249
|
+
|
|
250
|
+
#### Global state.vscdb vs Workspace state.vscdb
|
|
251
|
+
|
|
252
|
+
| Location | Contents |
|
|
253
|
+
|----------|----------|
|
|
254
|
+
| `globalStorage/state.vscdb` | App-wide state (recently opened projects, etc.) |
|
|
255
|
+
| `workspaceStorage/{hash}/state.vscdb` | Per-workspace state (includes chat history) |
|
|
256
|
+
|
|
257
|
+
### workspace.json
|
|
258
|
+
|
|
259
|
+
Configuration file for each workspace:
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"folder": "file:///Users/jane/project"
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Or (when using workspace file):
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"workspace": "file:///Users/jane/project.code-workspace"
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Chat History Storage (cursorDiskKV)
|
|
276
|
+
|
|
277
|
+
Cursor's chat history is stored in `workspaceStorage/{hash}/state.vscdb` in the `cursorDiskKV` table:
|
|
278
|
+
|
|
279
|
+
```sql
|
|
280
|
+
CREATE TABLE cursorDiskKV (
|
|
281
|
+
key TEXT PRIMARY KEY,
|
|
282
|
+
value TEXT
|
|
283
|
+
);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Key Keys:**
|
|
287
|
+
- `composer.composerData` - Composer (chat) data
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Migration Considerations
|
|
292
|
+
|
|
293
|
+
### ccmv Processing Flow
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
1. validate - Validate paths
|
|
297
|
+
2. detect_cursor - Detect Cursor installation
|
|
298
|
+
3. check_cursor_not_running - Ensure Cursor is closed
|
|
299
|
+
4. find_cursor_workspaces - Identify workspace by hash calculation
|
|
300
|
+
5. create_backup - Create backups
|
|
301
|
+
6. backup_cursor_data - Backup Cursor data
|
|
302
|
+
7. move_project - Move project directory
|
|
303
|
+
8. rename_cursor_workspace - Rename workspace directory
|
|
304
|
+
9. rename_claude_dir - Rename Claude project directory
|
|
305
|
+
10. update_project_files - Update cwd in session files
|
|
306
|
+
11. update_history - Update history.jsonl
|
|
307
|
+
12. update_cursor_* - Update various Cursor files
|
|
308
|
+
13. verify - Verify migration
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Key Update Points
|
|
312
|
+
|
|
313
|
+
#### Claude Code
|
|
314
|
+
|
|
315
|
+
| File/Directory | Update |
|
|
316
|
+
|----------------|--------|
|
|
317
|
+
| `~/.claude/projects/{encoded-old}/` | Rename directory |
|
|
318
|
+
| `cwd` field in `*.jsonl` | String replacement |
|
|
319
|
+
| `project` field in `history.jsonl` | String replacement |
|
|
320
|
+
|
|
321
|
+
#### Cursor
|
|
322
|
+
|
|
323
|
+
| File | Update |
|
|
324
|
+
|------|--------|
|
|
325
|
+
| `workspaceStorage/{old-hash}/` | Rename directory to new hash |
|
|
326
|
+
| `workspace.json` | Update `folder`/`workspace` URI |
|
|
327
|
+
| `storage.json` | Update keys in `profileAssociations.workspaces` |
|
|
328
|
+
| `state.vscdb` (global) | Update paths in ItemTable values |
|
|
329
|
+
| `state.vscdb` (workspace) | Update paths in ItemTable + cursorDiskKV |
|
|
330
|
+
|
|
331
|
+
### Duplicate Workspace Handling
|
|
332
|
+
|
|
333
|
+
Cursor can create multiple workspace directories for the same path (from failed migrations, Cursor quirks, etc.).
|
|
334
|
+
|
|
335
|
+
**Merge Strategy:**
|
|
336
|
+
1. Detect all workspaces pointing to the new path
|
|
337
|
+
2. Compare `state.vscdb` sizes
|
|
338
|
+
3. Keep the larger one (more data)
|
|
339
|
+
4. Remove duplicates
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Appendix
|
|
344
|
+
|
|
345
|
+
### JSON Examples
|
|
346
|
+
|
|
347
|
+
#### Complete User Message Record
|
|
348
|
+
|
|
349
|
+
```json
|
|
350
|
+
{
|
|
351
|
+
"parentUuid": null,
|
|
352
|
+
"isSidechain": false,
|
|
353
|
+
"userType": "external",
|
|
354
|
+
"cwd": "/Users/jane/projects/myapp",
|
|
355
|
+
"sessionId": "fe4c5fad-9703-4485-9e4f-b73a1f638c02",
|
|
356
|
+
"version": "2.0.72",
|
|
357
|
+
"gitBranch": "feature-branch",
|
|
358
|
+
"type": "user",
|
|
359
|
+
"message": {
|
|
360
|
+
"role": "user",
|
|
361
|
+
"content": "want to check PR, what PR do we have?"
|
|
362
|
+
},
|
|
363
|
+
"uuid": "fd49a8a2-68f3-4cdf-81f8-38508beda957",
|
|
364
|
+
"timestamp": "2026-01-07T12:33:48.236Z"
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### Complete Assistant Message Record
|
|
369
|
+
|
|
370
|
+
```json
|
|
371
|
+
{
|
|
372
|
+
"parentUuid": "fd49a8a2-68f3-4cdf-81f8-38508beda957",
|
|
373
|
+
"isSidechain": false,
|
|
374
|
+
"userType": "external",
|
|
375
|
+
"cwd": "/Users/jane/projects/myapp",
|
|
376
|
+
"sessionId": "fe4c5fad-9703-4485-9e4f-b73a1f638c02",
|
|
377
|
+
"version": "2.0.72",
|
|
378
|
+
"gitBranch": "feature-branch",
|
|
379
|
+
"message": {
|
|
380
|
+
"model": "claude-sonnet-4-5-20250929",
|
|
381
|
+
"id": "msg_014FrefSYxpSnNHJcpNJZugw",
|
|
382
|
+
"type": "message",
|
|
383
|
+
"role": "assistant",
|
|
384
|
+
"content": [
|
|
385
|
+
{
|
|
386
|
+
"type": "text",
|
|
387
|
+
"text": "I'll check the PR list for you."
|
|
388
|
+
}
|
|
389
|
+
],
|
|
390
|
+
"stop_reason": null,
|
|
391
|
+
"stop_sequence": null,
|
|
392
|
+
"usage": {
|
|
393
|
+
"input_tokens": 3,
|
|
394
|
+
"cache_creation_input_tokens": 22320,
|
|
395
|
+
"cache_read_input_tokens": 0,
|
|
396
|
+
"cache_creation": {
|
|
397
|
+
"ephemeral_5m_input_tokens": 22320,
|
|
398
|
+
"ephemeral_1h_input_tokens": 0
|
|
399
|
+
},
|
|
400
|
+
"output_tokens": 1,
|
|
401
|
+
"service_tier": "standard"
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
"requestId": "req_011CWt2PcwHgdwPS37vwqRzv",
|
|
405
|
+
"type": "assistant",
|
|
406
|
+
"uuid": "0b0ede00-8fff-4d9e-b79e-fb4395ea2d34",
|
|
407
|
+
"timestamp": "2026-01-07T12:33:54.356Z"
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Useful Commands
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
# Claude Code: Check cwd values in session files
|
|
415
|
+
grep -h "\"cwd\"" ~/.claude/projects/*/fe*.jsonl | jq -r '.cwd' | sort -u
|
|
416
|
+
|
|
417
|
+
# Claude Code: Count sessions for a specific project
|
|
418
|
+
ls -la ~/.claude/projects/-Users-jane-Desktop-myproject/*.jsonl | wc -l
|
|
419
|
+
|
|
420
|
+
# Cursor: Calculate workspace hash
|
|
421
|
+
node -e "
|
|
422
|
+
const fs = require('fs');
|
|
423
|
+
const crypto = require('crypto');
|
|
424
|
+
const path = '/Users/jane/myproject';
|
|
425
|
+
const stat = fs.statSync(path);
|
|
426
|
+
console.log(crypto.createHash('md5')
|
|
427
|
+
.update(path)
|
|
428
|
+
.update(String(stat.birthtime.getTime()))
|
|
429
|
+
.digest('hex'));
|
|
430
|
+
"
|
|
431
|
+
|
|
432
|
+
# Cursor: Get recently opened projects from state.vscdb
|
|
433
|
+
sqlite3 ~/Library/Application\ Support/Cursor/User/globalStorage/state.vscdb \
|
|
434
|
+
"SELECT value FROM ItemTable WHERE key = 'history.recentlyOpenedPathsList';" | jq .
|
|
435
|
+
|
|
436
|
+
# Cursor: Check workspace folders
|
|
437
|
+
cat ~/Library/Application\ Support/Cursor/User/workspaceStorage/*/workspace.json | jq -r '.folder // .workspace'
|
|
438
|
+
```
|