memhouse 0.0.1 → 0.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 (96) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +59 -3
  3. package/bin/memhouse.js +487 -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/antigravity.js +1019 -0
  9. package/editors/base.js +321 -0
  10. package/editors/claude.js +386 -0
  11. package/editors/codebuff.js +338 -0
  12. package/editors/codex.js +537 -0
  13. package/editors/commandcode.js +166 -0
  14. package/editors/copilot-jetbrains.js +207 -0
  15. package/editors/copilot.js +260 -0
  16. package/editors/cursor-agent.js +202 -0
  17. package/editors/cursor.js +438 -0
  18. package/editors/gemini.js +231 -0
  19. package/editors/goose.js +358 -0
  20. package/editors/gsd.js +366 -0
  21. package/editors/index.js +179 -0
  22. package/editors/kiro.js +315 -0
  23. package/editors/opencode.js +374 -0
  24. package/editors/vscode.js +413 -0
  25. package/editors/windsurf.js +634 -0
  26. package/editors/zed.js +213 -0
  27. package/index.js +424 -2
  28. package/mcp-server.js +294 -0
  29. package/mem-house/DESIGN.md +119 -0
  30. package/mem-house/delivery/AGENT-INSTALL.md +75 -0
  31. package/mem-house/delivery/PROMPT.md +42 -0
  32. package/mem-house/delivery/install.sh +181 -0
  33. package/mem-house/delivery/kernel-install.md +76 -0
  34. package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
  35. package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
  36. package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
  37. package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
  38. package/mem-house/rls.sql +17 -0
  39. package/mem-house/schema.sql +145 -0
  40. package/mem-house/server/queries.js +878 -0
  41. package/mem-house/server/server.js +228 -0
  42. package/mem-house/shipper/ship.js +402 -0
  43. package/misc/syntax-check.js +38 -0
  44. package/package.json +56 -7
  45. package/pricing.js +88 -0
  46. package/pricing.json +992 -0
  47. package/public/assets/index-BsgjOSTv.js +75 -0
  48. package/public/assets/index-DV6ONi_F.css +2 -0
  49. package/public/index.html +16 -0
  50. package/relay-client.js +306 -0
  51. package/relay-server.js +577 -0
  52. package/server.js +860 -0
  53. package/share-image.js +367 -0
  54. package/ui/eslint.config.js +29 -0
  55. package/ui/index.html +15 -0
  56. package/ui/package-lock.json +4229 -0
  57. package/ui/package.json +38 -0
  58. package/ui/src/App.jsx +364 -0
  59. package/ui/src/components/ActivityHeatmap.jsx +206 -0
  60. package/ui/src/components/AiAuditCard.jsx +254 -0
  61. package/ui/src/components/AnimatedLoader.jsx +14 -0
  62. package/ui/src/components/AnimatedLogo.jsx +96 -0
  63. package/ui/src/components/ChatSidebar.jsx +226 -0
  64. package/ui/src/components/DateRangePicker.jsx +104 -0
  65. package/ui/src/components/EditorDot.jsx +11 -0
  66. package/ui/src/components/EditorIcon.jsx +117 -0
  67. package/ui/src/components/KpiCard.jsx +9 -0
  68. package/ui/src/components/LiveFeed.jsx +128 -0
  69. package/ui/src/components/LoginScreen.jsx +81 -0
  70. package/ui/src/components/MessageRenderer.jsx +148 -0
  71. package/ui/src/components/PageHeader.jsx +11 -0
  72. package/ui/src/components/SectionTitle.jsx +3 -0
  73. package/ui/src/components/ShareModal.jsx +305 -0
  74. package/ui/src/components/TokenTimeline.jsx +258 -0
  75. package/ui/src/hooks/useLive.jsx +15 -0
  76. package/ui/src/index.css +83 -0
  77. package/ui/src/lib/api.js +343 -0
  78. package/ui/src/lib/constants.js +88 -0
  79. package/ui/src/lib/theme.jsx +23 -0
  80. package/ui/src/main.jsx +16 -0
  81. package/ui/src/pages/Artifacts.jsx +575 -0
  82. package/ui/src/pages/Compare.jsx +232 -0
  83. package/ui/src/pages/CostAnalysis.jsx +354 -0
  84. package/ui/src/pages/Dashboard.jsx +529 -0
  85. package/ui/src/pages/DeepAnalysis.jsx +495 -0
  86. package/ui/src/pages/GSD.jsx +726 -0
  87. package/ui/src/pages/MCPs.jsx +744 -0
  88. package/ui/src/pages/ProjectDetail.jsx +325 -0
  89. package/ui/src/pages/Projects.jsx +249 -0
  90. package/ui/src/pages/RelayDashboard.jsx +590 -0
  91. package/ui/src/pages/RelayUserDetail.jsx +466 -0
  92. package/ui/src/pages/Sessions.jsx +535 -0
  93. package/ui/src/pages/Settings.jsx +241 -0
  94. package/ui/src/pages/SqlViewer.jsx +344 -0
  95. package/ui/src/pages/Subscriptions.jsx +411 -0
  96. package/ui/vite.config.js +18 -0
