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.
- package/LICENSE +22 -0
- package/README.md +59 -3
- package/bin/memhouse.js +487 -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/antigravity.js +1019 -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 +438 -0
- package/editors/gemini.js +231 -0
- package/editors/goose.js +358 -0
- package/editors/gsd.js +366 -0
- package/editors/index.js +179 -0
- package/editors/kiro.js +315 -0
- package/editors/opencode.js +374 -0
- package/editors/vscode.js +413 -0
- package/editors/windsurf.js +634 -0
- package/editors/zed.js +213 -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 +75 -0
- package/mem-house/delivery/PROMPT.md +42 -0
- package/mem-house/delivery/install.sh +181 -0
- package/mem-house/delivery/kernel-install.md +76 -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 +402 -0
- package/misc/syntax-check.js +38 -0
- package/package.json +56 -7
- package/pricing.js +88 -0
- package/pricing.json +992 -0
- package/public/assets/index-BsgjOSTv.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 +344 -0
- package/ui/src/pages/Subscriptions.jsx +411 -0
- package/ui/vite.config.js +18 -0
package/mcp-server.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
2
|
+
const { SSEServerTransport } = require('@modelcontextprotocol/sdk/server/sse.js');
|
|
3
|
+
const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
4
|
+
const { z } = require('zod');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates an MCP server instance wired to the relay database.
|
|
9
|
+
* Returns { mcpServer, transports } — caller wires SSE endpoints into Express.
|
|
10
|
+
*/
|
|
11
|
+
function createMcpServer(getDb) {
|
|
12
|
+
const mcpServer = new McpServer({
|
|
13
|
+
name: 'agentlytics-relay',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// ── Tool: list_users ──
|
|
18
|
+
mcpServer.tool(
|
|
19
|
+
'list_users',
|
|
20
|
+
'List all connected users and their shared projects',
|
|
21
|
+
{},
|
|
22
|
+
async () => {
|
|
23
|
+
const db = getDb();
|
|
24
|
+
if (!db) return { content: [{ type: 'text', text: 'Relay database not initialized' }] };
|
|
25
|
+
const users = db.prepare('SELECT username, last_seen, projects FROM users ORDER BY last_seen DESC').all();
|
|
26
|
+
const result = users.map(u => ({
|
|
27
|
+
username: u.username,
|
|
28
|
+
lastSeen: new Date(u.last_seen).toISOString(),
|
|
29
|
+
projects: JSON.parse(u.projects || '[]'),
|
|
30
|
+
}));
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// ── Tool: search_sessions ──
|
|
38
|
+
mcpServer.tool(
|
|
39
|
+
'search_sessions',
|
|
40
|
+
'Search across all users\' chat messages by keyword. Use this to find what someone worked on, or find discussions about a specific file or topic.',
|
|
41
|
+
{
|
|
42
|
+
query: z.string().describe('Search query — keyword, file name, or topic'),
|
|
43
|
+
username: z.string().optional().describe('Filter by specific username'),
|
|
44
|
+
project: z.string().optional().describe('Filter by project folder path (partial match)'),
|
|
45
|
+
limit: z.number().optional().describe('Max results (default 20)'),
|
|
46
|
+
},
|
|
47
|
+
async ({ query, username, project, limit }) => {
|
|
48
|
+
const db = getDb();
|
|
49
|
+
if (!db) return { content: [{ type: 'text', text: 'Relay database not initialized' }] };
|
|
50
|
+
|
|
51
|
+
let sql = `
|
|
52
|
+
SELECT rm.chat_id, rm.username, rm.role, rm.content, rm.model, rm.seq,
|
|
53
|
+
rc.name as chat_name, rc.source, rc.folder, rc.last_updated_at
|
|
54
|
+
FROM relay_messages rm
|
|
55
|
+
JOIN relay_chats rc ON rm.chat_id = rc.id AND rm.username = rc.username
|
|
56
|
+
WHERE rm.content LIKE ?`;
|
|
57
|
+
const params = [`%${query}%`];
|
|
58
|
+
|
|
59
|
+
if (username) { sql += ' AND rm.username = ?'; params.push(username); }
|
|
60
|
+
if (project) { sql += ' AND rc.folder LIKE ?'; params.push(`%${project}%`); }
|
|
61
|
+
sql += ' ORDER BY rc.last_updated_at DESC LIMIT ?';
|
|
62
|
+
params.push(limit || 20);
|
|
63
|
+
|
|
64
|
+
const rows = db.prepare(sql).all(...params);
|
|
65
|
+
|
|
66
|
+
if (rows.length === 0) {
|
|
67
|
+
return { content: [{ type: 'text', text: `No results found for "${query}"` }] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const results = rows.map(r => ({
|
|
71
|
+
chatId: r.chat_id,
|
|
72
|
+
chatName: r.chat_name,
|
|
73
|
+
username: r.username,
|
|
74
|
+
role: r.role,
|
|
75
|
+
source: r.source,
|
|
76
|
+
folder: r.folder,
|
|
77
|
+
lastUpdated: r.last_updated_at ? new Date(r.last_updated_at).toISOString() : null,
|
|
78
|
+
model: r.model,
|
|
79
|
+
content: r.content.length > 300 ? r.content.substring(0, 300) + '...' : r.content,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// ── Tool: get_user_activity ──
|
|
89
|
+
mcpServer.tool(
|
|
90
|
+
'get_user_activity',
|
|
91
|
+
'Get recent activity for a specific user — their recent sessions, what they worked on, which editors and models they used.',
|
|
92
|
+
{
|
|
93
|
+
username: z.string().describe('Username to look up'),
|
|
94
|
+
project: z.string().optional().describe('Filter by project folder (partial match)'),
|
|
95
|
+
file_path: z.string().optional().describe('Filter by file path mentioned in messages'),
|
|
96
|
+
limit: z.number().optional().describe('Max sessions to return (default 20)'),
|
|
97
|
+
},
|
|
98
|
+
async ({ username, project, file_path, limit }) => {
|
|
99
|
+
const db = getDb();
|
|
100
|
+
if (!db) return { content: [{ type: 'text', text: 'Relay database not initialized' }] };
|
|
101
|
+
|
|
102
|
+
// First get sessions
|
|
103
|
+
let sql = `
|
|
104
|
+
SELECT rc.*, rcs.total_messages, rcs.models, rcs.tool_calls,
|
|
105
|
+
rcs.total_input_tokens, rcs.total_output_tokens
|
|
106
|
+
FROM relay_chats rc
|
|
107
|
+
LEFT JOIN relay_chat_stats rcs ON rc.id = rcs.chat_id AND rc.username = rcs.username
|
|
108
|
+
WHERE rc.username = ?`;
|
|
109
|
+
const params = [username];
|
|
110
|
+
|
|
111
|
+
if (project) { sql += ' AND rc.folder LIKE ?'; params.push(`%${project}%`); }
|
|
112
|
+
sql += ' ORDER BY rc.last_updated_at DESC LIMIT ?';
|
|
113
|
+
params.push(limit || 20);
|
|
114
|
+
|
|
115
|
+
let sessions = db.prepare(sql).all(...params);
|
|
116
|
+
|
|
117
|
+
// If file_path filter, narrow down to sessions mentioning that file
|
|
118
|
+
if (file_path && sessions.length > 0) {
|
|
119
|
+
const chatIds = sessions.map(s => s.id);
|
|
120
|
+
const placeholders = chatIds.map(() => '?').join(',');
|
|
121
|
+
const fileMatches = db.prepare(`
|
|
122
|
+
SELECT DISTINCT chat_id FROM relay_messages
|
|
123
|
+
WHERE chat_id IN (${placeholders}) AND username = ? AND content LIKE ?
|
|
124
|
+
`).all(...chatIds, username, `%${file_path}%`);
|
|
125
|
+
const matchingIds = new Set(fileMatches.map(m => m.chat_id));
|
|
126
|
+
sessions = sessions.filter(s => matchingIds.has(s.id));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (sessions.length === 0) {
|
|
130
|
+
return { content: [{ type: 'text', text: `No activity found for user "${username}"` }] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result = sessions.map(s => ({
|
|
134
|
+
id: s.id,
|
|
135
|
+
name: s.name,
|
|
136
|
+
source: s.source,
|
|
137
|
+
mode: s.mode,
|
|
138
|
+
folder: s.folder,
|
|
139
|
+
lastUpdated: s.last_updated_at ? new Date(s.last_updated_at).toISOString() : null,
|
|
140
|
+
totalMessages: s.total_messages,
|
|
141
|
+
models: s.models ? [...new Set(JSON.parse(s.models))].slice(0, 5) : [],
|
|
142
|
+
toolCalls: s.tool_calls ? JSON.parse(s.tool_calls).length : 0,
|
|
143
|
+
totalInputTokens: s.total_input_tokens,
|
|
144
|
+
totalOutputTokens: s.total_output_tokens,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// ── Tool: get_session_detail ──
|
|
154
|
+
mcpServer.tool(
|
|
155
|
+
'get_session_detail',
|
|
156
|
+
'Get the full conversation messages for a specific session. Use the session ID from search_sessions or get_user_activity results.',
|
|
157
|
+
{
|
|
158
|
+
session_id: z.string().describe('The chat/session ID'),
|
|
159
|
+
username: z.string().optional().describe('Username who owns the session (optional, auto-detected if unique)'),
|
|
160
|
+
},
|
|
161
|
+
async ({ session_id, username }) => {
|
|
162
|
+
const db = getDb();
|
|
163
|
+
if (!db) return { content: [{ type: 'text', text: 'Relay database not initialized' }] };
|
|
164
|
+
|
|
165
|
+
let chatSql = 'SELECT * FROM relay_chats WHERE id = ?';
|
|
166
|
+
const chatParams = [session_id];
|
|
167
|
+
if (username) { chatSql += ' AND username = ?'; chatParams.push(username); }
|
|
168
|
+
chatSql += ' LIMIT 1';
|
|
169
|
+
|
|
170
|
+
const chat = db.prepare(chatSql).get(...chatParams);
|
|
171
|
+
if (!chat) {
|
|
172
|
+
return { content: [{ type: 'text', text: `Session "${session_id}" not found` }] };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const messages = db.prepare(
|
|
176
|
+
'SELECT seq, role, content, model FROM relay_messages WHERE chat_id = ? AND username = ? ORDER BY seq'
|
|
177
|
+
).all(chat.id, chat.username);
|
|
178
|
+
|
|
179
|
+
const formatted = messages.map(m => {
|
|
180
|
+
const label = m.role === 'user' ? 'User' : m.role === 'assistant' ? 'Assistant' : m.role;
|
|
181
|
+
const modelTag = m.model ? ` (${m.model})` : '';
|
|
182
|
+
const content = m.content.length > 2000 ? m.content.substring(0, 2000) + '\n... [truncated]' : m.content;
|
|
183
|
+
return `## ${label}${modelTag}\n\n${content}`;
|
|
184
|
+
}).join('\n\n---\n\n');
|
|
185
|
+
|
|
186
|
+
const header = `# ${chat.name || 'Untitled Session'}\n**User:** ${chat.username} | **Editor:** ${chat.source} | **Project:** ${chat.folder || 'N/A'}\n\n---\n\n`;
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: 'text', text: header + formatted }],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return mcpServer;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Wire MCP SSE transport into an Express app.
|
|
199
|
+
* GET /mcp → establishes SSE connection
|
|
200
|
+
* POST /mcp → receives messages from MCP client
|
|
201
|
+
*/
|
|
202
|
+
function wireMcpToExpress(app, getDb) {
|
|
203
|
+
const sseTransports = {};
|
|
204
|
+
const httpTransports = {};
|
|
205
|
+
|
|
206
|
+
// Same auth gate as the relay's /relay/* routes (same token derivation as
|
|
207
|
+
// createRelayApp): when RELAY_PASSWORD is set, /mcp must not expose
|
|
208
|
+
// search_sessions / get_session_detail to unauthenticated clients.
|
|
209
|
+
const RELAY_PASSWORD = process.env.RELAY_PASSWORD || null;
|
|
210
|
+
const AUTH_TOKEN = RELAY_PASSWORD
|
|
211
|
+
? require('crypto').createHmac('sha256', 'agentlytics-relay').update(RELAY_PASSWORD).digest('hex')
|
|
212
|
+
: null;
|
|
213
|
+
function requireAuth(req, res, next) {
|
|
214
|
+
if (!AUTH_TOKEN) return next();
|
|
215
|
+
const header = req.headers.authorization || '';
|
|
216
|
+
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
|
|
217
|
+
if (token === AUTH_TOKEN) return next();
|
|
218
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// SSE: GET /mcp establishes SSE stream
|
|
222
|
+
app.get('/mcp', requireAuth, async (req, res) => {
|
|
223
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
224
|
+
|
|
225
|
+
// Streamable HTTP GET for SSE stream resumption
|
|
226
|
+
if (sessionId && httpTransports[sessionId]) {
|
|
227
|
+
const transport = httpTransports[sessionId];
|
|
228
|
+
await transport.handleRequest(req, res);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Legacy SSE transport
|
|
233
|
+
const transport = new SSEServerTransport('/mcp', res);
|
|
234
|
+
sseTransports[transport.sessionId] = transport;
|
|
235
|
+
|
|
236
|
+
const mcpServer = createMcpServer(getDb);
|
|
237
|
+
|
|
238
|
+
res.on('close', () => {
|
|
239
|
+
delete sseTransports[transport.sessionId];
|
|
240
|
+
mcpServer.close().catch(() => {});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await mcpServer.connect(transport);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// POST /mcp handles both SSE messages and Streamable HTTP
|
|
247
|
+
app.post('/mcp', requireAuth, async (req, res) => {
|
|
248
|
+
// Check for SSE session first
|
|
249
|
+
const sseSessionId = req.query.sessionId;
|
|
250
|
+
if (sseSessionId && sseTransports[sseSessionId]) {
|
|
251
|
+
await sseTransports[sseSessionId].handlePostMessage(req, res);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Streamable HTTP: check for existing session
|
|
256
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
257
|
+
if (sessionId && httpTransports[sessionId]) {
|
|
258
|
+
await httpTransports[sessionId].handleRequest(req, res, req.body);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// New Streamable HTTP session (initialization request)
|
|
263
|
+
const transport = new StreamableHTTPServerTransport({
|
|
264
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
265
|
+
onsessioninitialized: (id) => {
|
|
266
|
+
httpTransports[id] = transport;
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
transport.onclose = () => {
|
|
271
|
+
if (transport.sessionId) {
|
|
272
|
+
delete httpTransports[transport.sessionId];
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const mcpServer = createMcpServer(getDb);
|
|
277
|
+
await mcpServer.connect(transport);
|
|
278
|
+
await transport.handleRequest(req, res, req.body);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// DELETE /mcp for session cleanup
|
|
282
|
+
app.delete('/mcp', requireAuth, async (req, res) => {
|
|
283
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
284
|
+
if (sessionId && httpTransports[sessionId]) {
|
|
285
|
+
await httpTransports[sessionId].handleRequest(req, res);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
res.status(404).end();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return { sseTransports, httpTransports };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = { createMcpServer, wireMcpToExpress };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# mem-house — design
|
|
2
|
+
|
|
3
|
+
**mem-house** is agent conversation memory as a product: every coding-agent session
|
|
4
|
+
on your machines — across **all 17 editors** agentlytics supports — parsed locally,
|
|
5
|
+
shipped to one typed ClickHouse store, shareable with a team, installable on the
|
|
6
|
+
ultimagent kernel as an **agency**, and visible through the agentlytics dashboard.
|
|
7
|
+
|
|
8
|
+
**Agency, precisely.** In constellation terms (`../TERMINOLOGY.md`) mem-house is a
|
|
9
|
+
**house** — the `memhouse` database — **plus a resident**: the shipper. That pairing
|
|
10
|
+
is what the word *agency* means; a house on its own only holds.
|
|
11
|
+
|
|
12
|
+
The test is mechanical — **residents write, routines read**. The shipper fires on a
|
|
13
|
+
trigger no query supplies (its own loop) and produces writes that outlive the call:
|
|
14
|
+
|
|
15
|
+
```toml
|
|
16
|
+
[[resident]]
|
|
17
|
+
kind = "worker" # deterministic code, no LLM
|
|
18
|
+
on = "loop" # a daemon — not an insert trigger, not a schedule
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`sessions_v` is a plain view: computed during your query, for your query, writing
|
|
22
|
+
nothing. It is a **routine** — house machinery, not a second resident. mem-house has
|
|
23
|
+
**no materialized views at all**, so the shipper is not merely a resident, it is the
|
|
24
|
+
only candidate in the tree. Strip it and every remaining moving part is a routine
|
|
25
|
+
over rows nobody is writing any more.
|
|
26
|
+
|
|
27
|
+
Note that `user_id MATERIALIZED currentUser()` is a materialized *column*: it computes
|
|
28
|
+
during your own insert and is part of the table's definition. It makes own-only RLS
|
|
29
|
+
work and it records who wrote each row, but it is an identity property, not what makes
|
|
30
|
+
this an agency.
|
|
31
|
+
|
|
32
|
+
**Positioning:** an alternative agency **competing with memory-house**. It borrows
|
|
33
|
+
memory-house's proven ideas (server-stamped identity, own-only RLS, idempotent
|
|
34
|
+
shipping, skills/plugin delivery) and agentlytics' proven assets (adapters, UI,
|
|
35
|
+
cost engine). If it wins, it can become memory-house v4; until then the two run
|
|
36
|
+
side by side as separate agencies, each in its own house. Naming: the product is
|
|
37
|
+
**mem-house**; ClickHouse identifiers use **`memhouse`** (agency/house name, owner
|
|
38
|
+
`memhouse_root`) since identifiers can't carry a dash.
|
|
39
|
+
|
|
40
|
+
## The four bets
|
|
41
|
+
|
|
42
|
+
1. **Parse on the client.** memory-house ships raw transcript lines and parses in
|
|
43
|
+
views — workable only because its 4 agents store sessions as JSON/JSONL files.
|
|
44
|
+
agentlytics' breadth includes sqlite-backed editors (Cursor, VS Code, Zed,
|
|
45
|
+
Devin…) where no "raw line" exists; the adapter must crack a DB. So client-side
|
|
46
|
+
parsing is not a style choice — it is what unlocks 17-editor coverage. The
|
|
47
|
+
shipper runs the adapters (`editors/`) and ships **typed rows**. On the canon's
|
|
48
|
+
write axis this bet is precisely *moving work from routine to resident*:
|
|
49
|
+
memory-house parses when you query, mem-house parses before anyone asks and
|
|
50
|
+
writes the result down.
|
|
51
|
+
2. **Typed common schema** (`schema.sql`). Physical typed columns (what
|
|
52
|
+
memory-house derives in views) + `tool_calls` (memory-house has none) + one
|
|
53
|
+
`extra JSON` escape hatch per table so unnormalized adapter fields are never
|
|
54
|
+
lost. `ReplacingMergeTree(ingested_at)`; `messages` keyed `(session_id, seq)` so
|
|
55
|
+
a re-ship of a grown/changed session replaces stale rows (latest-wins). FTS text
|
|
56
|
+
indexes built in (CH ≥ 26.2).
|
|
57
|
+
3. **Kernel-installable agency.** Same install path proven for agentlytics-agency:
|
|
58
|
+
`install-agency{memhouse}` → house + `memhouse_root` + credential; members via
|
|
59
|
+
`register-member` + owner `GRANT`; own-only visibility via the row policy
|
|
60
|
+
(`rls.sql`) applied by the ego (the `kernel` user) or the mayor. The shipper is
|
|
61
|
+
the resident that lands with it. Identity is `user_id MATERIALIZED currentUser()`
|
|
62
|
+
(requires `async_insert=0`). Sharing modes: own-only (policy TO member) or
|
|
63
|
+
team-pool (no policy).
|
|
64
|
+
4. **Borrowed UI, zero fork.** The React SPA consumes REST JSON, not tables. The
|
|
65
|
+
mem-house server implements the **same `/api/*` contract** as agentlytics'
|
|
66
|
+
`server.js` (same routes, same response shapes) over the mem-house schema, and
|
|
67
|
+
serves the same built `public/` bundle. UI changes: none.
|
|
68
|
+
|
|
69
|
+
## Components (this directory)
|
|
70
|
+
|
|
71
|
+
| Path | What | Notes |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `schema.sql` | the house schema (typed) | unqualified names; owner applies in its house |
|
|
74
|
+
| `rls.sql` | own-only row policies | the ego (`kernel`) or the mayor applies (owner lacks ACCESS MANAGEMENT) |
|
|
75
|
+
| `shipper/ship.js` | parse-on-client shipper CLI — the resident (`worker`) | reuses `../../editors`; incremental; idempotent |
|
|
76
|
+
| `server/server.js` | REST API + dashboard | same API contract as agentlytics; serves `../../public` |
|
|
77
|
+
| `delivery/` | delivery kit | installer, skills, plugin, AGENT-INSTALL.md, prompt |
|
|
78
|
+
|
|
79
|
+
## Environment contract
|
|
80
|
+
|
|
81
|
+
| Var | Default | Meaning |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `MEMHOUSE_URL` | `http://localhost:8123` | ClickHouse HTTP(S) endpoint |
|
|
84
|
+
| `MEMHOUSE_USER` | `memhouse_root` | CH user (owner, or a member's own credential) |
|
|
85
|
+
| `MEMHOUSE_PASSWORD` | *(empty)* | credential |
|
|
86
|
+
| `MEMHOUSE_DB` | `memhouse` | the house |
|
|
87
|
+
| `MEMHOUSE_PORT` | `4640` | dashboard/API port |
|
|
88
|
+
|
|
89
|
+
Config file: `~/.memhouse/config.json` (`hiddenProjects`, future prefs).
|
|
90
|
+
|
|
91
|
+
## Data-plane rules (binding for the shipper and server)
|
|
92
|
+
|
|
93
|
+
- Inserts: `JSONEachRow`, batches ≤ 2000 rows, **`async_insert=0` always** (else
|
|
94
|
+
`user_id` stamping breaks). Reads: set `final: 1` so ReplacingMergeTree collapses.
|
|
95
|
+
- All Int64-bound values coerced to integers (some adapters emit fractional ms).
|
|
96
|
+
- Per-message `ts`: interpolated across the session's `[createdAt, lastUpdatedAt]`
|
|
97
|
+
(adapters don't expose per-message timestamps; monotonic by `seq`; documented
|
|
98
|
+
approximation, refine per-adapter later).
|
|
99
|
+
- `seq` is the message index within the session (0-based). `tool_calls.idx` is the
|
|
100
|
+
call index within the session. `is_subagent` = message content begins with
|
|
101
|
+
`[subagent]` (the fold marker from the Claude adapter).
|
|
102
|
+
- Incremental shipping: read existing `(session_id, last_updated_at, message_count)`
|
|
103
|
+
(FINAL) and skip chats that haven't grown/changed; a full re-ship must remain
|
|
104
|
+
safe (ReplacingMergeTree collapses).
|
|
105
|
+
- Costs are computed in the server from `pricing.js` (repo root) over per-model
|
|
106
|
+
token sums; `<synthetic>` and empty models excluded from model lists.
|
|
107
|
+
|
|
108
|
+
## Delivery kit (mirrors memory-house)
|
|
109
|
+
|
|
110
|
+
`install.sh` (standalone), `AGENT-INSTALL.md` (agent-facing install steps),
|
|
111
|
+
`skills/` (Claude Code skills to query/search mem-house), `plugin/` (Claude Code
|
|
112
|
+
plugin wrapping the skills), `PROMPT.md` (system-prompt snippet teaching an agent
|
|
113
|
+
to use mem-house as memory), `kernel-install.md` (provision as agency `memhouse`).
|
|
114
|
+
|
|
115
|
+
## Non-goals (v0)
|
|
116
|
+
|
|
117
|
+
Relay/team-server (the kernel + RLS already covers sharing), launchd daemonization
|
|
118
|
+
(ship `--loop` instead), migration tooling from memory-house's `memory` db, and
|
|
119
|
+
per-adapter true message timestamps.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# mem-house — agent install
|
|
2
|
+
|
|
3
|
+
Imperative steps for an AI coding agent installing mem-house on a machine. Execute
|
|
4
|
+
in order; every command is copy-pasteable. Prereqs: Node >= 20.19, a reachable
|
|
5
|
+
ClickHouse >= 26.2 (local server, Cloud, or a kernel house — see
|
|
6
|
+
`kernel-install.md` for the kernel path), and this repo checked out.
|
|
7
|
+
|
|
8
|
+
## 1. Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cd <repo> # the memhouse checkout
|
|
12
|
+
npm install --no-audit --no-fund
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 2. Configure the connection
|
|
16
|
+
|
|
17
|
+
Write `~/.memhouse/env` (the installer can do this interactively — `bash
|
|
18
|
+
mem-house/delivery/install.sh` — or write it yourself):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
mkdir -p ~/.memhouse
|
|
22
|
+
cat > ~/.memhouse/env <<'EOF'
|
|
23
|
+
MEMHOUSE_URL=http://localhost:8123
|
|
24
|
+
MEMHOUSE_USER=memhouse_root
|
|
25
|
+
MEMHOUSE_PASSWORD=<credential>
|
|
26
|
+
MEMHOUSE_DB=memhouse
|
|
27
|
+
EOF
|
|
28
|
+
chmod 600 ~/.memhouse/env
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 3. Create the schema and run the first ship
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
set -a; source ~/.memhouse/env; set +a
|
|
35
|
+
node mem-house/shipper/ship.js --ensure-schema
|
|
36
|
+
node mem-house/shipper/ship.js # first full parse-on-client ship
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Expected output shape: `[mem-house] shipped N sessions (0 skipped) → M msg rows, T
|
|
40
|
+
tool rows in Xs` with N in the hundreds on a machine with real agent usage.
|
|
41
|
+
|
|
42
|
+
## 4. Verify
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node mem-house/shipper/ship.js --stats
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Expect one row per source (claude-code, codex, gemini-cli, …) with non-zero
|
|
49
|
+
sessions/messages. Or verify by SQL:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
curl -s -u "$MEMHOUSE_USER:$MEMHOUSE_PASSWORD" "$MEMHOUSE_URL/?database=$MEMHOUSE_DB" \
|
|
53
|
+
--data-binary "SELECT source, count() FROM sessions_v GROUP BY source SETTINGS final=1 FORMAT PrettyCompact"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 5. Start the dashboard (optional)
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cd ui && npm install && npm run build && cd .. # first time only (builds public/)
|
|
60
|
+
node mem-house/server/server.js # → http://localhost:4640
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 6. Keep it fresh
|
|
64
|
+
|
|
65
|
+
Re-ship on a schedule (incremental — cheap):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node mem-house/shipper/ship.js --loop 300 &
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 7. Install the skills (optional, for Claude Code)
|
|
72
|
+
|
|
73
|
+
Copy `mem-house/delivery/skills/*` into the target agent's skills directory
|
|
74
|
+
(e.g. `$CLAUDE_CONFIG_DIR/skills/`), or install `mem-house/delivery/plugin/` as a
|
|
75
|
+
Claude Code plugin. The skills read the connection from `~/.memhouse/env`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# mem-house — system-prompt snippet
|
|
2
|
+
|
|
3
|
+
Paste (or import) this into an agent's system prompt to make mem-house its
|
|
4
|
+
long-term conversation memory.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Conversation memory (mem-house)
|
|
9
|
+
|
|
10
|
+
You have persistent memory of past agent sessions — every conversation this user
|
|
11
|
+
has had with coding agents (Claude Code, Codex, Gemini CLI, Cursor, and other
|
|
12
|
+
editors), across machines, stored in a ClickHouse database called **mem-house**.
|
|
13
|
+
Connection: read `~/.memhouse/env` (`MEMHOUSE_URL/USER/PASSWORD/DB`); query over
|
|
14
|
+
HTTP with `curl -u "$MEMHOUSE_USER:$MEMHOUSE_PASSWORD" "$MEMHOUSE_URL/?database=$MEMHOUSE_DB"`
|
|
15
|
+
and always add `SETTINGS final=1` to reads.
|
|
16
|
+
|
|
17
|
+
**When to reach for it — before claiming ignorance.** If the user refers to past
|
|
18
|
+
work that is not in your current context ("that session where…", "how did I solve
|
|
19
|
+
X before", "did we ever…", "what was I working on last week"), search mem-house
|
|
20
|
+
FIRST. Only say you don't know after a search comes back empty.
|
|
21
|
+
|
|
22
|
+
**How to query.** Prefer the installed skills when present: `/memhouse:search`
|
|
23
|
+
(full-text over messages), `/memhouse:sessions` (list/filter sessions),
|
|
24
|
+
`/memhouse:sql` (free-form read-only SQL). Without skills, query directly:
|
|
25
|
+
|
|
26
|
+
- Find sessions about a topic (FTS, lowercase your terms):
|
|
27
|
+
`SELECT DISTINCT session_id, any(project), min(ts) FROM messages
|
|
28
|
+
WHERE hasToken(text_word, 'clickhouse') GROUP BY session_id
|
|
29
|
+
ORDER BY 3 DESC LIMIT 10 SETTINGS final=1 FORMAT PrettyCompact`
|
|
30
|
+
- Recent sessions: `SELECT session_id, source, project, started, first_prompt
|
|
31
|
+
FROM sessions_v ORDER BY started DESC LIMIT 20 SETTINGS final=1`
|
|
32
|
+
- Replay one session: `SELECT role, text FROM messages
|
|
33
|
+
WHERE session_id = '<id>' ORDER BY seq SETTINGS final=1`
|
|
34
|
+
|
|
35
|
+
**Rules.**
|
|
36
|
+
- Memory is READ-ONLY for you. Never INSERT/ALTER/DROP — ingestion belongs to the
|
|
37
|
+
mem-house shipper alone.
|
|
38
|
+
- Quote retrieved content as *the user's past sessions*, and cite the session_id
|
|
39
|
+
when the user may want to dig deeper.
|
|
40
|
+
- Other members' sessions may be invisible to you (row-level security). An empty
|
|
41
|
+
result means "nothing visible", not "nothing ever happened".
|
|
42
|
+
- Do not paste credentials from `~/.memhouse/env` into responses, commits, or logs.
|