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,413 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const { getAppDataPath } = require('./base');
5
+
6
+ // VS Code variants: stable and insiders
7
+ const VARIANTS = [
8
+ {
9
+ id: 'vscode',
10
+ appSupport: getAppDataPath('Code'),
11
+ },
12
+ {
13
+ id: 'vscode-insiders',
14
+ appSupport: getAppDataPath('Code - Insiders'),
15
+ },
16
+ ];
17
+
18
+ // ============================================================
19
+ // JSONL reconstruction: kind:0 = init, kind:1 = patch at key path
20
+ // ============================================================
21
+
22
+ function reconstructSession(filePath) {
23
+ const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
24
+ let state = null;
25
+ for (const line of lines) {
26
+ let obj;
27
+ try { obj = JSON.parse(line); } catch { continue; }
28
+ if (obj.kind === 0) {
29
+ state = obj.v;
30
+ } else if (obj.kind === 1 && state) {
31
+ let target = state;
32
+ const p = obj.k;
33
+ for (let i = 0; i < p.length - 1; i++) {
34
+ if (target[p[i]] === undefined) {
35
+ target[p[i]] = typeof p[i + 1] === 'number' ? [] : {};
36
+ }
37
+ target = target[p[i]];
38
+ }
39
+ target[p[p.length - 1]] = obj.v;
40
+ }
41
+ }
42
+ return state;
43
+ }
44
+
45
+ // ============================================================
46
+ // Extract user text from renderedUserMessage (JSONL) or message.text (JSON)
47
+ // ============================================================
48
+
49
+ function extractUserText(renderedParts) {
50
+ if (!Array.isArray(renderedParts)) return '';
51
+ for (const part of renderedParts) {
52
+ let text = part.text || '';
53
+ // Strip context/reminder wrappers, extract userRequest content
54
+ const userReq = text.match(/<userRequest>([\s\S]*?)<\/userRequest>/);
55
+ if (userReq) return userReq[1].trim();
56
+ const stripped = text
57
+ .replace(/<context>[\s\S]*?<\/context>/g, '')
58
+ .replace(/<reminderInstructions>[\s\S]*?<\/reminderInstructions>/g, '')
59
+ .replace(/<attachments>[\s\S]*?<\/attachments>/g, '')
60
+ .trim();
61
+ if (stripped) return stripped;
62
+ }
63
+ return '';
64
+ }
65
+
66
+ // ============================================================
67
+ // Reconstruct assistant response from codeBlocks metadata
68
+ // ============================================================
69
+
70
+ function reconstructResponse(metadata) {
71
+ if (!metadata) return '';
72
+
73
+ // Method 1: codeBlocks (simple chat responses)
74
+ const codeBlocks = metadata.codeBlocks || [];
75
+ if (codeBlocks.length > 0) {
76
+ let response = '';
77
+ for (const b of codeBlocks) {
78
+ if (b.markdownBeforeBlock) response += b.markdownBeforeBlock;
79
+ if (b.code) response += '\n```' + (b.language || '') + '\n' + b.code + '```\n';
80
+ }
81
+ if (response.trim()) return response.trim();
82
+ }
83
+
84
+ // Method 2: toolCallRounds (agent mode responses)
85
+ const rounds = metadata.toolCallRounds || [];
86
+ if (rounds.length > 0) {
87
+ const parts = [];
88
+ const _toolCalls = [];
89
+ for (const round of rounds) {
90
+ const text = (round.response || '').trim();
91
+ if (text) parts.push(text);
92
+ for (const tc of round.toolCalls || []) {
93
+ let args = {};
94
+ try { args = JSON.parse(tc.arguments || '{}'); } catch {}
95
+ const argKeys = typeof args === 'object' ? Object.keys(args).join(', ') : '';
96
+ parts.push(`[tool-call: ${tc.name || 'tool'}(${argKeys})]`);
97
+ _toolCalls.push({ name: tc.name || 'tool', args });
98
+ }
99
+ }
100
+ if (parts.length > 0) return { text: parts.join('\n'), toolCalls: _toolCalls };
101
+ }
102
+
103
+ return '';
104
+ }
105
+
106
+ // ============================================================
107
+ // Parse a session file (.jsonl or .json) into { meta, requests }
108
+ // ============================================================
109
+
110
+ function parseSessionFile(filePath) {
111
+ const ext = path.extname(filePath);
112
+ if (ext === '.jsonl') {
113
+ const state = reconstructSession(filePath);
114
+ if (!state) return null;
115
+ return {
116
+ sessionId: state.sessionId,
117
+ createdAt: state.creationDate,
118
+ title: state.customTitle || null,
119
+ requests: state.requests || [],
120
+ format: 'jsonl',
121
+ selectedModel: state.inputState?.selectedModel?.metadata?.id || null,
122
+ };
123
+ } else if (ext === '.json') {
124
+ let data;
125
+ try { data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return null; }
126
+ return {
127
+ sessionId: data.sessionId,
128
+ createdAt: data.creationDate || data.lastMessageDate,
129
+ title: data.customTitle || null,
130
+ requests: data.requests || [],
131
+ format: 'json',
132
+ selectedModel: data.inputState?.selectedModel?.metadata?.id || null,
133
+ };
134
+ }
135
+ return null;
136
+ }
137
+
138
+ // ============================================================
139
+ // Discover workspace folder from workspaceStorage hash dir
140
+ // ============================================================
141
+
142
+ function getWorkspaceFolder(wsDir) {
143
+ const wsJson = path.join(wsDir, 'workspace.json');
144
+ if (!fs.existsSync(wsJson)) return null;
145
+ try {
146
+ const data = JSON.parse(fs.readFileSync(wsJson, 'utf-8'));
147
+ const uri = data.folder || data.workspace;
148
+ if (uri) return decodeURIComponent(uri.replace('file://', ''));
149
+ } catch {}
150
+ return null;
151
+ }
152
+
153
+ // ============================================================
154
+ // Adapter interface
155
+ // ============================================================
156
+
157
+ const name = 'vscode';
158
+
159
+ function getChats() {
160
+ const chats = [];
161
+
162
+ for (const variant of VARIANTS) {
163
+ if (!fs.existsSync(variant.appSupport)) continue;
164
+
165
+ // 1. Global (empty window) chat sessions
166
+ const globalDir = path.join(variant.appSupport, 'User', 'globalStorage', 'emptyWindowChatSessions');
167
+ if (fs.existsSync(globalDir)) {
168
+ collectSessions(globalDir, null, variant.id, chats);
169
+ }
170
+
171
+ // 2. Workspace chat sessions
172
+ const wsRoot = path.join(variant.appSupport, 'User', 'workspaceStorage');
173
+ if (fs.existsSync(wsRoot)) {
174
+ for (const wsHash of fs.readdirSync(wsRoot)) {
175
+ const wsDir = path.join(wsRoot, wsHash);
176
+ if (!fs.statSync(wsDir).isDirectory()) continue;
177
+ const chatDir = path.join(wsDir, 'chatSessions');
178
+ if (!fs.existsSync(chatDir)) continue;
179
+ const folder = getWorkspaceFolder(wsDir);
180
+ collectSessions(chatDir, folder, variant.id, chats);
181
+ }
182
+ }
183
+ }
184
+
185
+ return chats;
186
+ }
187
+
188
+ function collectSessions(dir, folder, source, chats) {
189
+ let files;
190
+ try { files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl') || f.endsWith('.json')); } catch { return; }
191
+
192
+ for (const file of files) {
193
+ const filePath = path.join(dir, file);
194
+ try {
195
+ const stat = fs.statSync(filePath);
196
+ // Quick peek: for listing we only need metadata, not full reconstruction
197
+ const meta = peekMeta(filePath);
198
+ chats.push({
199
+ source,
200
+ composerId: meta.sessionId || file.replace(/\.(jsonl|json)$/, ''),
201
+ name: meta.title || meta.firstUserText || null,
202
+ createdAt: meta.createdAt || stat.birthtime.getTime(),
203
+ lastUpdatedAt: stat.mtime.getTime(),
204
+ mode: 'copilot',
205
+ folder,
206
+ encrypted: false,
207
+ bubbleCount: meta.requestCount || 0,
208
+ _filePath: filePath,
209
+ _modelPref: meta.selectedModel || null,
210
+ });
211
+ } catch { /* skip */ }
212
+ }
213
+ }
214
+
215
+ function peekMeta(filePath) {
216
+ const ext = path.extname(filePath);
217
+ if (ext === '.json') {
218
+ try {
219
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
220
+ const firstText = data.requests?.[0]?.message?.text || '';
221
+ return {
222
+ sessionId: data.sessionId,
223
+ title: data.customTitle || null,
224
+ createdAt: data.creationDate || data.lastMessageDate,
225
+ requestCount: data.requests?.length || 0,
226
+ firstUserText: firstText.substring(0, 120) || null,
227
+ selectedModel: data.inputState?.selectedModel?.metadata?.id || null,
228
+ };
229
+ } catch { return {}; }
230
+ }
231
+ // JSONL: read first line (kind:0) for session metadata + scan for customTitle
232
+ try {
233
+ const content = fs.readFileSync(filePath, 'utf-8');
234
+ const firstNewline = content.indexOf('\n');
235
+ const firstLine = firstNewline > 0 ? content.substring(0, firstNewline) : content;
236
+ const init = JSON.parse(firstLine);
237
+ const state = init.v || {};
238
+ let title = state.customTitle || null;
239
+ // Scan for customTitle patch if not in init
240
+ if (!title) {
241
+ const titleIdx = content.indexOf('"customTitle"');
242
+ if (titleIdx !== -1) {
243
+ const lineStart = content.lastIndexOf('\n', titleIdx) + 1;
244
+ const lineEnd = content.indexOf('\n', titleIdx);
245
+ const patchLine = content.substring(lineStart, lineEnd > 0 ? lineEnd : undefined);
246
+ try {
247
+ const patch = JSON.parse(patchLine);
248
+ if (patch.kind === 1 && patch.k[0] === 'customTitle') title = patch.v;
249
+ } catch {}
250
+ }
251
+ }
252
+ return {
253
+ sessionId: state.sessionId,
254
+ title,
255
+ createdAt: state.creationDate,
256
+ requestCount: null,
257
+ firstUserText: null,
258
+ selectedModel: state.inputState?.selectedModel?.metadata?.id || null,
259
+ };
260
+ } catch { return {}; }
261
+ }
262
+
263
+ function getMessages(chat) {
264
+ if (!chat._filePath || !fs.existsSync(chat._filePath)) return [];
265
+
266
+ const parsed = parseSessionFile(chat._filePath);
267
+ if (!parsed) return [];
268
+
269
+ const messages = [];
270
+ for (const req of parsed.requests) {
271
+ if (!req) continue;
272
+
273
+ // User message
274
+ let userText = '';
275
+ if (parsed.format === 'json') {
276
+ userText = req.message?.text || '';
277
+ } else {
278
+ // JSONL: user text is in result.metadata.renderedUserMessage
279
+ userText = extractUserText(req.result?.metadata?.renderedUserMessage);
280
+ }
281
+ if (userText) {
282
+ messages.push({ role: 'user', content: userText });
283
+ }
284
+
285
+ // Assistant response
286
+ let responseText = '';
287
+ let _toolCalls = [];
288
+ if (parsed.format === 'json') {
289
+ // Older format: response is array-like object
290
+ const resp = req.response;
291
+ if (resp) {
292
+ const values = Object.values(resp);
293
+ responseText = values.map(v => v.value || '').filter(Boolean).join('\n');
294
+ }
295
+ } else {
296
+ // JSONL: response from codeBlocks or toolCallRounds
297
+ const result = reconstructResponse(req.result?.metadata);
298
+ if (typeof result === 'object' && result.text) {
299
+ responseText = result.text;
300
+ _toolCalls = result.toolCalls || [];
301
+ } else {
302
+ responseText = result || '';
303
+ }
304
+ }
305
+ if (responseText) {
306
+ const meta = req.result?.metadata;
307
+ messages.push({
308
+ role: 'assistant', content: responseText,
309
+ _model: parsed.selectedModel || null,
310
+ _inputTokens: meta?.promptTokens, _outputTokens: meta?.outputTokens,
311
+ _toolCalls,
312
+ });
313
+ }
314
+ }
315
+ return messages;
316
+ }
317
+
318
+ // ============================================================
319
+ // Usage / quota data from GitHub Copilot internal API
320
+ // ============================================================
321
+
322
+ function getCopilotToken() {
323
+ const appsPath = path.join(os.homedir(), '.config', 'github-copilot', 'apps.json');
324
+ try {
325
+ if (!fs.existsSync(appsPath)) return null;
326
+ const data = JSON.parse(fs.readFileSync(appsPath, 'utf-8'));
327
+ // Pick the first available oauth_token
328
+ for (const entry of Object.values(data)) {
329
+ if (entry.oauth_token) return { token: entry.oauth_token, user: entry.user || null };
330
+ }
331
+ } catch {}
332
+ return null;
333
+ }
334
+
335
+ function fetchCopilotStatus(token) {
336
+ return new Promise((resolve) => {
337
+ const https = require('https');
338
+ const req = https.get('https://api.github.com/copilot_internal/v2/token', {
339
+ headers: {
340
+ 'Authorization': `token ${token}`,
341
+ 'Accept': 'application/json',
342
+ 'User-Agent': 'agentlytics/1.0',
343
+ },
344
+ timeout: 10000,
345
+ }, (res) => {
346
+ let data = '';
347
+ res.on('data', (chunk) => { data += chunk; });
348
+ res.on('end', () => {
349
+ try { resolve(JSON.parse(data)); } catch { resolve(null); }
350
+ });
351
+ });
352
+ req.on('error', () => resolve(null));
353
+ req.on('timeout', () => { req.destroy(); resolve(null); });
354
+ });
355
+ }
356
+
357
+ async function getUsage() {
358
+ const { isSubscriptionAccessAllowed } = require('./base');
359
+ if (!isSubscriptionAccessAllowed()) return null;
360
+ const creds = getCopilotToken();
361
+ if (!creds) return null;
362
+
363
+ const status = await fetchCopilotStatus(creds.token);
364
+ if (!status || status.message) return null;
365
+
366
+ return {
367
+ source: 'vscode',
368
+ plan: {
369
+ name: status.sku || null,
370
+ individual: status.individual || false,
371
+ },
372
+ features: {
373
+ chat: status.chat_enabled || false,
374
+ codeReview: status.code_review_enabled || false,
375
+ agentMode: status.agent_mode_auto_approval || false,
376
+ xcode: status.xcode || false,
377
+ mcp: status.mcp || false,
378
+ },
379
+ limits: {
380
+ quotas: status.limited_user_quotas || null,
381
+ resetDate: status.limited_user_reset_date || null,
382
+ },
383
+ user: {
384
+ login: creds.user || null,
385
+ },
386
+ };
387
+ }
388
+
389
+ const labels = { 'vscode': 'VS Code', 'vscode-insiders': 'VS Code Insiders' };
390
+
391
+ function getArtifacts(folder) {
392
+ const { scanArtifacts } = require('./base');
393
+ return scanArtifacts(folder, {
394
+ editor: 'vscode',
395
+ label: 'VS Code',
396
+ files: ['.github/copilot-instructions.md'],
397
+ dirs: ['.vscode'],
398
+ });
399
+ }
400
+
401
+ function getMCPServers() {
402
+ const { parseMcpConfigFile } = require('./base');
403
+ const results = [];
404
+ for (const variant of VARIANTS) {
405
+ if (!fs.existsSync(variant.appSupport)) continue;
406
+ // User-level: <appSupport>/User/mcp.json
407
+ const userMcp = path.join(variant.appSupport, 'User', 'mcp.json');
408
+ results.push(...parseMcpConfigFile(userMcp, { editor: variant.id, label: variant.id === 'vscode' ? 'VS Code' : 'VS Code Insiders', scope: 'global' }));
409
+ }
410
+ return results;
411
+ }
412
+
413
+ module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts, getMCPServers };