memhouse 0.0.1 → 0.3.1
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/LICENSE +22 -0
- package/README.md +72 -3
- package/bin/memhouse.js +559 -0
- package/cache.js +850 -0
- package/ch-ingest.js +329 -0
- package/ch-schema.js +152 -0
- package/clickhouse.js +87 -0
- package/editors/adapter-errors.js +52 -0
- package/editors/antigravity.js +1048 -0
- package/editors/base.js +321 -0
- package/editors/claude.js +386 -0
- package/editors/codebuff.js +338 -0
- package/editors/codex.js +537 -0
- package/editors/commandcode.js +166 -0
- package/editors/copilot-jetbrains.js +207 -0
- package/editors/copilot.js +260 -0
- package/editors/cursor-agent.js +202 -0
- package/editors/cursor.js +498 -0
- package/editors/gemini.js +231 -0
- package/editors/goose.js +386 -0
- package/editors/gsd.js +366 -0
- package/editors/index.js +274 -0
- package/editors/kiro.js +315 -0
- package/editors/opencode.js +432 -0
- package/editors/vscode.js +413 -0
- package/editors/windsurf.js +634 -0
- package/editors/zed.js +229 -0
- package/index.js +424 -2
- package/mcp-server.js +294 -0
- package/mem-house/DESIGN.md +119 -0
- package/mem-house/delivery/AGENT-INSTALL.md +87 -0
- package/mem-house/delivery/PROMPT.md +42 -0
- package/mem-house/delivery/install.sh +181 -0
- package/mem-house/delivery/kernel-install.md +83 -0
- package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
- package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
- package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
- package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
- package/mem-house/rls.sql +17 -0
- package/mem-house/schema.sql +145 -0
- package/mem-house/server/queries.js +878 -0
- package/mem-house/server/server.js +228 -0
- package/mem-house/shipper/ship.js +461 -0
- package/misc/syntax-check.js +38 -0
- package/package.json +59 -7
- package/pricing.js +88 -0
- package/pricing.json +992 -0
- package/public/assets/index-CCPh7a2Z.js +75 -0
- package/public/assets/index-DV6ONi_F.css +2 -0
- package/public/index.html +16 -0
- package/relay-client.js +306 -0
- package/relay-server.js +577 -0
- package/server.js +860 -0
- package/share-image.js +367 -0
- package/ui/eslint.config.js +29 -0
- package/ui/index.html +15 -0
- package/ui/package-lock.json +4229 -0
- package/ui/package.json +38 -0
- package/ui/src/App.jsx +364 -0
- package/ui/src/components/ActivityHeatmap.jsx +206 -0
- package/ui/src/components/AiAuditCard.jsx +254 -0
- package/ui/src/components/AnimatedLoader.jsx +14 -0
- package/ui/src/components/AnimatedLogo.jsx +96 -0
- package/ui/src/components/ChatSidebar.jsx +226 -0
- package/ui/src/components/DateRangePicker.jsx +104 -0
- package/ui/src/components/EditorDot.jsx +11 -0
- package/ui/src/components/EditorIcon.jsx +117 -0
- package/ui/src/components/KpiCard.jsx +9 -0
- package/ui/src/components/LiveFeed.jsx +128 -0
- package/ui/src/components/LoginScreen.jsx +81 -0
- package/ui/src/components/MessageRenderer.jsx +148 -0
- package/ui/src/components/PageHeader.jsx +11 -0
- package/ui/src/components/SectionTitle.jsx +3 -0
- package/ui/src/components/ShareModal.jsx +305 -0
- package/ui/src/components/TokenTimeline.jsx +258 -0
- package/ui/src/hooks/useLive.jsx +15 -0
- package/ui/src/index.css +83 -0
- package/ui/src/lib/api.js +343 -0
- package/ui/src/lib/constants.js +88 -0
- package/ui/src/lib/theme.jsx +23 -0
- package/ui/src/main.jsx +16 -0
- package/ui/src/pages/Artifacts.jsx +575 -0
- package/ui/src/pages/Compare.jsx +232 -0
- package/ui/src/pages/CostAnalysis.jsx +354 -0
- package/ui/src/pages/Dashboard.jsx +529 -0
- package/ui/src/pages/DeepAnalysis.jsx +495 -0
- package/ui/src/pages/GSD.jsx +726 -0
- package/ui/src/pages/MCPs.jsx +744 -0
- package/ui/src/pages/ProjectDetail.jsx +325 -0
- package/ui/src/pages/Projects.jsx +249 -0
- package/ui/src/pages/RelayDashboard.jsx +590 -0
- package/ui/src/pages/RelayUserDetail.jsx +466 -0
- package/ui/src/pages/Sessions.jsx +535 -0
- package/ui/src/pages/Settings.jsx +241 -0
- package/ui/src/pages/SqlViewer.jsx +370 -0
- package/ui/src/pages/Subscriptions.jsx +411 -0
- package/ui/vite.config.js +18 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
const Database = require('better-sqlite3');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { getAppDataPath } = require('./base');
|
|
6
|
+
const adapterErrors = require('./adapter-errors');
|
|
7
|
+
|
|
8
|
+
const HOME = os.homedir();
|
|
9
|
+
const CURSOR_CHATS_DIR = path.join(HOME, '.cursor', 'chats');
|
|
10
|
+
const CURSOR_USER_DIR = path.join(getAppDataPath('Cursor'), 'User');
|
|
11
|
+
const WORKSPACE_STORAGE_DIR = path.join(CURSOR_USER_DIR, 'workspaceStorage');
|
|
12
|
+
const GLOBAL_STORAGE_DB = path.join(CURSOR_USER_DIR, 'globalStorage', 'state.vscdb');
|
|
13
|
+
|
|
14
|
+
// ============================================================
|
|
15
|
+
// Source 1: ~/.cursor/chats/<hash>/<chatId>/store.db (agent KV)
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
function getAgentStoreChats() {
|
|
19
|
+
const results = [];
|
|
20
|
+
if (!fs.existsSync(CURSOR_CHATS_DIR)) return results;
|
|
21
|
+
|
|
22
|
+
for (const workspace of fs.readdirSync(CURSOR_CHATS_DIR)) {
|
|
23
|
+
const wsDir = path.join(CURSOR_CHATS_DIR, workspace);
|
|
24
|
+
if (!fs.statSync(wsDir).isDirectory()) continue;
|
|
25
|
+
for (const chat of fs.readdirSync(wsDir)) {
|
|
26
|
+
const dbPath = path.join(wsDir, chat, 'store.db');
|
|
27
|
+
if (fs.existsSync(dbPath)) {
|
|
28
|
+
results.push({ workspace, chatId: chat, dbPath });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hexToString(hex) {
|
|
36
|
+
let str = '';
|
|
37
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
38
|
+
str += String.fromCharCode(parseInt(hex.substring(i, i + 2), 16));
|
|
39
|
+
}
|
|
40
|
+
return str;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readStoreMeta(db) {
|
|
44
|
+
const row = db.prepare('SELECT value FROM meta WHERE key = ?').get('0');
|
|
45
|
+
if (!row) return null;
|
|
46
|
+
const hex = typeof row.value === 'string' ? row.value : Buffer.from(row.value).toString('hex');
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(hexToString(hex));
|
|
49
|
+
} catch {
|
|
50
|
+
try { return JSON.parse(row.value); } catch { return null; }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseTreeBlob(data) {
|
|
55
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
56
|
+
const messageRefs = [];
|
|
57
|
+
const childRefs = [];
|
|
58
|
+
let offset = 0;
|
|
59
|
+
while (offset < buf.length) {
|
|
60
|
+
if (offset + 34 > buf.length) break;
|
|
61
|
+
const tag = buf[offset];
|
|
62
|
+
const len = buf[offset + 1];
|
|
63
|
+
if (len !== 0x20) break;
|
|
64
|
+
const hash = buf.slice(offset + 2, offset + 2 + 32).toString('hex');
|
|
65
|
+
if (tag === 0x0a) messageRefs.push(hash);
|
|
66
|
+
else if (tag === 0x12) childRefs.push(hash);
|
|
67
|
+
else break;
|
|
68
|
+
offset += 2 + 32;
|
|
69
|
+
}
|
|
70
|
+
return { messageRefs, childRefs };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeStoreMessage(json) {
|
|
74
|
+
const msg = { role: json.role, content: '', _toolCalls: [] };
|
|
75
|
+
// Content may be string or array of parts
|
|
76
|
+
if (typeof json.content === 'string') {
|
|
77
|
+
msg.content = json.content;
|
|
78
|
+
} else if (Array.isArray(json.content)) {
|
|
79
|
+
msg.content = json.content.map(p => typeof p === 'string' ? p : (p.text || '')).join('\n');
|
|
80
|
+
}
|
|
81
|
+
// Tool calls (OpenAI format)
|
|
82
|
+
if (json.tool_calls && Array.isArray(json.tool_calls)) {
|
|
83
|
+
for (const tc of json.tool_calls) {
|
|
84
|
+
const name = tc.function?.name || tc.name || 'unknown';
|
|
85
|
+
let args = {};
|
|
86
|
+
try {
|
|
87
|
+
args = typeof tc.function?.arguments === 'string' ? JSON.parse(tc.function.arguments) : (tc.function?.arguments || {});
|
|
88
|
+
} catch {}
|
|
89
|
+
const argKeys = typeof args === 'object' ? Object.keys(args).join(', ') : '';
|
|
90
|
+
msg.content += `\n[tool-call: ${name}(${argKeys})]`;
|
|
91
|
+
msg._toolCalls.push({ name, args });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (json.model) msg._model = json.model;
|
|
95
|
+
return msg;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function collectStoreMessages(db, rootBlobId) {
|
|
99
|
+
const allMessages = [];
|
|
100
|
+
const visited = new Set();
|
|
101
|
+
function walk(blobId) {
|
|
102
|
+
if (visited.has(blobId)) return;
|
|
103
|
+
visited.add(blobId);
|
|
104
|
+
const row = db.prepare('SELECT data FROM blobs WHERE id = ?').get(blobId);
|
|
105
|
+
if (!row) return;
|
|
106
|
+
const data = row.data;
|
|
107
|
+
try {
|
|
108
|
+
const json = typeof data === 'string' ? JSON.parse(data) : JSON.parse(data.toString('utf-8'));
|
|
109
|
+
if (json && json.role) { allMessages.push(normalizeStoreMessage(json)); return; }
|
|
110
|
+
} catch { /* tree blob */ }
|
|
111
|
+
const { messageRefs, childRefs } = parseTreeBlob(data);
|
|
112
|
+
for (const ref of messageRefs) walk(ref);
|
|
113
|
+
for (const ref of childRefs) walk(ref);
|
|
114
|
+
}
|
|
115
|
+
walk(rootBlobId);
|
|
116
|
+
return allMessages;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================
|
|
120
|
+
// Source 2: workspaceStorage + globalStorage (composer bubbles)
|
|
121
|
+
// ============================================================
|
|
122
|
+
|
|
123
|
+
function getWorkspaceMap() {
|
|
124
|
+
const map = [];
|
|
125
|
+
if (!fs.existsSync(WORKSPACE_STORAGE_DIR)) return map;
|
|
126
|
+
for (const hash of fs.readdirSync(WORKSPACE_STORAGE_DIR)) {
|
|
127
|
+
const dir = path.join(WORKSPACE_STORAGE_DIR, hash);
|
|
128
|
+
const wsJson = path.join(dir, 'workspace.json');
|
|
129
|
+
const stateDb = path.join(dir, 'state.vscdb');
|
|
130
|
+
if (!fs.existsSync(wsJson) || !fs.existsSync(stateDb)) continue;
|
|
131
|
+
try {
|
|
132
|
+
const ws = JSON.parse(fs.readFileSync(wsJson, 'utf-8'));
|
|
133
|
+
const folder = (ws.folder || '').replace('file://', '');
|
|
134
|
+
map.push({ hash, folder, stateDb });
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// The descriptor is only where the folder NAME comes from — state.vscdb holds
|
|
137
|
+
// the sessions and was already confirmed present. Dropping the whole workspace
|
|
138
|
+
// over an unreadable workspace.json would take every composer in it out of
|
|
139
|
+
// discovery, where nothing downstream can notice them missing. Keep the
|
|
140
|
+
// workspace with no folder, and record why the name is absent.
|
|
141
|
+
adapterErrors.record('cursor', e, wsJson);
|
|
142
|
+
map.push({ hash, folder: '', stateDb });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return map;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getComposerHeaders(stateDbPath) {
|
|
149
|
+
try {
|
|
150
|
+
const db = new Database(stateDbPath, { readonly: true });
|
|
151
|
+
const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'composer.composerData'").get();
|
|
152
|
+
db.close();
|
|
153
|
+
if (!row) return [];
|
|
154
|
+
const data = JSON.parse(row.value);
|
|
155
|
+
return (data.allComposers || []).map((c) => ({
|
|
156
|
+
composerId: c.composerId,
|
|
157
|
+
name: c.name || null,
|
|
158
|
+
createdAt: c.createdAt || null,
|
|
159
|
+
lastUpdatedAt: c.lastUpdatedAt || null,
|
|
160
|
+
mode: c.unifiedMode || c.forceMode || 'unknown',
|
|
161
|
+
isAgentic: c.unifiedMode === 'agent',
|
|
162
|
+
}));
|
|
163
|
+
} catch (e) {
|
|
164
|
+
// One workspace's state.vscdb being locked, corrupt, or on an incompatible
|
|
165
|
+
// ItemTable removes every composer in that workspace. The global database still
|
|
166
|
+
// opens, so without this Cursor looks healthy while a whole workspace of
|
|
167
|
+
// sessions quietly disappears.
|
|
168
|
+
adapterErrors.record('cursor', e, stateDbPath);
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getModelPreference(globalDb) {
|
|
174
|
+
try {
|
|
175
|
+
const row = globalDb.prepare("SELECT value FROM ItemTable WHERE key = 'cursor/lastSingleModelPreference'").get();
|
|
176
|
+
if (!row) return null;
|
|
177
|
+
const pref = JSON.parse(row.value);
|
|
178
|
+
return pref.composer || pref.agent || null;
|
|
179
|
+
} catch { return null; }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getComposerBubbles(globalDb, composerId) {
|
|
183
|
+
const prefix = `bubbleId:${composerId}:`;
|
|
184
|
+
const rows = globalDb.prepare(
|
|
185
|
+
"SELECT key, value FROM cursorDiskKV WHERE key LIKE ? ORDER BY key"
|
|
186
|
+
).all(prefix + '%');
|
|
187
|
+
|
|
188
|
+
const bubbles = [];
|
|
189
|
+
for (const row of rows) {
|
|
190
|
+
try {
|
|
191
|
+
const obj = JSON.parse(row.value);
|
|
192
|
+
bubbles.push(obj);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
// Historically treated as "binary blob, skip", but a row that will not decode
|
|
195
|
+
// is a message missing from the transcript either way. Unreported, the
|
|
196
|
+
// shortened result overwrites the stored one on a re-ship.
|
|
197
|
+
adapterErrors.record('cursor', e, `bubble ${row.key || '(unknown)'}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return bubbles;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function bubblesToMessages(bubbles) {
|
|
204
|
+
const messages = [];
|
|
205
|
+
for (const b of bubbles) {
|
|
206
|
+
if (!b) continue;
|
|
207
|
+
const type = b.type; // 1=user, 2=assistant
|
|
208
|
+
if (type === 1) {
|
|
209
|
+
const text = b.text || '';
|
|
210
|
+
if (text) {
|
|
211
|
+
messages.push({ role: 'user', content: text });
|
|
212
|
+
}
|
|
213
|
+
} else if (type === 2) {
|
|
214
|
+
const textParts = [];
|
|
215
|
+
// Thinking block
|
|
216
|
+
if (b.thinking && b.thinking.text) {
|
|
217
|
+
textParts.push(`[thinking] ${b.thinking.text}`);
|
|
218
|
+
}
|
|
219
|
+
// Main text
|
|
220
|
+
if (b.text) {
|
|
221
|
+
textParts.push(b.text);
|
|
222
|
+
}
|
|
223
|
+
// Tool calls — format as [tool-call: name(argKeys)] for analytics
|
|
224
|
+
const _toolCalls = [];
|
|
225
|
+
const tfd = b.toolFormerData;
|
|
226
|
+
if (tfd && tfd.name) {
|
|
227
|
+
let args = {};
|
|
228
|
+
try {
|
|
229
|
+
args = typeof tfd.rawArgs === 'string' && tfd.rawArgs !== '' ? JSON.parse(tfd.rawArgs) : (tfd.rawArgs || {});
|
|
230
|
+
} catch {}
|
|
231
|
+
// Fallback: use params field when rawArgs is empty
|
|
232
|
+
if (!args || (typeof args === 'object' && Object.keys(args).length === 0)) {
|
|
233
|
+
try {
|
|
234
|
+
const p = typeof tfd.params === 'string' ? JSON.parse(tfd.params) : tfd.params;
|
|
235
|
+
if (p && typeof p === 'object' && Object.keys(p).length > 0) args = p;
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
const argKeys = typeof args === 'object' ? Object.keys(args).join(', ') : '';
|
|
239
|
+
textParts.push(`[tool-call: ${tfd.name}(${argKeys})]`);
|
|
240
|
+
_toolCalls.push({ name: tfd.name, args });
|
|
241
|
+
if (tfd.result) {
|
|
242
|
+
const preview = typeof tfd.result === 'string' ? tfd.result.substring(0, 300) : JSON.stringify(tfd.result).substring(0, 300);
|
|
243
|
+
textParts.push(`[tool-result: ${tfd.name}] ${preview}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Code blocks
|
|
247
|
+
if (b.codeBlocks && b.codeBlocks.length > 0) {
|
|
248
|
+
for (const cb of b.codeBlocks) {
|
|
249
|
+
const filePath = cb.uri ? cb.uri.path : '';
|
|
250
|
+
if (filePath) textParts.push(`[file: ${filePath}]`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (textParts.length > 0) {
|
|
254
|
+
messages.push({
|
|
255
|
+
role: 'assistant',
|
|
256
|
+
content: textParts.join('\n'),
|
|
257
|
+
_model: b.modelId || b.model || null,
|
|
258
|
+
_inputTokens: b.tokenCount?.inputTokens,
|
|
259
|
+
_outputTokens: b.tokenCount?.outputTokens,
|
|
260
|
+
_toolCalls,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return messages;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ============================================================
|
|
269
|
+
// Adapter interface
|
|
270
|
+
// ============================================================
|
|
271
|
+
|
|
272
|
+
const name = 'cursor';
|
|
273
|
+
|
|
274
|
+
function getChats() {
|
|
275
|
+
const chats = [];
|
|
276
|
+
|
|
277
|
+
// Source 1: ~/.cursor/chats store.db
|
|
278
|
+
for (const { workspace, chatId, dbPath } of getAgentStoreChats()) {
|
|
279
|
+
try {
|
|
280
|
+
const db = new Database(dbPath, { readonly: true });
|
|
281
|
+
const meta = readStoreMeta(db);
|
|
282
|
+
db.close();
|
|
283
|
+
if (meta) {
|
|
284
|
+
// The agent store carries no per-chat updated time and no message count, so
|
|
285
|
+
// the shipper had nothing that changes as the conversation grows: it
|
|
286
|
+
// compared a fixed createdAt against itself and an absent count against 0,
|
|
287
|
+
// and skipped every intact session forever. store.db's mtime is the real
|
|
288
|
+
// change signal — it moves when blobs are appended. If it cannot be read,
|
|
289
|
+
// fall back to declaring the count unknown so the skip stands down instead
|
|
290
|
+
// of silently freezing the transcript.
|
|
291
|
+
let storeMtime = null;
|
|
292
|
+
try { storeMtime = fs.statSync(dbPath).mtimeMs; } catch { /* fall back below */ }
|
|
293
|
+
chats.push({
|
|
294
|
+
source: 'cursor',
|
|
295
|
+
composerId: chatId,
|
|
296
|
+
name: meta.name || null,
|
|
297
|
+
createdAt: meta.createdAt || null,
|
|
298
|
+
lastUpdatedAt: storeMtime,
|
|
299
|
+
mode: meta.mode || null,
|
|
300
|
+
folder: null,
|
|
301
|
+
_countUnknown: storeMtime === null,
|
|
302
|
+
_dbPath: dbPath,
|
|
303
|
+
_rootBlobId: meta.latestRootBlobId,
|
|
304
|
+
_lastUsedModel: meta.lastUsedModel || null,
|
|
305
|
+
_type: 'agent-store',
|
|
306
|
+
});
|
|
307
|
+
} else {
|
|
308
|
+
// The database opened but its meta row would not decode, so this session is
|
|
309
|
+
// dropped here at discovery — before doctor's probes or the shipper ever see
|
|
310
|
+
// it. Nothing downstream can notice a chat that was never listed, which is
|
|
311
|
+
// why it has to be recorded at the point of omission.
|
|
312
|
+
adapterErrors.record('cursor', new Error('agent-store meta row could not be decoded'), dbPath);
|
|
313
|
+
}
|
|
314
|
+
} catch (e) { adapterErrors.record('cursor', e, dbPath); }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Source 2: workspaceStorage composers
|
|
318
|
+
let globalDb = null;
|
|
319
|
+
try { globalDb = new Database(GLOBAL_STORAGE_DB, { readonly: true }); }
|
|
320
|
+
catch (e) {
|
|
321
|
+
// Absent is normal — Cursor may simply not be installed. Unreadable is not.
|
|
322
|
+
if (fs.existsSync(GLOBAL_STORAGE_DB)) adapterErrors.record('cursor', e, GLOBAL_STORAGE_DB);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const modelPref = globalDb ? getModelPreference(globalDb) : null;
|
|
326
|
+
|
|
327
|
+
for (const { hash, folder, stateDb } of getWorkspaceMap()) {
|
|
328
|
+
const headers = getComposerHeaders(stateDb);
|
|
329
|
+
for (const h of headers) {
|
|
330
|
+
let bubbleCount = 0;
|
|
331
|
+
let countUnknown = false;
|
|
332
|
+
if (globalDb) {
|
|
333
|
+
try {
|
|
334
|
+
const countRow = globalDb.prepare(
|
|
335
|
+
"SELECT count(*) as cnt FROM cursorDiskKV WHERE key LIKE ?"
|
|
336
|
+
).get(`bubbleId:${h.composerId}:%`);
|
|
337
|
+
bubbleCount = countRow ? countRow.cnt : 0;
|
|
338
|
+
} catch (e) {
|
|
339
|
+
// A failed count is NOT a count of zero, and the difference decides
|
|
340
|
+
// whether this session ever updates again. bubbleCount feeds the shipper's
|
|
341
|
+
// skip predicate (prev.bc >= chat.bubbleCount); left at 0 that is
|
|
342
|
+
// trivially true, so a grown transcript stays stale on every incremental
|
|
343
|
+
// pass. Recording alone does not save it either — the skip is evaluated
|
|
344
|
+
// before any per-chat error guard, so the chat is dropped before the guard
|
|
345
|
+
// can see it. Flag it instead, and let the predicate refuse to skip.
|
|
346
|
+
adapterErrors.record('cursor', e, `bubble count for ${h.composerId}`);
|
|
347
|
+
countUnknown = true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
chats.push({
|
|
351
|
+
source: 'cursor',
|
|
352
|
+
composerId: h.composerId,
|
|
353
|
+
name: h.name || null,
|
|
354
|
+
createdAt: h.createdAt || null,
|
|
355
|
+
lastUpdatedAt: h.lastUpdatedAt || null,
|
|
356
|
+
mode: h.mode,
|
|
357
|
+
folder,
|
|
358
|
+
bubbleCount,
|
|
359
|
+
_countUnknown: countUnknown,
|
|
360
|
+
_type: 'workspace',
|
|
361
|
+
_modelPref: modelPref,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (globalDb) globalDb.close();
|
|
367
|
+
return chats;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function getMessages(chat) {
|
|
371
|
+
if (chat._type === 'agent-store') {
|
|
372
|
+
const db = new Database(chat._dbPath, { readonly: true });
|
|
373
|
+
const msgs = collectStoreMessages(db, chat._rootBlobId);
|
|
374
|
+
db.close();
|
|
375
|
+
// Use lastUsedModel as fallback for assistant messages without model info
|
|
376
|
+
if (chat._lastUsedModel) {
|
|
377
|
+
for (const m of msgs) {
|
|
378
|
+
if (m.role === 'assistant' && !m._model) m._model = chat._lastUsedModel;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return msgs;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let globalDb;
|
|
385
|
+
try { globalDb = new Database(GLOBAL_STORAGE_DB, { readonly: true }); }
|
|
386
|
+
catch (e) {
|
|
387
|
+
// Returning [] unreported would let a re-ship overwrite this session's stored
|
|
388
|
+
// transcript with nothing.
|
|
389
|
+
adapterErrors.record('cursor', e, GLOBAL_STORAGE_DB);
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
const bubbles = getComposerBubbles(globalDb, chat.composerId);
|
|
393
|
+
globalDb.close();
|
|
394
|
+
const msgs = bubblesToMessages(bubbles);
|
|
395
|
+
// Use model preference as fallback for messages without model info
|
|
396
|
+
if (chat._modelPref) {
|
|
397
|
+
for (const m of msgs) {
|
|
398
|
+
if (m.role === 'assistant' && !m._model) m._model = chat._modelPref;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return msgs;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ============================================================
|
|
405
|
+
// Usage / quota data from Cursor REST API
|
|
406
|
+
// ============================================================
|
|
407
|
+
|
|
408
|
+
function getCursorAccessToken() {
|
|
409
|
+
try {
|
|
410
|
+
const db = new Database(GLOBAL_STORAGE_DB, { readonly: true });
|
|
411
|
+
const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'").get();
|
|
412
|
+
db.close();
|
|
413
|
+
return row ? row.value : null;
|
|
414
|
+
} catch { return null; }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function cursorApiFetch(endpoint, token) {
|
|
418
|
+
return new Promise((resolve) => {
|
|
419
|
+
const https = require('https');
|
|
420
|
+
const url = `https://api2.cursor.sh/auth/${endpoint}`;
|
|
421
|
+
const req = https.get(url, {
|
|
422
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
423
|
+
timeout: 10000,
|
|
424
|
+
}, (res) => {
|
|
425
|
+
let data = '';
|
|
426
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
427
|
+
res.on('end', () => {
|
|
428
|
+
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
req.on('error', () => resolve(null));
|
|
432
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function getUsage() {
|
|
437
|
+
const { isSubscriptionAccessAllowed } = require('./base');
|
|
438
|
+
if (!isSubscriptionAccessAllowed()) return null;
|
|
439
|
+
const token = getCursorAccessToken();
|
|
440
|
+
if (!token) return null;
|
|
441
|
+
|
|
442
|
+
const [profile, usage] = await Promise.all([
|
|
443
|
+
cursorApiFetch('full_stripe_profile', token),
|
|
444
|
+
cursorApiFetch('usage', token),
|
|
445
|
+
]);
|
|
446
|
+
|
|
447
|
+
if (!profile && !usage) return null;
|
|
448
|
+
|
|
449
|
+
const result = {
|
|
450
|
+
source: 'cursor',
|
|
451
|
+
plan: {
|
|
452
|
+
name: profile?.individualMembershipType || profile?.membershipType || null,
|
|
453
|
+
status: profile?.subscriptionStatus || null,
|
|
454
|
+
isTeamMember: profile?.isTeamMember || false,
|
|
455
|
+
isYearlyPlan: profile?.isYearlyPlan || false,
|
|
456
|
+
},
|
|
457
|
+
usage: {},
|
|
458
|
+
startOfMonth: usage?.startOfMonth || null,
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Parse per-model usage from the usage endpoint
|
|
462
|
+
if (usage) {
|
|
463
|
+
for (const [model, data] of Object.entries(usage)) {
|
|
464
|
+
if (model === 'startOfMonth') continue;
|
|
465
|
+
result.usage[model] = {
|
|
466
|
+
numRequests: data.numRequests || 0,
|
|
467
|
+
numRequestsTotal: data.numRequestsTotal || 0,
|
|
468
|
+
numTokens: data.numTokens || 0,
|
|
469
|
+
maxRequestUsage: data.maxRequestUsage || null,
|
|
470
|
+
maxTokenUsage: data.maxTokenUsage || null,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const labels = { 'cursor': 'Cursor' };
|
|
479
|
+
|
|
480
|
+
function getArtifacts(folder) {
|
|
481
|
+
const { scanArtifacts } = require('./base');
|
|
482
|
+
return scanArtifacts(folder, {
|
|
483
|
+
editor: 'cursor',
|
|
484
|
+
label: 'Cursor',
|
|
485
|
+
files: ['.cursorrules', 'AGENTS.md'],
|
|
486
|
+
dirs: ['.cursor/rules', '.cursor/plans'],
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function getMCPServers() {
|
|
491
|
+
const { parseMcpConfigFile } = require('./base');
|
|
492
|
+
const globalConfig = path.join(HOME, '.cursor', 'mcp.json');
|
|
493
|
+
return [
|
|
494
|
+
...parseMcpConfigFile(globalConfig, { editor: 'cursor', label: 'Cursor', scope: 'global' }),
|
|
495
|
+
];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts, getMCPServers };
|