spiracha 1.2.0 → 1.3.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.
Files changed (149) hide show
  1. package/AGENTS.md +49 -12
  2. package/README.md +117 -64
  3. package/apps/ui/AGENTS.md +16 -8
  4. package/apps/ui/README.md +28 -12
  5. package/apps/ui/dist/client/assets/{analytics-Cv0JMDN2.js → analytics-B_hYz65v.js} +1 -1
  6. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-qiyygB7e.js +1 -0
  7. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-z1SQC2Kg.js +1 -0
  8. package/apps/ui/dist/client/assets/antigravity-keychain-panel-dYuRWtCf.js +1 -0
  9. package/apps/ui/dist/client/assets/antigravity._workspaceKey-CliqUr7o.js +1 -0
  10. package/apps/ui/dist/client/assets/antigravity._workspaceKey-CnoBzyX6.js +1 -0
  11. package/apps/ui/dist/client/assets/antigravity.index-CakfZz_E.js +1 -0
  12. package/apps/ui/dist/client/assets/antigravity.index-DY7M1KhG.js +1 -0
  13. package/apps/ui/dist/client/assets/badge-aHE9ETVe.js +1 -0
  14. package/apps/ui/dist/client/assets/checkbox-DN3XnJaA.js +1 -0
  15. package/apps/ui/dist/client/assets/cursor-threads._composerId-BMQyx8qG.js +1 -0
  16. package/apps/ui/dist/client/assets/cursor-threads._composerId-BTlaA-tV.js +1 -0
  17. package/apps/ui/dist/client/assets/cursor._workspaceKey-CrgrfevV.js +1 -0
  18. package/apps/ui/dist/client/assets/cursor._workspaceKey-bYS2syGL.js +1 -0
  19. package/apps/ui/dist/client/assets/cursor.index-CTqZMPYU.js +1 -0
  20. package/apps/ui/dist/client/assets/cursor.index-Clsz4E_e.js +2 -0
  21. package/apps/ui/dist/client/assets/{data-table-Bgnh7phF.js → data-table-Cj-v-uyB.js} +2 -2
  22. package/apps/ui/dist/client/assets/delete-confirm-dialog-DTpzBiNK.js +11 -0
  23. package/apps/ui/dist/client/assets/dist-BNAn99Pu.js +1 -0
  24. package/apps/ui/dist/client/assets/download-P3Rp23Ad.js +1 -0
  25. package/apps/ui/dist/client/assets/dropdown-menu-3qB5j9nt.js +1 -0
  26. package/apps/ui/dist/client/assets/es2015-Dwm_turD.js +41 -0
  27. package/apps/ui/dist/client/assets/export-dialog-CazdrASq.js +1 -0
  28. package/apps/ui/dist/client/assets/formatters-BdnWuM1z.js +1 -0
  29. package/apps/ui/dist/client/assets/index-BVFnfS78.js +22 -0
  30. package/apps/ui/dist/client/assets/json-panel-DLkS30sQ.js +1 -0
  31. package/apps/ui/dist/client/assets/metadata-section-jnIkB7dB.js +1 -0
  32. package/apps/ui/dist/client/assets/{metric-card-BJX5rkHK.js → metric-card-CBZuWLzQ.js} +1 -1
  33. package/apps/ui/dist/client/assets/page-header-CnD21cPn.js +1 -0
  34. package/apps/ui/dist/client/assets/projects._project-BLszwvYL.js +1 -0
  35. package/apps/ui/dist/client/assets/projects._project-DvLxYbvk.js +1 -0
  36. package/apps/ui/dist/client/assets/projects.index-COn8woBR.js +1 -0
  37. package/apps/ui/dist/client/assets/projects.index-DYs98skV.js +3 -0
  38. package/apps/ui/dist/client/assets/refresh-ccw-BDrYXjtD.js +1 -0
  39. package/apps/ui/dist/client/assets/reload-error-panel-DLAg0AW2.js +1 -0
  40. package/apps/ui/dist/client/assets/routes-BtF5-coe.js +1 -0
  41. package/apps/ui/dist/client/assets/scroll-text-CqaFm9by.js +1 -0
  42. package/apps/ui/dist/client/assets/select-DbnpwqL6.js +1 -0
  43. package/apps/ui/dist/client/assets/settings-CGX3VleN.js +1 -0
  44. package/apps/ui/dist/client/assets/styles-Ch0r3kMZ.css +1 -0
  45. package/apps/ui/dist/client/assets/text-document-panel-DPleOmmq.js +1 -0
  46. package/apps/ui/dist/client/assets/text-filter-7M6wRo-t.js +2 -0
  47. package/apps/ui/dist/client/assets/threads._threadId-D5w76IB-.js +7 -0
  48. package/apps/ui/dist/client/assets/{threads._threadId-CUiCZSwo.js → threads._threadId-Dx85sI9P.js} +1 -1
  49. package/apps/ui/dist/client/assets/useMutation-MZ3Hr9h9.js +1 -0
  50. package/apps/ui/dist/client/assets/useQuery-Cb4V0AT0.js +1 -0
  51. package/apps/ui/dist/client/icon.svg +28 -0
  52. package/apps/ui/dist/client/manifest.json +6 -16
  53. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CBbkUXw6.js +227 -0
  54. package/apps/ui/dist/server/assets/{analytics-2QpLKjlG.js → analytics-CBNOYZwJ.js} +2 -2
  55. package/apps/ui/dist/server/assets/antigravity-conversation-state-HgzS302O.js +16 -0
  56. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-B9Rm4EXh.js +212 -0
  57. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BIdYNy68.js +20 -0
  58. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-D426O-64.js +11 -0
  59. package/apps/ui/dist/server/assets/antigravity-db-D9gW1D8G.js +576 -0
  60. package/apps/ui/dist/server/assets/antigravity-keychain-DOiuHDwK.js +126 -0
  61. package/apps/ui/dist/server/assets/antigravity-keychain-panel-DcLyBBwd.js +55 -0
  62. package/apps/ui/dist/server/assets/antigravity-queries-CgQhlQ7J.js +37 -0
  63. package/apps/ui/dist/server/assets/antigravity-server-DFUx4Khk.js +114 -0
  64. package/apps/ui/dist/server/assets/antigravity._workspaceKey-3m_MzNFA.js +11 -0
  65. package/apps/ui/dist/server/assets/antigravity._workspaceKey-D42ixtzp.js +210 -0
  66. package/apps/ui/dist/server/assets/antigravity._workspaceKey-DnSlSC-C.js +28 -0
  67. package/apps/ui/dist/server/assets/antigravity.index-DZVT-cac.js +104 -0
  68. package/apps/ui/dist/server/assets/antigravity.index-DudTB3Tq.js +11 -0
  69. package/apps/ui/dist/server/assets/badge-EvdhKK_Z.js +26 -0
  70. package/apps/ui/dist/server/assets/{codex-queries-BH4Cb0v3.js → codex-queries-eOJGfHQj.js} +4 -16
  71. package/apps/ui/dist/server/assets/{codex-server-DqzruLmg.js → codex-server-nrETIF--.js} +149 -140
  72. package/apps/ui/dist/server/assets/createServerRpc-BtXIw2iP.js +12 -0
  73. package/apps/ui/dist/server/assets/createSsrRpc-COf5Zuye.js +16 -0
  74. package/apps/ui/dist/server/assets/cursor-db-B7agkAvM.js +643 -0
  75. package/apps/ui/dist/server/assets/cursor-exporter-types-CI3goo-c.js +34 -0
  76. package/apps/ui/dist/server/assets/cursor-queries-BMhuJeUO.js +65 -0
  77. package/apps/ui/dist/server/assets/cursor-recovery-9bJLs7vG.js +361 -0
  78. package/apps/ui/dist/server/assets/cursor-server-BgylIFgn.js +184 -0
  79. package/apps/ui/dist/server/assets/cursor-threads._composerId-BB0Y_Mao.js +11 -0
  80. package/apps/ui/dist/server/assets/cursor-threads._composerId-BsxFKzoJ.js +218 -0
  81. package/apps/ui/dist/server/assets/cursor-threads._composerId-DXffY_CK.js +18 -0
  82. package/apps/ui/dist/server/assets/cursor-transcript-2iL3KFSK.js +125 -0
  83. package/apps/ui/dist/server/assets/cursor._workspaceKey-BP2J1x_x.js +28 -0
  84. package/apps/ui/dist/server/assets/cursor._workspaceKey-BQd0e-Pd.js +399 -0
  85. package/apps/ui/dist/server/assets/cursor._workspaceKey-nmg3YIOQ.js +11 -0
  86. package/apps/ui/dist/server/assets/cursor.index-CQVxtCm8.js +189 -0
  87. package/apps/ui/dist/server/assets/cursor.index-CcsX7DG0.js +11 -0
  88. package/apps/ui/dist/server/assets/{delete-confirm-dialog-CWqcTXTF.js → delete-confirm-dialog-PCD7S0_M.js} +5 -4
  89. package/apps/ui/dist/server/assets/download-DMmiy1xf.js +92 -0
  90. package/apps/ui/dist/server/assets/{input-B4tEzctc.js → dropdown-menu-Dy_9t6TN.js} +1 -11
  91. package/apps/ui/dist/server/assets/{download-Drctxary.js → export-dialog-DaPlOGFT.js} +1 -92
  92. package/apps/ui/dist/server/assets/json-panel-RYsxWFae.js +16 -0
  93. package/apps/ui/dist/server/assets/{loading-panel-DbLdvjtR.js → loading-panel-BGFnWseS.js} +1 -1
  94. package/apps/ui/dist/server/assets/metadata-section-D6Lbc7D6.js +54 -0
  95. package/apps/ui/dist/server/assets/page-header-VNSaM3xd.js +29 -0
  96. package/apps/ui/dist/server/assets/projects._project-Bshqk7JA.js +12 -0
  97. package/apps/ui/dist/server/assets/{projects._project-gT01HBqH.js → projects._project-DUN3iWfg.js} +4 -4
  98. package/apps/ui/dist/server/assets/{projects._project-DreIU5b0.js → projects._project-Dim9Y0kD.js} +54 -26
  99. package/apps/ui/dist/server/assets/projects.index-BLXOx5eL.js +12 -0
  100. package/apps/ui/dist/server/assets/{projects.index-BYmgSGAj.js → projects.index-DjSQK5dm.js} +23 -27
  101. package/apps/ui/dist/server/assets/{projects.index-CaplpeMy.js → reload-error-panel-BJMxY3U1.js} +5 -6
  102. package/apps/ui/dist/server/assets/{router-Qj5Kn7bl.js → router-DrDgc-LD.js} +131 -44
  103. package/apps/ui/dist/server/assets/{routes-_LbCIjtJ.js → routes-B-GlEe2C.js} +54 -39
  104. package/apps/ui/dist/server/assets/{routes-BtcXuK0x.js → routes-CNHAUMwo.js} +2 -2
  105. package/apps/ui/dist/server/assets/{settings-MvWDgc1u.js → settings-OayxIYQQ.js} +1 -1
  106. package/apps/ui/dist/server/assets/shared-CPRNYIql.js +134 -0
  107. package/apps/ui/dist/server/assets/text-document-panel-D8JbQWAn.js +23 -0
  108. package/apps/ui/dist/server/assets/text-filter-CGKxMCKt.js +36 -0
  109. package/apps/ui/dist/server/assets/{threads._threadId-DcbAJkwf.js → threads._threadId-CJzm4KrZ.js} +3 -3
  110. package/apps/ui/dist/server/assets/{threads._threadId-D5m6ypGw.js → threads._threadId-DODTYddm.js} +69 -76
  111. package/apps/ui/dist/server/server.js +77 -13
  112. package/package.json +19 -9
  113. package/src/export-cursor.ts +244 -0
  114. package/src/lib/antigravity-db.ts +936 -0
  115. package/src/lib/antigravity-exporter-types.ts +70 -0
  116. package/src/lib/antigravity-keychain.ts +203 -0
  117. package/src/lib/codex-browser-db.ts +7 -1
  118. package/src/lib/codex-browser-types.ts +22 -1
  119. package/src/lib/codex-thread-recovery.ts +202 -0
  120. package/src/lib/cursor-db.ts +1096 -0
  121. package/src/lib/cursor-exporter-types.ts +190 -0
  122. package/src/lib/cursor-exporter.ts +266 -0
  123. package/src/lib/cursor-recovery.ts +543 -0
  124. package/src/lib/cursor-transcript.ts +183 -0
  125. package/src/spiracha.ts +16 -3
  126. package/src/ui-cli.ts +2 -2
  127. package/apps/ui/dist/client/assets/checkbox-DjHij7DJ.js +0 -1
  128. package/apps/ui/dist/client/assets/delete-confirm-dialog-CIZy_LXD.js +0 -11
  129. package/apps/ui/dist/client/assets/download-DQtfva4z.js +0 -1
  130. package/apps/ui/dist/client/assets/es2015-DsDKdYCE.js +0 -41
  131. package/apps/ui/dist/client/assets/formatters-CWFrMKSn.js +0 -1
  132. package/apps/ui/dist/client/assets/index-C_-e0lDI.js +0 -22
  133. package/apps/ui/dist/client/assets/input-BbgApiqZ.js +0 -1
  134. package/apps/ui/dist/client/assets/page-header-ODLuGLAB.js +0 -1
  135. package/apps/ui/dist/client/assets/projects._project-C2Pys_bB.js +0 -1
  136. package/apps/ui/dist/client/assets/projects._project-CHvAKvlu.js +0 -1
  137. package/apps/ui/dist/client/assets/projects.index-BmwtS1x-.js +0 -1
  138. package/apps/ui/dist/client/assets/projects.index-CuLw73mt.js +0 -1
  139. package/apps/ui/dist/client/assets/routes-CfnaTOlj.js +0 -1
  140. package/apps/ui/dist/client/assets/select-B1kH_5lx.js +0 -1
  141. package/apps/ui/dist/client/assets/settings-mYTB3sso.js +0 -1
  142. package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +0 -1
  143. package/apps/ui/dist/client/assets/threads._threadId-C_47okme.js +0 -7
  144. package/apps/ui/dist/client/favicon.ico +0 -0
  145. package/apps/ui/dist/client/logo192.png +0 -0
  146. package/apps/ui/dist/client/logo512.png +0 -0
  147. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-kj_QB_26.js +0 -99
  148. package/apps/ui/dist/server/assets/page-header-CxdZM86z.js +0 -25
  149. package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +0 -26
