whatisgoingon 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.
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
+ <title>Cursor Chat Browser</title>
11
+ <script type="module" crossorigin src="/assets/index-CGRhpi7l.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-BC9n2roG.css">
13
+ </head>
14
+ <body>
15
+ <div id="root"></div>
16
+ </body>
17
+ </html>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
package/dist/server.js ADDED
@@ -0,0 +1,364 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import Database from 'better-sqlite3';
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+ import os from 'os';
7
+ const app = express();
8
+ const PORT = process.env.PORT || 3456;
9
+ app.use(cors());
10
+ app.use(express.json());
11
+ // Serve static files from built frontend
12
+ const staticPath = path.join(import.meta.dirname, 'public');
13
+ if (fs.existsSync(staticPath)) {
14
+ app.use(express.static(staticPath));
15
+ }
16
+ function getCursorStoragePath() {
17
+ const home = os.homedir();
18
+ if (process.platform === 'win32') {
19
+ return path.join(home, 'AppData', 'Roaming', 'Cursor', 'User', 'workspaceStorage');
20
+ }
21
+ else if (process.platform === 'darwin') {
22
+ return path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'workspaceStorage');
23
+ }
24
+ else {
25
+ return path.join(home, '.config', 'Cursor', 'User', 'workspaceStorage');
26
+ }
27
+ }
28
+ function getGlobalStoragePath() {
29
+ const home = os.homedir();
30
+ if (process.platform === 'win32') {
31
+ return path.join(home, 'AppData', 'Roaming', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
32
+ }
33
+ else if (process.platform === 'darwin') {
34
+ return path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
35
+ }
36
+ else {
37
+ return path.join(home, '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
38
+ }
39
+ }
40
+ function getWorkspaceFolder(workspaceDir) {
41
+ try {
42
+ // First try to read from workspace.json file
43
+ const workspaceJsonPath = path.join(workspaceDir, 'workspace.json');
44
+ if (fs.existsSync(workspaceJsonPath)) {
45
+ const content = fs.readFileSync(workspaceJsonPath, 'utf-8');
46
+ const data = JSON.parse(content);
47
+ if (data?.folder) {
48
+ // Convert file:// URI to path
49
+ return data.folder.replace('file://', '').replace(/%20/g, ' ');
50
+ }
51
+ }
52
+ // Fallback: try to read from database
53
+ const dbPath = path.join(workspaceDir, 'state.vscdb');
54
+ if (fs.existsSync(dbPath)) {
55
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
56
+ const result = db.prepare("SELECT value FROM ItemTable WHERE key LIKE '%folder%' LIMIT 1").get();
57
+ db.close();
58
+ if (result?.value) {
59
+ try {
60
+ const data = JSON.parse(result.value);
61
+ if (typeof data === 'string')
62
+ return data;
63
+ if (data?.uri)
64
+ return data.uri.replace('file://', '');
65
+ }
66
+ catch {
67
+ return result.value.substring(0, 100);
68
+ }
69
+ }
70
+ }
71
+ }
72
+ catch {
73
+ // Error reading files
74
+ }
75
+ return null;
76
+ }
77
+ function extractPrompts(dbPath) {
78
+ try {
79
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
80
+ const result = db.prepare("SELECT value FROM ItemTable WHERE key = 'aiService.prompts'").get();
81
+ db.close();
82
+ if (result?.value) {
83
+ const data = JSON.parse(result.value);
84
+ if (Array.isArray(data)) {
85
+ return data;
86
+ }
87
+ }
88
+ }
89
+ catch {
90
+ // Database locked or parsing error
91
+ }
92
+ return [];
93
+ }
94
+ function extractComposers(dbPath) {
95
+ try {
96
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
97
+ const result = db.prepare("SELECT value FROM ItemTable WHERE key = 'composer.composerData'").get();
98
+ db.close();
99
+ if (result?.value) {
100
+ const data = JSON.parse(result.value);
101
+ if (data?.allComposers && Array.isArray(data.allComposers)) {
102
+ return data.allComposers
103
+ .filter((c) => {
104
+ // Filter out composers without valid timestamps
105
+ return c.composerId &&
106
+ typeof c.lastUpdatedAt === 'number' &&
107
+ c.lastUpdatedAt > 0;
108
+ })
109
+ .map((c) => ({
110
+ composerId: c.composerId,
111
+ name: c.name || `Chat ${new Date(c.createdAt || c.lastUpdatedAt).toLocaleString()}`,
112
+ createdAt: c.createdAt || c.lastUpdatedAt,
113
+ lastUpdatedAt: c.lastUpdatedAt,
114
+ mode: c.unifiedMode || 'chat',
115
+ }));
116
+ }
117
+ }
118
+ }
119
+ catch {
120
+ // Database locked or parsing error
121
+ }
122
+ return [];
123
+ }
124
+ function listWorkspaces(daysBack = 30) {
125
+ const storagePath = getCursorStoragePath();
126
+ const workspaces = [];
127
+ const cutoffDate = Date.now() - (daysBack * 24 * 60 * 60 * 1000);
128
+ if (!fs.existsSync(storagePath)) {
129
+ return workspaces;
130
+ }
131
+ const dirs = fs.readdirSync(storagePath);
132
+ for (const dir of dirs) {
133
+ const workspaceDir = path.join(storagePath, dir);
134
+ const dbPath = path.join(workspaceDir, 'state.vscdb');
135
+ if (!fs.existsSync(dbPath))
136
+ continue;
137
+ const stats = fs.statSync(dbPath);
138
+ const modifiedTimestamp = stats.mtimeMs;
139
+ if (modifiedTimestamp < cutoffDate)
140
+ continue;
141
+ const folder = getWorkspaceFolder(workspaceDir);
142
+ const prompts = extractPrompts(dbPath);
143
+ const composers = extractComposers(dbPath);
144
+ if (prompts.length > 0 || composers.length > 0) {
145
+ workspaces.push({
146
+ id: dir,
147
+ path: dbPath,
148
+ folder,
149
+ modified: new Date(modifiedTimestamp).toISOString(),
150
+ modifiedTimestamp,
151
+ promptCount: prompts.length,
152
+ prompts,
153
+ composers,
154
+ });
155
+ }
156
+ }
157
+ // Sort by modification date (most recent first)
158
+ workspaces.sort((a, b) => b.modifiedTimestamp - a.modifiedTimestamp);
159
+ return workspaces;
160
+ }
161
+ // API Routes
162
+ app.get('/api/workspaces', (req, res) => {
163
+ const days = parseInt(req.query.days) || 30;
164
+ const workspaces = listWorkspaces(days);
165
+ // Return without full prompts for listing
166
+ const summary = workspaces.map(ws => ({
167
+ id: ws.id,
168
+ folder: ws.folder,
169
+ modified: ws.modified,
170
+ modifiedTimestamp: ws.modifiedTimestamp,
171
+ promptCount: ws.promptCount,
172
+ composerCount: ws.composers.length,
173
+ }));
174
+ res.json(summary);
175
+ });
176
+ app.get('/api/workspaces/:id', (req, res) => {
177
+ const filterDate = req.query.date; // YYYY-MM-DD format
178
+ const workspaces = listWorkspaces(365);
179
+ const workspace = workspaces.find(ws => ws.id === req.params.id);
180
+ if (!workspace) {
181
+ return res.status(404).json({ error: 'Workspace not found' });
182
+ }
183
+ // If a date filter is provided, filter composers by that date
184
+ let filteredComposers = workspace.composers;
185
+ if (filterDate) {
186
+ const startOfDay = new Date(filterDate + 'T00:00:00').getTime();
187
+ const endOfDay = new Date(filterDate + 'T23:59:59.999').getTime();
188
+ filteredComposers = workspace.composers.filter(c => {
189
+ // Include if lastUpdatedAt falls on the selected date
190
+ return c.lastUpdatedAt >= startOfDay && c.lastUpdatedAt <= endOfDay;
191
+ });
192
+ }
193
+ res.json({
194
+ ...workspace,
195
+ composers: filteredComposers,
196
+ // Also include composers grouped by date for the UI
197
+ composersByDate: groupComposersByDate(workspace.composers),
198
+ });
199
+ });
200
+ function groupComposersByDate(composers) {
201
+ const grouped = {};
202
+ for (const composer of composers) {
203
+ const date = new Date(composer.lastUpdatedAt).toLocaleDateString('en-CA');
204
+ if (!grouped[date]) {
205
+ grouped[date] = [];
206
+ }
207
+ grouped[date].push(composer);
208
+ }
209
+ return grouped;
210
+ }
211
+ app.get('/api/dates', (req, res) => {
212
+ const workspaces = listWorkspaces(30);
213
+ // Group by date based on composer activity, not just workspace modification
214
+ const dates = new Map();
215
+ for (const ws of workspaces) {
216
+ // Add based on workspace modification date (legacy prompts)
217
+ const wsDate = new Date(ws.modifiedTimestamp).toLocaleDateString('en-CA');
218
+ if (!dates.has(wsDate)) {
219
+ dates.set(wsDate, { promptCount: 0, workspaces: new Set(), composerCount: 0 });
220
+ }
221
+ // Group composers by their actual lastUpdatedAt date
222
+ for (const composer of ws.composers) {
223
+ const composerDate = new Date(composer.lastUpdatedAt).toLocaleDateString('en-CA');
224
+ if (!dates.has(composerDate)) {
225
+ dates.set(composerDate, { promptCount: 0, workspaces: new Set(), composerCount: 0 });
226
+ }
227
+ const entry = dates.get(composerDate);
228
+ entry.workspaces.add(ws.id);
229
+ entry.composerCount++;
230
+ }
231
+ // Also add prompts to workspace mod date
232
+ const wsEntry = dates.get(wsDate);
233
+ wsEntry.promptCount += ws.promptCount;
234
+ wsEntry.workspaces.add(ws.id);
235
+ }
236
+ const result = Array.from(dates.entries())
237
+ .map(([date, data]) => ({
238
+ date,
239
+ promptCount: data.promptCount,
240
+ composerCount: data.composerCount,
241
+ workspaceIds: Array.from(data.workspaces),
242
+ }))
243
+ .sort((a, b) => b.date.localeCompare(a.date)); // Sort by date descending
244
+ res.json(result);
245
+ });
246
+ app.get('/api/composers', (req, res) => {
247
+ const filterDate = req.query.date; // YYYY-MM-DD format
248
+ const workspaces = listWorkspaces(30);
249
+ const allComposers = [];
250
+ for (const ws of workspaces) {
251
+ for (const composer of ws.composers) {
252
+ const composerDate = new Date(composer.lastUpdatedAt).toLocaleDateString('en-CA');
253
+ // If date filter provided, only include composers from that date
254
+ if (filterDate && composerDate !== filterDate) {
255
+ continue;
256
+ }
257
+ allComposers.push({
258
+ ...composer,
259
+ workspaceId: ws.id,
260
+ workspaceFolder: ws.folder,
261
+ });
262
+ }
263
+ }
264
+ // Sort by lastUpdatedAt descending
265
+ allComposers.sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt);
266
+ res.json(allComposers);
267
+ });
268
+ // Get bubbles (chat messages) for a specific composer
269
+ app.get('/api/composers/:composerId/bubbles', (req, res) => {
270
+ const { composerId } = req.params;
271
+ try {
272
+ const globalDbPath = getGlobalStoragePath();
273
+ if (!fs.existsSync(globalDbPath)) {
274
+ return res.json([]);
275
+ }
276
+ const db = new Database(globalDbPath, { readonly: true, fileMustExist: true });
277
+ const rows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE ?").all(`bubbleId:${composerId}:%`);
278
+ db.close();
279
+ const bubbles = [];
280
+ for (const row of rows) {
281
+ try {
282
+ const data = JSON.parse(row.value);
283
+ const bubbleId = row.key.split(':')[2];
284
+ // Only include bubbles with text content
285
+ if (data.text) {
286
+ bubbles.push({
287
+ bubbleId,
288
+ type: data.type || 0, // 1 = user, 2 = assistant
289
+ text: data.text,
290
+ createdAt: data.createdAt,
291
+ });
292
+ }
293
+ }
294
+ catch {
295
+ // Skip invalid JSON
296
+ }
297
+ }
298
+ // Sort by createdAt if available
299
+ bubbles.sort((a, b) => {
300
+ if (a.createdAt && b.createdAt) {
301
+ return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
302
+ }
303
+ return 0;
304
+ });
305
+ res.json(bubbles);
306
+ }
307
+ catch (error) {
308
+ console.error('Error fetching bubbles:', error);
309
+ res.json([]);
310
+ }
311
+ });
312
+ app.get('/api/search', (req, res) => {
313
+ const query = (req.query.q || '').toLowerCase();
314
+ if (!query) {
315
+ return res.json([]);
316
+ }
317
+ const workspaces = listWorkspaces(30);
318
+ const results = [];
319
+ for (const ws of workspaces) {
320
+ for (const prompt of ws.prompts) {
321
+ if (prompt.text?.toLowerCase().includes(query)) {
322
+ results.push({
323
+ workspace: ws.id,
324
+ folder: ws.folder,
325
+ prompt
326
+ });
327
+ }
328
+ }
329
+ }
330
+ res.json(results.slice(0, 100)); // Limit to 100 results
331
+ });
332
+ // Serve frontend - handle SPA routing
333
+ app.get('/', (req, res) => {
334
+ const indexPath = path.join(import.meta.dirname, 'public', 'index.html');
335
+ if (fs.existsSync(indexPath)) {
336
+ res.sendFile(indexPath);
337
+ }
338
+ else {
339
+ res.status(404).send('Frontend not built. Run "pnpm build" first.');
340
+ }
341
+ });
342
+ // Fallback for SPA client-side routing
343
+ app.use((req, res, next) => {
344
+ if (req.method === 'GET' && !req.path.startsWith('/api')) {
345
+ const indexPath = path.join(import.meta.dirname, 'public', 'index.html');
346
+ if (fs.existsSync(indexPath)) {
347
+ return res.sendFile(indexPath);
348
+ }
349
+ }
350
+ next();
351
+ });
352
+ const server = app.listen(PORT, () => {
353
+ console.log(`
354
+ ╔═══════════════════════════════════════════════════════════╗
355
+ ║ ║
356
+ ║ 🔍 What Is Going On? - Cursor Chat History Browser ║
357
+ ║ ║
358
+ ║ Server running at: ║
359
+ ║ → http://localhost:${PORT} ║
360
+ ║ ║
361
+ ╚═══════════════════════════════════════════════════════════╝
362
+ `);
363
+ });
364
+ export { app, server, PORT };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "whatisgoingon",
3
+ "version": "1.0.0",
4
+ "description": "Browse and explore your Cursor AI chat history with a beautiful web UI",
5
+ "type": "module",
6
+ "bin": {
7
+ "whatisgoingon": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "concurrently \"tsx watch src/server.ts\" \"cd frontend && pnpm dev\"",
11
+ "build": "pnpm build:server && pnpm build:frontend",
12
+ "build:frontend": "cd frontend && pnpm build && rm -rf ../dist/public && cp -r dist ../dist/public",
13
+ "build:server": "tsc",
14
+ "start": "node bin/cli.js",
15
+ "prepublishOnly": "pnpm build",
16
+ "postinstall": "cd frontend && pnpm install || true"
17
+ },
18
+ "keywords": [
19
+ "cursor",
20
+ "chat",
21
+ "history",
22
+ "browser",
23
+ "ai",
24
+ "dev-tools"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "files": [
29
+ "dist",
30
+ "bin"
31
+ ],
32
+ "dependencies": {
33
+ "better-sqlite3": "^12.5.0",
34
+ "cors": "^2.8.5",
35
+ "express": "^5.2.1",
36
+ "open": "^10.2.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/better-sqlite3": "^7.6.13",
40
+ "@types/cors": "^2.8.19",
41
+ "@types/express": "^5.0.6",
42
+ "@types/node": "^25.0.3",
43
+ "concurrently": "^9.1.2",
44
+ "tsx": "^4.21.0",
45
+ "typescript": "^5.9.3"
46
+ }
47
+ }