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.
- package/README.md +75 -0
- package/bin/cli.js +42 -0
- package/dist/public/assets/index-BC9n2roG.css +1 -0
- package/dist/public/assets/index-CGRhpi7l.js +44 -0
- package/dist/public/index.html +17 -0
- package/dist/public/vite.svg +1 -0
- package/dist/server.js +364 -0
- package/package.json +47 -0
|
@@ -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
|
+
}
|