@@ -0,0 +1,190 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import type { ExportFormat } from './shared';
4
+
5
+ type CursorPlatform = NodeJS.Platform;
6
+
7
+ // Cursor keeps chat history in two SQLite stores under the user data dir:
8
+ // - per-workspace buckets under workspaceStorage/<bucketId>/state.vscdb
9
+ // - a single globalStorage/state.vscdb holding composer headers and message bubbles
10
+ export const getDefaultCursorUserDir = (
11
+ platform: CursorPlatform = process.platform,
12
+ env: NodeJS.ProcessEnv = process.env,
13
+ homeDir = os.homedir(),
14
+ ): string => {
15
+ if (platform === 'win32') {
16
+ return path.win32.join(env.APPDATA || path.win32.join(homeDir, 'AppData', 'Roaming'), 'Cursor', 'User');
17
+ }
18
+
19
+ if (platform === 'linux') {
20
+ return path.posix.join(env.XDG_DATA_HOME || path.posix.join(homeDir, '.local', 'share'), 'Cursor', 'User');
21
+ }
22
+
23
+ return path.posix.join(homeDir, 'Library', 'Application Support', 'Cursor', 'User');
24
+ };
25
+
26
+ export const DEFAULT_CURSOR_USER_DIR = getDefaultCursorUserDir();
27
+
28
+ export const resolveCursorUserDir = (): string => {
29
+ const configured = process.env.SPIRACHA_CURSOR_USER_DIR?.trim();
30
+ return configured ? configured : DEFAULT_CURSOR_USER_DIR;
31
+ };
32
+
33
+ export const getCursorWorkspaceStorageDir = (userDir = resolveCursorUserDir()): string =>
34
+ path.join(userDir, 'workspaceStorage');
35
+
36
+ export const getCursorGlobalDbPath = (userDir = resolveCursorUserDir()): string =>
37
+ path.join(userDir, 'globalStorage', 'state.vscdb');
38
+
39
+ const inferHomeDirFromCursorUserDir = (userDir: string): string | null => {
40
+ const normalized = userDir.replace(/\\/gu, '/');
41
+ const macSuffix = '/Library/Application Support/Cursor/User';
42
+ const windowsSuffix = '/AppData/Roaming/Cursor/User';
43
+ const linuxSuffix = '/.local/share/Cursor/User';
44
+
45
+ for (const suffix of [macSuffix, windowsSuffix, linuxSuffix]) {
46
+ if (normalized.endsWith(suffix)) {
47
+ return userDir.slice(0, userDir.length - suffix.length);
48
+ }
49
+ }
50
+
51
+ return null;
52
+ };
53
+
54
+ export const getCursorProjectsDir = (userDir = resolveCursorUserDir()): string => {
55
+ const configured = process.env.SPIRACHA_CURSOR_PROJECTS_DIR?.trim();
56
+ if (configured) {
57
+ return configured;
58
+ }
59
+
60
+ const inferredHomeDir = inferHomeDirFromCursorUserDir(userDir);
61
+ return inferredHomeDir ? path.join(inferredHomeDir, '.cursor', 'projects') : path.join(userDir, 'projects');
62
+ };
63
+
64
+ export const COMPOSER_DATA_KEY = 'composer.composerData';
65
+ export const COMPOSER_HEADERS_KEY = 'composer.composerHeaders';
66
+
67
+ export type CursorWorkspaceKind = 'folder' | 'workspace' | 'unknown';
68
+
69
+ export type CursorWorkspaceBucket = {
70
+ bucketId: string;
71
+ workspaceJsonPath: string;
72
+ dbPath: string;
73
+ mtimeMs: number;
74
+ dbSizeBytes: number;
75
+ kind: CursorWorkspaceKind;
76
+ uri: string;
77
+ label: string;
78
+ folders: string[];
79
+ composerCount: number;
80
+ globalHeaderCount: number;
81
+ // Distinct composer ids attributed to this bucket (bucket composer.composerData plus the global
82
+ // headers that point at it). Used to compute an accurate, de-duplicated workspace thread count.
83
+ threadComposerIds: string[];
84
+ };
85
+
86
+ export type CursorWorkspaceGroup = {
87
+ key: string;
88
+ label: string;
89
+ kind: CursorWorkspaceKind;
90
+ uri: string;
91
+ folders: string[];
92
+ buckets: CursorWorkspaceBucket[];
93
+ threadCount: number;
94
+ lastActiveMs: number;
95
+ needsRecovery: boolean;
96
+ };
97
+
98
+ export type CursorThreadSummary = {
99
+ composerId: string;
100
+ name: string;
101
+ bucketId: string | null;
102
+ workspaceLabel: string;
103
+ workspaceKey: string;
104
+ createdAtMs: number | null;
105
+ lastUpdatedAtMs: number | null;
106
+ bubbleCount: number;
107
+ bubbleBytes: number;
108
+ transcriptDirs: string[];
109
+ mode: string | null;
110
+ };
111
+
112
+ export type CursorBubbleKind = 'user' | 'assistant' | 'unknown';
113
+
114
+ export type CursorToolCall = {
115
+ name: string;
116
+ callId: string | null;
117
+ status: string | null;
118
+ argumentsText: string | null;
119
+ resultText: string | null;
120
+ };
121
+
122
+ export type CursorBubble = {
123
+ bubbleId: string;
124
+ kind: CursorBubbleKind;
125
+ text: string;
126
+ thinking: string | null;
127
+ toolCall: CursorToolCall | null;
128
+ createdAtMs: number | null;
129
+ };
130
+
131
+ export type CursorThreadHead = {
132
+ composerId: string;
133
+ name: string | null;
134
+ createdAtMs: number | null;
135
+ lastUpdatedAtMs: number | null;
136
+ mode: string | null;
137
+ orderedBubbleIds: string[];
138
+ totalBubbleHeaders: number;
139
+ };
140
+
141
+ export type CursorThreadTranscript = {
142
+ head: CursorThreadHead;
143
+ bubbles: CursorBubble[];
144
+ renderableBubbleCount: number;
145
+ omittedBubbleCount: number;
146
+ };
147
+
148
+ export type CursorExportOptions = {
149
+ includeMetadata: boolean;
150
+ includeCommentary: boolean;
151
+ includeTools: boolean;
152
+ outputFormat: ExportFormat;
153
+ };
154
+
155
+ export type CursorCliOptions = CursorExportOptions & {
156
+ userDir: string;
157
+ workspaceQuery: string | null;
158
+ threadIds: string[];
159
+ outputDir: string | null;
160
+ };
161
+
162
+ export type CursorExportedFile = {
163
+ composerId: string;
164
+ outputPath: string;
165
+ };
166
+
167
+ export type CursorExportRunResult = {
168
+ outputDir: string;
169
+ exportedCount: number;
170
+ files: CursorExportedFile[];
171
+ missingThreadIds: string[];
172
+ };
173
+
174
+ export type CursorRecoverResult = {
175
+ workspaceKey: string;
176
+ activeBucketId: string;
177
+ mergedThreadCount: number;
178
+ relinkedHeaderCount: number;
179
+ addedHeaderCount: number;
180
+ threads: Array<{ composerId: string; name: string; bubbleCount: number }>;
181
+ };
182
+
183
+ export type CursorPruneResult = {
184
+ bubblesDeleted: number;
185
+ composerDataDeleted: number;
186
+ headersRemoved: number;
187
+ workspaceBucketsUpdated: number;
188
+ transcriptDirsRemoved: number;
189
+ composerIds: string[];
190
+ };
@@ -0,0 +1,266 @@
1
+ import path from 'node:path';
2
+ import {
3
+ findCursorWorkspaceGroups,
4
+ listCursorThreadsForGroup,
5
+ listCursorWorkspaceGroups,
6
+ readCursorThreadHead,
7
+ readCursorThreadTranscript,
8
+ } from './cursor-db';
9
+ import {
10
+ type CursorCliOptions,
11
+ type CursorExportedFile,
12
+ type CursorExportRunResult,
13
+ type CursorThreadSummary,
14
+ type CursorWorkspaceGroup,
15
+ getCursorGlobalDbPath,
16
+ resolveCursorUserDir,
17
+ } from './cursor-exporter-types';
18
+ import { renderCursorTranscript } from './cursor-transcript';
19
+ import { CliUsageError, type ExportFormat, expandHome, writeExportFile } from './shared';
20
+
21
+ export const DEFAULT_CURSOR_OUTPUT_DIR = path.join(process.cwd(), 'exports', 'cursor');
22
+
23
+ const resolveSingleGroup = (groups: CursorWorkspaceGroup[], query: string): CursorWorkspaceGroup => {
24
+ const matched = findCursorWorkspaceGroups(groups, query);
25
+ if (matched.length === 0) {
26
+ throw new Error(`No Cursor workspace matched query: ${query}`);
27
+ }
28
+
29
+ if (matched.length > 1) {
30
+ const keys = matched.map((group) => ` - ${group.key}`).join('\n');
31
+ throw new Error(
32
+ `Query "${query}" matched multiple Cursor workspaces. Refine it to a folder path or .code-workspace file:\n${keys}`,
33
+ );
34
+ }
35
+
36
+ return matched[0]!;
37
+ };
38
+
39
+ const collectThreadsToExport = async (
40
+ options: CursorCliOptions,
41
+ ): Promise<{ threads: CursorThreadSummary[]; missingThreadIds: string[] }> => {
42
+ if (options.threadIds.length > 0) {
43
+ return collectThreadsById(options);
44
+ }
45
+
46
+ if (!options.workspaceQuery) {
47
+ throw new CliUsageError('Provide a workspace (--workspace) or one or more --thread ids to export.');
48
+ }
49
+
50
+ const groups = await listCursorWorkspaceGroups(options.userDir);
51
+ const group = resolveSingleGroup(groups, options.workspaceQuery);
52
+ const threads = await listCursorThreadsForGroup(group, options.userDir);
53
+ return { missingThreadIds: [], threads: threads.filter((thread) => thread.bubbleCount > 0) };
54
+ };
55
+
56
+ // Export by id reads the global head record directly so we avoid scanning every workspace bucket.
57
+ const collectThreadsById = async (
58
+ options: CursorCliOptions,
59
+ ): Promise<{ threads: CursorThreadSummary[]; missingThreadIds: string[] }> => {
60
+ const globalDbPath = getCursorGlobalDbPath(options.userDir);
61
+ const threads: CursorThreadSummary[] = [];
62
+ const missingThreadIds: string[] = [];
63
+
64
+ for (const threadId of options.threadIds) {
65
+ const head = readCursorThreadHead(globalDbPath, threadId);
66
+ if (!head) {
67
+ missingThreadIds.push(threadId);
68
+ continue;
69
+ }
70
+
71
+ threads.push({
72
+ bubbleBytes: 0,
73
+ bubbleCount: head.orderedBubbleIds.length,
74
+ bucketId: null,
75
+ composerId: head.composerId,
76
+ createdAtMs: head.createdAtMs,
77
+ lastUpdatedAtMs: head.lastUpdatedAtMs,
78
+ mode: head.mode,
79
+ name: head.name ?? '(untitled)',
80
+ transcriptDirs: [],
81
+ workspaceKey: '',
82
+ workspaceLabel: '',
83
+ });
84
+ }
85
+
86
+ return { missingThreadIds, threads };
87
+ };
88
+
89
+ export const runCursorExport = async (options: CursorCliOptions): Promise<CursorExportRunResult> => {
90
+ const globalDbPath = getCursorGlobalDbPath(options.userDir);
91
+ const outputDir = options.outputDir ?? DEFAULT_CURSOR_OUTPUT_DIR;
92
+ const { threads, missingThreadIds } = await collectThreadsToExport(options);
93
+ const files: CursorExportedFile[] = [];
94
+
95
+ for (const thread of threads) {
96
+ const exported = await exportSingleThread(thread, globalDbPath, outputDir, options);
97
+ if (exported) {
98
+ files.push(exported);
99
+ } else {
100
+ missingThreadIds.push(thread.composerId);
101
+ }
102
+ }
103
+
104
+ return {
105
+ exportedCount: files.length,
106
+ files,
107
+ missingThreadIds,
108
+ outputDir,
109
+ };
110
+ };
111
+
112
+ const exportSingleThread = async (
113
+ thread: CursorThreadSummary,
114
+ globalDbPath: string,
115
+ outputDir: string,
116
+ options: CursorCliOptions,
117
+ ): Promise<CursorExportedFile | null> => {
118
+ const transcript = readCursorThreadTranscript(globalDbPath, thread.composerId);
119
+ if (!transcript) {
120
+ return null;
121
+ }
122
+
123
+ const content = renderCursorTranscript(transcript, options);
124
+ if (!content) {
125
+ return null;
126
+ }
127
+
128
+ const outputPath = path.join(outputDir, `${thread.composerId}.${options.outputFormat}`);
129
+ await writeExportFile(outputPath, content);
130
+
131
+ return { composerId: thread.composerId, outputPath };
132
+ };
133
+
134
+ const parseExportFormat = (value: string): ExportFormat => {
135
+ if (value === 'md' || value === 'txt') {
136
+ return value;
137
+ }
138
+
139
+ throw new CliUsageError(`Unsupported output format: ${value}`);
140
+ };
141
+
142
+ const requireValue = (value: string | undefined, flag: string): string => {
143
+ if (!value || (value.startsWith('-') && value !== '-')) {
144
+ throw new CliUsageError(`Missing value for ${flag}`);
145
+ }
146
+
147
+ return value;
148
+ };
149
+
150
+ export const parseCursorCliArgs = (argv: string[]): CursorCliOptions => {
151
+ const state: CursorCliOptions = {
152
+ includeCommentary: false,
153
+ includeMetadata: true,
154
+ includeTools: false,
155
+ outputDir: null,
156
+ outputFormat: 'md',
157
+ threadIds: [],
158
+ userDir: resolveCursorUserDir(),
159
+ workspaceQuery: null,
160
+ };
161
+
162
+ for (let index = 0; index < argv.length; index += 1) {
163
+ index = applyCursorCliArg(argv, index, state);
164
+ }
165
+
166
+ return state;
167
+ };
168
+
169
+ const applyCursorCliArg = (argv: string[], index: number, state: CursorCliOptions): number => {
170
+ const arg = argv[index] as string;
171
+ const flagIndex = applyCursorFlag(arg, state);
172
+ if (flagIndex) {
173
+ return index;
174
+ }
175
+
176
+ return applyCursorValueArg(argv, index, arg, state);
177
+ };
178
+
179
+ const applyCursorFlag = (arg: string, state: CursorCliOptions): boolean => {
180
+ if (arg === '--tools') {
181
+ state.includeTools = true;
182
+ return true;
183
+ }
184
+
185
+ if (arg === '--commentary' || arg === '--reasoning') {
186
+ state.includeCommentary = true;
187
+ return true;
188
+ }
189
+
190
+ if (arg === '--no-metadata') {
191
+ state.includeMetadata = false;
192
+ return true;
193
+ }
194
+
195
+ return false;
196
+ };
197
+
198
+ const applyCursorValueArg = (argv: string[], index: number, arg: string, state: CursorCliOptions): number => {
199
+ if (arg === '--workspace' || arg === '-w') {
200
+ state.workspaceQuery = expandHome(requireValue(argv[index + 1], arg));
201
+ return index + 1;
202
+ }
203
+
204
+ if (arg === '--thread' || arg === '-t') {
205
+ state.threadIds.push(requireValue(argv[index + 1], arg));
206
+ return index + 1;
207
+ }
208
+
209
+ if (arg === '--output' || arg === '-o') {
210
+ state.outputDir = expandHome(requireValue(argv[index + 1], arg));
211
+ return index + 1;
212
+ }
213
+
214
+ if (arg === '--user-dir') {
215
+ state.userDir = expandHome(requireValue(argv[index + 1], arg));
216
+ return index + 1;
217
+ }
218
+
219
+ if (arg.startsWith('--output-format=')) {
220
+ state.outputFormat = parseExportFormat(arg.slice('--output-format='.length));
221
+ return index;
222
+ }
223
+
224
+ if (arg === '--output-format') {
225
+ state.outputFormat = parseExportFormat(requireValue(argv[index + 1], '--output-format'));
226
+ return index + 1;
227
+ }
228
+
229
+ if (!arg.startsWith('-') && !state.workspaceQuery && state.threadIds.length === 0) {
230
+ state.workspaceQuery = expandHome(arg);
231
+ return index;
232
+ }
233
+
234
+ throw new CliUsageError(`Unknown argument: ${arg}`);
235
+ };
236
+
237
+ export const getCursorHelpText = (): string => {
238
+ return [
239
+ 'Export, recover, and prune local Cursor Agent/Composer threads.',
240
+ '',
241
+ 'Usage:',
242
+ ' spiracha cursor list [query]',
243
+ ' spiracha cursor export --workspace NAME [options]',
244
+ ' spiracha cursor export --thread COMPOSER_ID [--thread ...] [options]',
245
+ ' spiracha cursor recover <workspace> [--apply]',
246
+ ' spiracha cursor prune --workspace NAME [--thread ID ...] [--apply]',
247
+ '',
248
+ 'Export options:',
249
+ ' --workspace, -w Workspace folder name, path, or .code-workspace file',
250
+ ' --thread, -t Composer/thread id (repeatable)',
251
+ ' --output, -o Output directory (default: ./exports/cursor)',
252
+ ' --output-format md or txt (default: md)',
253
+ ' --tools Include tool calls and their results',
254
+ ' --commentary Include assistant reasoning blocks',
255
+ ' --no-metadata Omit the metadata header block',
256
+ ' --user-dir Override the Cursor User data directory',
257
+ '',
258
+ 'Recover/prune:',
259
+ ' --apply Perform writes (default is a dry run). Quit Cursor first.',
260
+ '',
261
+ 'Examples:',
262
+ ' spiracha cursor list',
263
+ ' spiracha cursor export --workspace gun-twizzle --tools --commentary',
264
+ ' spiracha cursor recover gun-twizzle --apply',
265
+ ].join('\n');
266
+ };