@@ -0,0 +1,207 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+
5
+ const JB_DIR = path.join(os.homedir(), '.copilot', 'jb');
6
+
7
+ // ============================================================
8
+ // Adapter interface
9
+ // ============================================================
10
+
11
+ const name = 'copilot-jetbrains';
12
+
13
+ /**
14
+ * Parse partition-*.jsonl files from a JetBrains Copilot session directory.
15
+ * Each line is one JSON event; partition-<N>.jsonl files are read in order.
16
+ */
17
+ function parseEvents(sessionDir) {
18
+ let entries;
19
+ try { entries = fs.readdirSync(sessionDir); } catch { return []; }
20
+ const partitions = entries
21
+ .filter(f => /^partition-\d+\.jsonl$/.test(f))
22
+ .sort((a, b) =>
23
+ parseInt(a.match(/^partition-(\d+)\.jsonl$/)[1], 10) -
24
+ parseInt(b.match(/^partition-(\d+)\.jsonl$/)[1], 10));
25
+
26
+ const events = [];
27
+ for (const p of partitions) {
28
+ try {
29
+ const raw = fs.readFileSync(path.join(sessionDir, p), 'utf-8');
30
+ for (const line of raw.split('\n')) {
31
+ if (!line.trim()) continue;
32
+ try { events.push(JSON.parse(line)); } catch { /* skip malformed */ }
33
+ }
34
+ } catch { /* skip unreadable partition */ }
35
+ }
36
+ return events;
37
+ }
38
+
39
+ /**
40
+ * Parse a JetBrains event `timestamp` field to epoch ms.
41
+ * Handles ISO 8601 and the US-locale `MM/dd/yyyy HH:mm:ss` format emitted by
42
+ * the JetBrains plugin.
43
+ */
44
+ function parseTimestamp(ts) {
45
+ if (!ts) return null;
46
+ const str = String(ts);
47
+ if (str.includes('T')) {
48
+ const t = Date.parse(str);
49
+ if (!Number.isNaN(t)) return t;
50
+ }
51
+ const m = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{2}):(\d{2})$/);
52
+ if (m) return Date.UTC(+m[3], +m[1] - 1, +m[2], +m[4], +m[5], +m[6]);
53
+ return null;
54
+ }
55
+
56
+ function getChats() {
57
+ const chats = [];
58
+ if (!fs.existsSync(JB_DIR)) return chats;
59
+
60
+ let sessionDirs;
61
+ try { sessionDirs = fs.readdirSync(JB_DIR); } catch { return chats; }
62
+
63
+ for (const dirName of sessionDirs) {
64
+ const sessionDir = path.join(JB_DIR, dirName);
65
+ try { if (!fs.statSync(sessionDir).isDirectory()) continue; } catch { continue; }
66
+
67
+ const events = parseEvents(sessionDir);
68
+ const userMessages = events.filter(e => e.type === 'user.message');
69
+ const assistantMessages = events.filter(e => e.type === 'assistant.message');
70
+ const firstUser = userMessages[0];
71
+
72
+ // Count meaningful messages (user + assistant)
73
+ const bubbleCount = userMessages.length + assistantMessages.length;
74
+ if (bubbleCount === 0) continue;
75
+
76
+ let createdAt = null, lastUpdatedAt = null;
77
+ for (const e of events) {
78
+ const t = parseTimestamp(e.timestamp);
79
+ if (t == null) continue;
80
+ if (createdAt == null || t < createdAt) createdAt = t;
81
+ if (lastUpdatedAt == null || t > lastUpdatedAt) lastUpdatedAt = t;
82
+ }
83
+
84
+ chats.push({
85
+ source: 'copilot-jetbrains',
86
+ composerId: dirName,
87
+ name: cleanPrompt(firstUser?.data?.content),
88
+ createdAt,
89
+ lastUpdatedAt,
90
+ mode: 'copilot',
91
+ folder: null,
92
+ encrypted: false,
93
+ bubbleCount,
94
+ _sessionDir: sessionDir,
95
+ });
96
+ }
97
+
98
+ return chats;
99
+ }
100
+
101
+ function cleanPrompt(text) {
102
+ if (!text) return null;
103
+ return text.replace(/\s+/g, ' ').trim().substring(0, 120) || null;
104
+ }
105
+
106
+ function getMessages(chat) {
107
+ const sessionDir = chat._sessionDir;
108
+ if (!sessionDir || !fs.existsSync(sessionDir)) return [];
109
+
110
+ const events = parseEvents(sessionDir);
111
+ const result = [];
112
+
113
+ // Tool calls in the JetBrains format are separate `tool.execution_start` /
114
+ // `tool.execution_complete` events (NOT embedded in assistant.message like
115
+ // the CLI format). Buffer pending tool calls and attach them to the next
116
+ // assistant message iteration, per the spec's grouping algorithm.
117
+ const pendingToolCalls = [];
118
+
119
+ for (const event of events) {
120
+ if (event.type === 'user.message') {
121
+ const content = extractContent(event.data?.content);
122
+ if (content) result.push({ role: 'user', content });
123
+
124
+ } else if (event.type === 'tool.execution_start') {
125
+ const data = event.data || {};
126
+ const tcName = data.toolName || 'unknown';
127
+ const args = data.arguments || {};
128
+ const parsedArgs = typeof args === 'string' ? safeParse(args) : args;
129
+ pendingToolCalls.push({ name: tcName, args: parsedArgs });
130
+
131
+ } else if (event.type === 'assistant.message') {
132
+ const data = event.data || {};
133
+ const parts = [];
134
+ const toolCalls = pendingToolCalls.length > 0 ? pendingToolCalls.splice(0) : [];
135
+
136
+ // Main text content — handle legacy double-encoded JSON array shape.
137
+ const text = extractContent(data.content != null ? data.content : data.text);
138
+ if (text) parts.push(text);
139
+
140
+ // Render buffered tool calls inline for compactness.
141
+ for (const tc of toolCalls) {
142
+ const argKeys = typeof tc.args === 'object' ? Object.keys(tc.args).join(', ') : '';
143
+ parts.push(`[tool-call: ${tc.name}(${argKeys})]`);
144
+ }
145
+
146
+ if (parts.length > 0) {
147
+ result.push({
148
+ role: 'assistant',
149
+ content: parts.join('\n'),
150
+ _toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ // Flush any tool calls left dangling (turn ended without a final assistant.message).
157
+ if (pendingToolCalls.length > 0) {
158
+ const parts = pendingToolCalls.map(tc => {
159
+ const argKeys = typeof tc.args === 'object' ? Object.keys(tc.args).join(', ') : '';
160
+ return `[tool-call: ${tc.name}(${argKeys})]`;
161
+ });
162
+ result.push({
163
+ role: 'assistant',
164
+ content: parts.join('\n'),
165
+ _toolCalls: pendingToolCalls.splice(0),
166
+ });
167
+ }
168
+
169
+ return result;
170
+ }
171
+
172
+ /**
173
+ * Extract assistant response text from a `data.content` value.
174
+ * Handles plain strings and the legacy double-encoded JSON array shape
175
+ * (older partitions store content as a stringified array of
176
+ * {role, content, tool_calls} objects — pick the last role:assistant
177
+ * entry with non-empty content).
178
+ */
179
+ function extractContent(contentVal) {
180
+ if (contentVal == null) return '';
181
+ if (typeof contentVal === 'string') {
182
+ const trimmed = contentVal.trim();
183
+ if (trimmed.startsWith('[')) {
184
+ try {
185
+ const arr = JSON.parse(contentVal);
186
+ if (Array.isArray(arr)) {
187
+ for (let i = arr.length - 1; i >= 0; i--) {
188
+ const el = arr[i];
189
+ if (el && el.role === 'assistant' && el.content) {
190
+ return typeof el.content === 'string' ? el.content : extractContent(el.content);
191
+ }
192
+ }
193
+ }
194
+ } catch { /* fall through to raw string */ }
195
+ }
196
+ return contentVal;
197
+ }
198
+ return '';
199
+ }
200
+
201
+ function safeParse(str) {
202
+ try { return JSON.parse(str); } catch { return {}; }
203
+ }
204
+
205
+ const labels = { 'copilot-jetbrains': 'GitHub Copilot - JetBrains' };
206
+
207
+ module.exports = { name, labels, getChats, getMessages };
@@ -0,0 +1,260 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+
5
+ const COPILOT_DIR = path.join(os.homedir(), '.copilot');
6
+ const SESSION_STATE_DIR = path.join(COPILOT_DIR, 'session-state');
7
+
8
+ // ============================================================
9
+ // Adapter interface
10
+ // ============================================================
11
+
12
+ const name = 'copilot-cli';
13
+
14
+ /**
15
+ * Parse workspace.yaml from a session directory.
16
+ * Fields: id, cwd, git_root, repository, branch, summary, created_at, updated_at
17
+ */
18
+ function parseWorkspace(sessionDir) {
19
+ const yamlPath = path.join(sessionDir, 'workspace.yaml');
20
+ if (!fs.existsSync(yamlPath)) return null;
21
+ try {
22
+ const raw = fs.readFileSync(yamlPath, 'utf-8');
23
+ // Simple YAML parsing — handle key: value lines
24
+ const meta = {};
25
+ for (const line of raw.split('\n')) {
26
+ const match = line.match(/^(\w+):\s*(.*)$/);
27
+ if (match) meta[match[1]] = match[2].trim();
28
+ }
29
+ return meta;
30
+ } catch { return null; }
31
+ }
32
+
33
+ /**
34
+ * Parse events.jsonl and extract user/assistant messages.
35
+ * Event types: session.start, user.message, assistant.message,
36
+ * assistant.turn_start, assistant.turn_end, session.shutdown
37
+ */
38
+ function parseEvents(sessionDir) {
39
+ const eventsPath = path.join(sessionDir, 'events.jsonl');
40
+ if (!fs.existsSync(eventsPath)) return [];
41
+ try {
42
+ const raw = fs.readFileSync(eventsPath, 'utf-8');
43
+ return raw.split('\n').filter(Boolean).map(line => {
44
+ try { return JSON.parse(line); } catch { return null; }
45
+ }).filter(Boolean);
46
+ } catch { return []; }
47
+ }
48
+
49
+ function getChats() {
50
+ const chats = [];
51
+ if (!fs.existsSync(SESSION_STATE_DIR)) return chats;
52
+
53
+ let sessionDirs;
54
+ try { sessionDirs = fs.readdirSync(SESSION_STATE_DIR); } catch { return chats; }
55
+
56
+ for (const dirName of sessionDirs) {
57
+ const sessionDir = path.join(SESSION_STATE_DIR, dirName);
58
+ try { if (!fs.statSync(sessionDir).isDirectory()) continue; } catch { continue; }
59
+
60
+ const meta = parseWorkspace(sessionDir);
61
+ if (!meta) continue;
62
+
63
+ const events = parseEvents(sessionDir);
64
+ const userMessages = events.filter(e => e.type === 'user.message');
65
+ const assistantMessages = events.filter(e => e.type === 'assistant.message');
66
+ const firstUser = userMessages[0];
67
+
68
+ // Count meaningful messages (user + assistant)
69
+ const bubbleCount = userMessages.length + assistantMessages.length;
70
+ if (bubbleCount === 0) continue;
71
+
72
+ // Extract model from shutdown event or assistant messages
73
+ const shutdown = events.find(e => e.type === 'session.shutdown');
74
+ const model = shutdown?.data?.currentModel || null;
75
+
76
+ chats.push({
77
+ source: 'copilot-cli',
78
+ composerId: meta.id || dirName,
79
+ name: meta.summary || cleanPrompt(firstUser?.data?.content),
80
+ createdAt: meta.created_at ? new Date(meta.created_at).getTime() : null,
81
+ lastUpdatedAt: meta.updated_at ? new Date(meta.updated_at).getTime() : null,
82
+ mode: 'copilot',
83
+ folder: meta.cwd || meta.git_root || null,
84
+ encrypted: false,
85
+ bubbleCount,
86
+ _sessionDir: sessionDir,
87
+ _model: model,
88
+ _shutdownData: shutdown?.data || null,
89
+ });
90
+ }
91
+
92
+ return chats;
93
+ }
94
+
95
+ function cleanPrompt(text) {
96
+ if (!text) return null;
97
+ return text.replace(/\s+/g, ' ').trim().substring(0, 120) || null;
98
+ }
99
+
100
+ function getMessages(chat) {
101
+ const sessionDir = chat._sessionDir;
102
+ if (!sessionDir || !fs.existsSync(sessionDir)) return [];
103
+
104
+ const events = parseEvents(sessionDir);
105
+ const result = [];
106
+
107
+ // Aggregate token usage from shutdown event's modelMetrics
108
+ const shutdown = events.find(e => e.type === 'session.shutdown');
109
+ const modelMetrics = shutdown?.data?.modelMetrics || {};
110
+
111
+ // Build total token counts from model metrics
112
+ let totalInput = 0, totalOutput = 0, totalCacheRead = 0;
113
+ for (const metrics of Object.values(modelMetrics)) {
114
+ const u = metrics.usage || {};
115
+ totalInput += u.inputTokens || 0;
116
+ totalOutput += u.outputTokens || 0;
117
+ totalCacheRead += u.cacheReadTokens || 0;
118
+ }
119
+
120
+ for (const event of events) {
121
+ if (event.type === 'user.message') {
122
+ const content = event.data?.content;
123
+ if (content) result.push({ role: 'user', content });
124
+
125
+ } else if (event.type === 'assistant.message') {
126
+ const data = event.data || {};
127
+ const parts = [];
128
+ const toolCalls = [];
129
+
130
+ // Main text content
131
+ if (data.content) parts.push(data.content);
132
+
133
+ // Tool requests
134
+ if (data.toolRequests && Array.isArray(data.toolRequests)) {
135
+ for (const tr of data.toolRequests) {
136
+ const tcName = tr.name || tr.toolName || 'unknown';
137
+ const args = tr.args || tr.arguments || tr.input || {};
138
+ const parsedArgs = typeof args === 'string' ? safeParse(args) : args;
139
+ const argKeys = typeof parsedArgs === 'object' ? Object.keys(parsedArgs).join(', ') : '';
140
+ parts.push(`[tool-call: ${tcName}(${argKeys})]`);
141
+ toolCalls.push({ name: tcName, args: parsedArgs });
142
+ }
143
+ }
144
+
145
+ if (parts.length > 0) {
146
+ result.push({
147
+ role: 'assistant',
148
+ content: parts.join('\n'),
149
+ _model: shutdown?.data?.currentModel || chat._model,
150
+ _outputTokens: data.outputTokens,
151
+ _toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
152
+ });
153
+ }
154
+ }
155
+ }
156
+
157
+ // Attach aggregate token info to the first assistant message if available
158
+ if (result.length > 0 && totalInput > 0) {
159
+ const firstAssistant = result.find(m => m.role === 'assistant');
160
+ if (firstAssistant) {
161
+ firstAssistant._inputTokens = totalInput;
162
+ if (!firstAssistant._outputTokens) firstAssistant._outputTokens = totalOutput;
163
+ if (totalCacheRead) firstAssistant._cacheRead = totalCacheRead;
164
+ }
165
+ }
166
+
167
+ return result;
168
+ }
169
+
170
+ function safeParse(str) {
171
+ try { return JSON.parse(str); } catch { return {}; }
172
+ }
173
+
174
+ // ============================================================
175
+ // Usage / quota data from GitHub Copilot internal API
176
+ // ============================================================
177
+
178
+ function getCopilotToken() {
179
+ // GitHub Copilot stores its OAuth token in ~/.config/github-copilot/apps.json
180
+ const appsPath = path.join(os.homedir(), '.config', 'github-copilot', 'apps.json');
181
+ try {
182
+ if (!fs.existsSync(appsPath)) return null;
183
+ const data = JSON.parse(fs.readFileSync(appsPath, 'utf-8'));
184
+ for (const entry of Object.values(data)) {
185
+ if (entry.oauth_token) return { token: entry.oauth_token, user: entry.user || null };
186
+ }
187
+ } catch {}
188
+ return null;
189
+ }
190
+
191
+ function fetchCopilotStatus(token) {
192
+ return new Promise((resolve) => {
193
+ const https = require('https');
194
+ const req = https.get('https://api.github.com/copilot_internal/v2/token', {
195
+ headers: {
196
+ 'Authorization': `token ${token}`,
197
+ 'Accept': 'application/json',
198
+ 'User-Agent': 'agentlytics/1.0',
199
+ },
200
+ timeout: 10000,
201
+ }, (res) => {
202
+ let data = '';
203
+ res.on('data', (chunk) => { data += chunk; });
204
+ res.on('end', () => {
205
+ try { resolve(JSON.parse(data)); } catch { resolve(null); }
206
+ });
207
+ });
208
+ req.on('error', () => resolve(null));
209
+ req.on('timeout', () => { req.destroy(); resolve(null); });
210
+ });
211
+ }
212
+
213
+ async function getUsage() {
214
+ const { isSubscriptionAccessAllowed } = require('./base');
215
+ if (!isSubscriptionAccessAllowed()) return null;
216
+ const creds = getCopilotToken();
217
+ if (!creds) return null;
218
+
219
+ const status = await fetchCopilotStatus(creds.token);
220
+ if (!status || status.message) return null;
221
+
222
+ return {
223
+ source: 'copilot-cli',
224
+ plan: {
225
+ name: status.sku || null,
226
+ individual: status.individual || false,
227
+ },
228
+ features: {
229
+ chat: status.chat_enabled || false,
230
+ codeReview: status.code_review_enabled || false,
231
+ agentMode: status.agent_mode_auto_approval || false,
232
+ },
233
+ limits: {
234
+ quotas: status.limited_user_quotas || null,
235
+ resetDate: status.limited_user_reset_date || null,
236
+ },
237
+ user: {
238
+ login: creds.user || null,
239
+ },
240
+ };
241
+ }
242
+
243
+ const labels = { 'copilot-cli': 'GitHub Copilot' };
244
+
245
+ function getArtifacts(folder) {
246
+ const { scanArtifacts } = require('./base');
247
+ return scanArtifacts(folder, {
248
+ editor: 'copilot-cli',
249
+ label: 'GitHub Copilot',
250
+ files: ['.github/copilot-instructions.md'],
251
+ dirs: [],
252
+ });
253
+ }
254
+
255
+ function getMCPServers() {
256
+ // GitHub Copilot shares MCP config with VS Code (handled by vscode.js)
257
+ return [];
258
+ }
259
+
260
+ module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts, getMCPServers };
@@ -0,0 +1,202 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+
5
+ const CURSOR_PROJECTS_DIR = path.join(os.homedir(), '.cursor', 'projects');
6
+
7
+ // ============================================================
8
+ // Adapter interface
9
+ // ============================================================
10
+
11
+ const name = 'cursor-agent';
12
+
13
+ /**
14
+ * Decode project directory name back to folder path.
15
+ * e.g. "Users-fka-Code-Wapple" → "/Users/fka/Code/Wapple"
16
+ */
17
+ function decodeProjectDir(dirName) {
18
+ // The encoding replaces "/" with "-". We reconstruct by prepending "/"
19
+ // and replacing "-" back to "/". Handle ambiguity by checking if path exists.
20
+ const candidate = '/' + dirName.replace(/-/g, '/');
21
+ if (fs.existsSync(candidate)) return candidate;
22
+ // Fallback: try common patterns (the first segment is usually "Users")
23
+ const parts = dirName.split('-');
24
+ for (let i = 2; i < parts.length; i++) {
25
+ const prefix = '/' + parts.slice(0, i).join('/');
26
+ const suffix = parts.slice(i).join('-');
27
+ const full = path.join(prefix, suffix);
28
+ if (fs.existsSync(full)) return full;
29
+ }
30
+ return candidate;
31
+ }
32
+
33
+ /**
34
+ * Find all agent transcript JSONL files across all projects.
35
+ * Two patterns:
36
+ * - <project>/agent-transcripts/<id>.jsonl (flat)
37
+ * - <project>/agent-transcripts/<id>/<id>.jsonl (nested)
38
+ */
39
+ function findTranscripts() {
40
+ const results = [];
41
+ if (!fs.existsSync(CURSOR_PROJECTS_DIR)) return results;
42
+
43
+ let projectDirs;
44
+ try { projectDirs = fs.readdirSync(CURSOR_PROJECTS_DIR); } catch { return results; }
45
+
46
+ for (const projDir of projectDirs) {
47
+ const transcriptsDir = path.join(CURSOR_PROJECTS_DIR, projDir, 'agent-transcripts');
48
+ if (!fs.existsSync(transcriptsDir)) continue;
49
+
50
+ let entries;
51
+ try { entries = fs.readdirSync(transcriptsDir); } catch { continue; }
52
+
53
+ const folder = decodeProjectDir(projDir);
54
+
55
+ for (const entry of entries) {
56
+ const entryPath = path.join(transcriptsDir, entry);
57
+
58
+ // Flat pattern: <id>.jsonl
59
+ if (entry.endsWith('.jsonl')) {
60
+ const sessionId = entry.replace('.jsonl', '');
61
+ results.push({ sessionId, jsonlPath: entryPath, folder });
62
+ continue;
63
+ }
64
+
65
+ // Nested pattern: <id>/<id>.jsonl
66
+ try {
67
+ if (fs.statSync(entryPath).isDirectory()) {
68
+ const nestedJsonl = path.join(entryPath, entry + '.jsonl');
69
+ if (fs.existsSync(nestedJsonl)) {
70
+ results.push({ sessionId: entry, jsonlPath: nestedJsonl, folder });
71
+ }
72
+ }
73
+ } catch { /* skip */ }
74
+ }
75
+ }
76
+ return results;
77
+ }
78
+
79
+ /**
80
+ * Parse a JSONL transcript file into an array of raw message objects.
81
+ * Each line: {"role":"user"|"assistant", "message":{"content":[{"type":"text","text":"..."}]}}
82
+ */
83
+ function parseJsonl(jsonlPath) {
84
+ try {
85
+ const raw = fs.readFileSync(jsonlPath, 'utf-8');
86
+ return raw.split('\n').filter(Boolean).map(line => {
87
+ try { return JSON.parse(line); } catch { return null; }
88
+ }).filter(Boolean);
89
+ } catch { return []; }
90
+ }
91
+
92
+ /**
93
+ * Extract the first user query text from a parsed transcript (for chat name).
94
+ */
95
+ function extractFirstUserText(entries) {
96
+ for (const e of entries) {
97
+ if (e.role !== 'user') continue;
98
+ const parts = e.message?.content;
99
+ if (!Array.isArray(parts)) continue;
100
+ for (const p of parts) {
101
+ if (p.type === 'text' && p.text) {
102
+ // Strip <user_query> wrapper if present
103
+ let text = p.text.replace(/<\/?user_query>/g, '').trim();
104
+ // Strip <attached_files> blocks
105
+ text = text.replace(/<attached_files>[\s\S]*?<\/attached_files>/g, '').trim();
106
+ if (text) return text.substring(0, 120);
107
+ }
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+
113
+ function getChats() {
114
+ const chats = [];
115
+ const transcripts = findTranscripts();
116
+
117
+ for (const { sessionId, jsonlPath, folder } of transcripts) {
118
+ try {
119
+ const stat = fs.statSync(jsonlPath);
120
+ const entries = parseJsonl(jsonlPath);
121
+ if (entries.length === 0) continue;
122
+
123
+ // Count user/assistant messages
124
+ const userCount = entries.filter(e => e.role === 'user').length;
125
+ const assistantCount = entries.filter(e => e.role === 'assistant').length;
126
+ const bubbleCount = userCount + assistantCount;
127
+ if (bubbleCount === 0) continue;
128
+
129
+ chats.push({
130
+ source: 'cursor-agent',
131
+ composerId: sessionId,
132
+ name: extractFirstUserText(entries),
133
+ createdAt: stat.birthtimeMs || stat.mtimeMs,
134
+ lastUpdatedAt: stat.mtimeMs,
135
+ mode: 'agent',
136
+ folder,
137
+ bubbleCount,
138
+ _jsonlPath: jsonlPath,
139
+ });
140
+ } catch { /* skip */ }
141
+ }
142
+
143
+ return chats;
144
+ }
145
+
146
+ function getMessages(chat) {
147
+ const jsonlPath = chat._jsonlPath;
148
+ if (!jsonlPath || !fs.existsSync(jsonlPath)) return [];
149
+
150
+ const entries = parseJsonl(jsonlPath);
151
+ const result = [];
152
+
153
+ for (const entry of entries) {
154
+ const parts = entry.message?.content;
155
+ if (!Array.isArray(parts)) continue;
156
+
157
+ const textParts = [];
158
+ for (const p of parts) {
159
+ if (p.type === 'text' && p.text) {
160
+ let text = p.text;
161
+ // Clean up user_query wrappers
162
+ text = text.replace(/<\/?user_query>/g, '').trim();
163
+ // Clean attached_files blocks but note file references
164
+ const fileRefs = [];
165
+ text = text.replace(/<attached_files>([\s\S]*?)<\/attached_files>/g, (_, inner) => {
166
+ const paths = inner.match(/path="([^"]+)"/g);
167
+ if (paths) {
168
+ for (const pm of paths) {
169
+ const fp = pm.match(/path="([^"]+)"/);
170
+ if (fp) fileRefs.push(fp[1]);
171
+ }
172
+ }
173
+ return '';
174
+ }).trim();
175
+ // Clean image_files blocks
176
+ text = text.replace(/<image_files>[\s\S]*?<\/image_files>/g, '[image]').trim();
177
+ if (text) textParts.push(text);
178
+ if (fileRefs.length > 0) {
179
+ textParts.push(fileRefs.map(f => `[file: ${f}]`).join('\n'));
180
+ }
181
+ }
182
+ }
183
+
184
+ if (textParts.length > 0) {
185
+ result.push({
186
+ role: entry.role === 'user' ? 'user' : 'assistant',
187
+ content: textParts.join('\n'),
188
+ });
189
+ }
190
+ }
191
+
192
+ return result;
193
+ }
194
+
195
+ const labels = { 'cursor-agent': 'Cursor Agent' };
196
+
197
+ function getMCPServers() {
198
+ // Cursor Agent shares MCP config with Cursor (handled by cursor.js)
199
+ return [];
200
+ }
201
+
202
+ module.exports = { name, labels, getChats, getMessages, getMCPServers };