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,228 @@
1
+ // mem-house server — the dashboard/API entrypoint.
2
+ //
3
+ // Speaks the agentlytics REST contract (same routes, same response shapes) over the
4
+ // mem-house typed schema, so the UNMODIFIED agentlytics React SPA is the dashboard:
5
+ // queries.js computes every response; this file is routing + config + stubs. Serves
6
+ // the built SPA from the repo root's public/.
7
+ //
8
+ // node mem-house/server/server.js (env: MEMHOUSE_* per DESIGN.md; port 4640)
9
+
10
+ const express = require('express');
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+ const qy = require('./queries');
15
+
16
+ const PORT = parseInt(process.env.MEMHOUSE_PORT || '4640', 10);
17
+ // Localhost by default: the dashboard exposes full transcripts and a SQL console
18
+ // with no auth. Remote access is an explicit opt-in (MEMHOUSE_HOST=0.0.0.0).
19
+ const HOST = process.env.MEMHOUSE_HOST || '127.0.0.1';
20
+ const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
21
+ const CONFIG_PATH = path.join(os.homedir(), '.memhouse', 'config.json');
22
+
23
+ const app = express();
24
+ app.use(express.json());
25
+ if (fs.existsSync(PUBLIC_DIR)) app.use(express.static(PUBLIC_DIR));
26
+
27
+ function readConfig() {
28
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return {}; }
29
+ }
30
+ function writeConfig(config) {
31
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
32
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
33
+ }
34
+ const hiddenFolders = () => readConfig().hiddenProjects || [];
35
+
36
+ function parseDateOpts(query) {
37
+ const opts = {};
38
+ if (query.dateFrom) opts.dateFrom = parseInt(query.dateFrom) || null;
39
+ if (query.dateTo) opts.dateTo = parseInt(query.dateTo) || null;
40
+ return opts;
41
+ }
42
+
43
+ // Route helper: async handler with the uniform 500 shape.
44
+ const route = (fn) => async (req, res) => {
45
+ try { await fn(req, res); } catch (err) { res.status(500).json({ error: err.message }); }
46
+ };
47
+
48
+ app.get('/api/ping', (req, res) => res.json({ app: 'agentlytics', pid: process.pid }));
49
+ app.get('/api/mode', (req, res) => res.json({ mode: 'local' }));
50
+
51
+ app.get('/api/overview', route(async (req, res) => {
52
+ res.json(await qy.getOverview({ editor: req.query.editor || null, ...parseDateOpts(req.query), hiddenFolders: hiddenFolders() }));
53
+ }));
54
+
55
+ app.get('/api/daily-activity', route(async (req, res) => {
56
+ res.json(await qy.getDailyActivity({ editor: req.query.editor || null, ...parseDateOpts(req.query), hiddenFolders: hiddenFolders() }));
57
+ }));
58
+
59
+ app.get('/api/dashboard-stats', route(async (req, res) => {
60
+ res.json(await qy.getDashboardStats({ editor: req.query.editor || null, ...parseDateOpts(req.query), hiddenFolders: hiddenFolders() }));
61
+ }));
62
+
63
+ app.get('/api/chats', route(async (req, res) => {
64
+ const opts = {
65
+ editor: req.query.editor || null,
66
+ folder: req.query.folder || null,
67
+ named: req.query.named !== 'false',
68
+ limit: req.query.limit ? parseInt(req.query.limit) : 200,
69
+ offset: req.query.offset ? parseInt(req.query.offset) : 0,
70
+ ...parseDateOpts(req.query),
71
+ hiddenFolders: hiddenFolders(),
72
+ };
73
+ const [total, chats] = await Promise.all([qy.countChats(opts), qy.getChats(opts)]);
74
+ res.json({ total, chats });
75
+ }));
76
+
77
+ app.get('/api/chats/:id', route(async (req, res) => {
78
+ const result = await qy.getChat(req.params.id);
79
+ if (!result) return res.status(404).json({ error: 'Chat not found' });
80
+ res.json(result);
81
+ }));
82
+
83
+ app.get('/api/chats/:id/markdown', route(async (req, res) => {
84
+ const result = await qy.getChat(req.params.id);
85
+ if (!result) return res.status(404).json({ error: 'Chat not found' });
86
+ const lines = [];
87
+ const title = result.name || 'Untitled Session';
88
+ lines.push(`# ${title}\n`);
89
+ const meta = [];
90
+ if (result.source) meta.push(`**Editor:** ${result.source}`);
91
+ if (result.mode) meta.push(`**Mode:** ${result.mode}`);
92
+ if (result.folder) meta.push(`**Project:** ${result.folder}`);
93
+ if (result.createdAt) meta.push(`**Created:** ${new Date(result.createdAt).toISOString()}`);
94
+ if (result.lastUpdatedAt) meta.push(`**Updated:** ${new Date(result.lastUpdatedAt).toISOString()}`);
95
+ if (result.stats) {
96
+ meta.push(`**Messages:** ${result.stats.totalMessages}`);
97
+ if (result.stats.totalInputTokens) meta.push(`**Input Tokens:** ${result.stats.totalInputTokens}`);
98
+ if (result.stats.totalOutputTokens) meta.push(`**Output Tokens:** ${result.stats.totalOutputTokens}`);
99
+ const models = [...new Set(result.stats.models || [])];
100
+ if (models.length > 0) meta.push(`**Models:** ${models.join(', ')}`);
101
+ }
102
+ if (meta.length > 0) lines.push(meta.join(' \n') + '\n');
103
+ lines.push('---\n');
104
+ for (const msg of result.messages) {
105
+ const label = msg.role === 'user' ? '## User' : msg.role === 'assistant' ? '## Assistant' : `## ${msg.role.charAt(0).toUpperCase() + msg.role.slice(1)}`;
106
+ const modelTag = msg.model ? ` *(${msg.model})*` : '';
107
+ lines.push(`${label}${modelTag}\n`);
108
+ lines.push(msg.content + '\n');
109
+ }
110
+ const filename = title.replace(/[^a-zA-Z0-9_-]/g, '_').substring(0, 80) + '.md';
111
+ res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
112
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
113
+ res.send(lines.join('\n'));
114
+ }));
115
+
116
+ app.get('/api/projects', route(async (req, res) => {
117
+ res.json(await qy.getProjects({ ...parseDateOpts(req.query), hiddenFolders: hiddenFolders() }));
118
+ }));
119
+
120
+ app.get('/api/all-projects', route(async (req, res) => {
121
+ res.json(await qy.getProjects({ ...parseDateOpts(req.query), includeHidden: true }));
122
+ }));
123
+
124
+ app.get('/api/deep-analytics', route(async (req, res) => {
125
+ res.json(await qy.getDeepAnalytics({
126
+ editor: req.query.editor || null,
127
+ folder: req.query.folder || null,
128
+ limit: Math.min(parseInt(req.query.limit) || 500, 5000),
129
+ ...parseDateOpts(req.query),
130
+ hiddenFolders: hiddenFolders(),
131
+ }));
132
+ }));
133
+
134
+ app.get('/api/costs', route(async (req, res) => {
135
+ res.json(await qy.estimateCosts({
136
+ editor: req.query.editor || null,
137
+ folder: req.query.folder || null,
138
+ chatId: req.query.chatId || null,
139
+ ...parseDateOpts(req.query),
140
+ hiddenFolders: hiddenFolders(),
141
+ }));
142
+ }));
143
+
144
+ app.get('/api/cost-analytics', route(async (req, res) => {
145
+ res.json(await qy.getCostAnalytics({ editor: req.query.editor || null, ...parseDateOpts(req.query), hiddenFolders: hiddenFolders() }));
146
+ }));
147
+
148
+ app.get('/api/tool-calls', route(async (req, res) => {
149
+ const name = req.query.name;
150
+ if (!name) return res.status(400).json({ error: 'name query param required' });
151
+ // Same filter set as the analytics the drill-down is opened from.
152
+ res.json(await qy.getToolCalls(name, {
153
+ limit: Math.min(parseInt(req.query.limit) || 200, 1000),
154
+ folder: req.query.folder || null,
155
+ editor: req.query.editor || null,
156
+ ...parseDateOpts(req.query),
157
+ hiddenFolders: hiddenFolders(),
158
+ }));
159
+ }));
160
+
161
+ app.post('/api/query', route(async (req, res) => {
162
+ const { sql } = req.body;
163
+ if (!sql || typeof sql !== 'string') return res.status(400).json({ error: 'sql string required' });
164
+ const first = sql.trim().replace(/^--.*$/gm, '').trim().split(/\s+/)[0].toUpperCase();
165
+ if (!['SELECT', 'WITH', 'EXPLAIN', 'DESCRIBE', 'SHOW'].includes(first)) {
166
+ return res.status(403).json({ error: 'Only SELECT queries are allowed' });
167
+ }
168
+ try { res.json(await qy.rawQuery(sql)); }
169
+ catch (err) { res.status(400).json({ error: err.message }); }
170
+ }));
171
+
172
+ app.get('/api/schema', route(async (req, res) => res.json(await qy.schema())));
173
+
174
+ app.get('/api/config', (req, res) => res.json(readConfig()));
175
+ app.put('/api/config', route(async (req, res) => {
176
+ const config = readConfig();
177
+ Object.assign(config, req.body);
178
+ writeConfig(config);
179
+ res.json(config);
180
+ }));
181
+
182
+ // Real refetch: run one full shipper pass, streaming SSE like the root contract.
183
+ app.get('/api/refetch', async (req, res) => {
184
+ res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
185
+ try {
186
+ const { runShip } = require('../shipper/ship');
187
+ res.write(`data: ${JSON.stringify({ scanned: 0, analyzed: 0, skipped: 0, total: 0 })}\n\n`);
188
+ const r = await runShip(qy.getClient(), { full: true });
189
+ res.write(`data: ${JSON.stringify({ done: true, total: r.sessions + r.skipped, analyzed: r.sessions })}\n\n`);
190
+ } catch (err) {
191
+ res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
192
+ }
193
+ res.end();
194
+ });
195
+
196
+ // Not applicable to mem-house (dashboard renders these pages empty/absent).
197
+ app.get('/api/usage', (req, res) => res.json([]));
198
+ app.get('/api/artifacts', (req, res) => res.json([]));
199
+ app.get('/api/artifact-content', (req, res) => res.status(404).json({ error: 'not available in mem-house' }));
200
+ app.get('/api/mcps', (req, res) => res.json({
201
+ servers: [], toolCalls: [], matchedTools: {}, topSessions: [], projectMcps: [],
202
+ summary: { totalServers: 0, totalToolCalls: 0, uniqueTools: 0, sessionsWithTools: 0, editorsWithServers: [] },
203
+ }));
204
+ app.get('/api/gsd/projects', (req, res) => res.json([]));
205
+ app.get('/api/gsd/phases', (req, res) => res.json([]));
206
+ app.get('/api/gsd/plan', (req, res) => res.status(404).json({ error: 'not available in mem-house' }));
207
+ app.get('/api/gsd/overview', (req, res) => res.json({ totalProjects: 0, totalPhases: 0, completedPhases: 0, activePhases: [], executingPhases: 0, plannedPhases: 0 }));
208
+ app.get('/api/gsd/config', (req, res) => res.json(null));
209
+ app.get('/api/gsd/phase-tokens', (req, res) => res.json([]));
210
+ app.get('/api/gsd/file', (req, res) => res.json({ content: null }));
211
+ app.get('/api/share-image', (req, res) => res.status(501).json({ error: 'not available in mem-house' }));
212
+ app.get('/api/check-ai', (req, res) => res.status(501).json({ error: 'not available in mem-house' }));
213
+
214
+ // SPA fallback
215
+ app.get('*', (req, res) => {
216
+ const index = path.join(PUBLIC_DIR, 'index.html');
217
+ if (fs.existsSync(index)) res.sendFile(index);
218
+ else res.status(503).json({ error: 'UI not built — run: cd ui && npm install && npm run build' });
219
+ });
220
+
221
+ if (require.main === module) {
222
+ app.listen(PORT, HOST, () => {
223
+ console.log(`[mem-house] dashboard → http://localhost:${PORT} (house '${qy.config.database}' @ ${qy.config.url.replace(/\/\/.*@/, '//')})`
224
+ + (HOST !== '127.0.0.1' ? ` [bound to ${HOST} — remotely reachable]` : ''));
225
+ });
226
+ }
227
+
228
+ module.exports = app;