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.
Files changed (97) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +72 -3
  3. package/bin/memhouse.js +559 -0
  4. package/cache.js +850 -0
  5. package/ch-ingest.js +329 -0
  6. package/ch-schema.js +152 -0
  7. package/clickhouse.js +87 -0
  8. package/editors/adapter-errors.js +52 -0
  9. package/editors/antigravity.js +1048 -0
  10. package/editors/base.js +321 -0
  11. package/editors/claude.js +386 -0
  12. package/editors/codebuff.js +338 -0
  13. package/editors/codex.js +537 -0
  14. package/editors/commandcode.js +166 -0
  15. package/editors/copilot-jetbrains.js +207 -0
  16. package/editors/copilot.js +260 -0
  17. package/editors/cursor-agent.js +202 -0
  18. package/editors/cursor.js +498 -0
  19. package/editors/gemini.js +231 -0
  20. package/editors/goose.js +386 -0
  21. package/editors/gsd.js +366 -0
  22. package/editors/index.js +274 -0
  23. package/editors/kiro.js +315 -0
  24. package/editors/opencode.js +432 -0
  25. package/editors/vscode.js +413 -0
  26. package/editors/windsurf.js +634 -0
  27. package/editors/zed.js +229 -0
  28. package/index.js +424 -2
  29. package/mcp-server.js +294 -0
  30. package/mem-house/DESIGN.md +119 -0
  31. package/mem-house/delivery/AGENT-INSTALL.md +87 -0
  32. package/mem-house/delivery/PROMPT.md +42 -0
  33. package/mem-house/delivery/install.sh +181 -0
  34. package/mem-house/delivery/kernel-install.md +83 -0
  35. package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
  36. package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
  37. package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
  38. package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
  39. package/mem-house/rls.sql +17 -0
  40. package/mem-house/schema.sql +145 -0
  41. package/mem-house/server/queries.js +878 -0
  42. package/mem-house/server/server.js +228 -0
  43. package/mem-house/shipper/ship.js +461 -0
  44. package/misc/syntax-check.js +38 -0
  45. package/package.json +59 -7
  46. package/pricing.js +88 -0
  47. package/pricing.json +992 -0
  48. package/public/assets/index-CCPh7a2Z.js +75 -0
  49. package/public/assets/index-DV6ONi_F.css +2 -0
  50. package/public/index.html +16 -0
  51. package/relay-client.js +306 -0
  52. package/relay-server.js +577 -0
  53. package/server.js +860 -0
  54. package/share-image.js +367 -0
  55. package/ui/eslint.config.js +29 -0
  56. package/ui/index.html +15 -0
  57. package/ui/package-lock.json +4229 -0
  58. package/ui/package.json +38 -0
  59. package/ui/src/App.jsx +364 -0
  60. package/ui/src/components/ActivityHeatmap.jsx +206 -0
  61. package/ui/src/components/AiAuditCard.jsx +254 -0
  62. package/ui/src/components/AnimatedLoader.jsx +14 -0
  63. package/ui/src/components/AnimatedLogo.jsx +96 -0
  64. package/ui/src/components/ChatSidebar.jsx +226 -0
  65. package/ui/src/components/DateRangePicker.jsx +104 -0
  66. package/ui/src/components/EditorDot.jsx +11 -0
  67. package/ui/src/components/EditorIcon.jsx +117 -0
  68. package/ui/src/components/KpiCard.jsx +9 -0
  69. package/ui/src/components/LiveFeed.jsx +128 -0
  70. package/ui/src/components/LoginScreen.jsx +81 -0
  71. package/ui/src/components/MessageRenderer.jsx +148 -0
  72. package/ui/src/components/PageHeader.jsx +11 -0
  73. package/ui/src/components/SectionTitle.jsx +3 -0
  74. package/ui/src/components/ShareModal.jsx +305 -0
  75. package/ui/src/components/TokenTimeline.jsx +258 -0
  76. package/ui/src/hooks/useLive.jsx +15 -0
  77. package/ui/src/index.css +83 -0
  78. package/ui/src/lib/api.js +343 -0
  79. package/ui/src/lib/constants.js +88 -0
  80. package/ui/src/lib/theme.jsx +23 -0
  81. package/ui/src/main.jsx +16 -0
  82. package/ui/src/pages/Artifacts.jsx +575 -0
  83. package/ui/src/pages/Compare.jsx +232 -0
  84. package/ui/src/pages/CostAnalysis.jsx +354 -0
  85. package/ui/src/pages/Dashboard.jsx +529 -0
  86. package/ui/src/pages/DeepAnalysis.jsx +495 -0
  87. package/ui/src/pages/GSD.jsx +726 -0
  88. package/ui/src/pages/MCPs.jsx +744 -0
  89. package/ui/src/pages/ProjectDetail.jsx +325 -0
  90. package/ui/src/pages/Projects.jsx +249 -0
  91. package/ui/src/pages/RelayDashboard.jsx +590 -0
  92. package/ui/src/pages/RelayUserDetail.jsx +466 -0
  93. package/ui/src/pages/Sessions.jsx +535 -0
  94. package/ui/src/pages/Settings.jsx +241 -0
  95. package/ui/src/pages/SqlViewer.jsx +370 -0
  96. package/ui/src/pages/Subscriptions.jsx +411 -0
  97. 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 };