memhouse 0.4.4 → 0.7.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.
- package/README.md +244 -114
- package/bin/memhouse.js +1177 -231
- package/editors/base.js +6 -0
- package/editors/claude.js +44 -9
- package/editors/goose.js +4 -0
- package/editors/index.js +0 -0
- package/editors/opencode.js +3 -0
- package/{mem-house → memhouse}/COMPETITION.md +5 -5
- package/{mem-house → memhouse}/DESIGN.md +18 -12
- package/memhouse/RESUME-AND-UPDATE.md +185 -0
- package/{mem-house → memhouse}/delivery/AGENT-INSTALL-PROMPT.md +1 -1
- package/{mem-house → memhouse}/delivery/AGENT-INSTALL.md +9 -7
- package/{mem-house → memhouse}/delivery/PROMPT.md +8 -8
- package/{mem-house → memhouse}/delivery/kernel-install.md +3 -3
- package/{mem-house → memhouse}/delivery/plugin/.claude-plugin/plugin.json +1 -1
- package/{mem-house/delivery/skills/memhouse-search → memhouse/delivery/plugin/skills/search}/SKILL.md +60 -18
- package/memhouse/delivery/plugin/skills/sessions/SKILL.md +177 -0
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +206 -0
- package/memhouse/host.js +101 -0
- package/{mem-house → memhouse}/per-member/INSTALL.md +59 -14
- package/{mem-house → memhouse}/per-member/SCHEMA.md +47 -0
- package/{mem-house → memhouse}/per-member/SHARING.md +47 -0
- package/memhouse/per-member/provision.js +297 -0
- package/memhouse/per-member/rooms.js +252 -0
- package/{mem-house → memhouse}/per-member/schema-member.sql.tpl +18 -14
- package/{mem-house → memhouse}/per-member/schema-merge.sql.tpl +3 -3
- package/memhouse/resume.js +150 -0
- package/memhouse/self-update.js +150 -0
- package/{mem-house → memhouse}/server/queries.js +80 -34
- package/memhouse/server/server.js +451 -0
- package/{mem-house → memhouse}/service.js +48 -29
- package/memhouse/shipper/ship.js +949 -0
- package/package.json +5 -20
- package/public/assets/{index-CtH-5i95.js → index-CnWzrjF3.js} +10 -7
- package/public/index.html +2 -2
- package/ui/index.html +1 -1
- package/ui/src/App.jsx +1 -1
- package/ui/src/lib/api.js +29 -13
- package/ui/src/pages/Dashboard.jsx +8 -5
- package/ui/src/pages/Sessions.jsx +3 -1
- package/ui/src/pages/SqlViewer.jsx +6 -23
- package/cache.js +0 -850
- package/ch-ingest.js +0 -329
- package/ch-schema.js +0 -152
- package/clickhouse.js +0 -87
- package/index.js +0 -425
- package/mcp-server.js +0 -294
- package/mem-house/delivery/install.sh +0 -183
- package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +0 -125
- package/mem-house/delivery/skills/memhouse-sql/SKILL.md +0 -146
- package/mem-house/per-member/provision.js +0 -142
- package/mem-house/per-member/rooms.js +0 -132
- package/mem-house/server/server.js +0 -228
- package/mem-house/shipper/ship.js +0 -566
- package/relay-client.js +0 -306
- package/relay-server.js +0 -577
- package/server.js +0 -860
- package/share-image.js +0 -367
- /package/{mem-house → memhouse}/deploy.js +0 -0
- /package/{mem-house → memhouse}/envfile.js +0 -0
- /package/{mem-house → memhouse}/per-member/PLAN.md +0 -0
- /package/{mem-house → memhouse}/per-member/PROVISIONING.md +0 -0
- /package/{mem-house → memhouse}/per-member/README.md +0 -0
package/mcp-server.js
DELETED
|
@@ -1,294 +0,0 @@
|
|
|
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 };
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# mem-house standalone installer.
|
|
3
|
-
#
|
|
4
|
-
# Checks node >= 20 and a reachable ClickHouse, writes ~/.memhouse/env
|
|
5
|
-
# (MEMHOUSE_* vars), applies the house schema via the shipper
|
|
6
|
-
# (`ship.js --ensure-schema` mints the caller's own rooms), runs a first ship, and prints how to start the
|
|
7
|
-
# dashboard and set up continuous shipping (`--loop`).
|
|
8
|
-
#
|
|
9
|
-
# Usage:
|
|
10
|
-
# ./install.sh # interactive (prompts for missing values)
|
|
11
|
-
# ./install.sh --url http://localhost:8123 --user memhouse_root --password 'pw' --db mem
|
|
12
|
-
# ./install.sh --yes # non-interactive, defaults / env / existing env file
|
|
13
|
-
#
|
|
14
|
-
# Flags:
|
|
15
|
-
# --url URL ClickHouse HTTP(S) endpoint (default http://localhost:8123)
|
|
16
|
-
# --user USER ClickHouse user (default memhouse_root; 'default' works for a local CH)
|
|
17
|
-
# --password PW credential (default empty)
|
|
18
|
-
# --db DB the house (database) (default mem)
|
|
19
|
-
# --port PORT dashboard/API port (default 4640)
|
|
20
|
-
# --yes never prompt; take flags > current env > ~/.memhouse/env > defaults
|
|
21
|
-
# --no-ship stop after writing env + ensure-schema (skip the first ship)
|
|
22
|
-
#
|
|
23
|
-
# Defaults resolve in order: flag > MEMHOUSE_* already in the environment >
|
|
24
|
-
# existing ~/.memhouse/env > built-in default. Runs from any cwd — the repo is
|
|
25
|
-
# resolved from this script's own location (mem-house/delivery/ -> repo root).
|
|
26
|
-
set -euo pipefail
|
|
27
|
-
|
|
28
|
-
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
29
|
-
REPO_ROOT="$(cd "$HERE/../.." && pwd)"
|
|
30
|
-
SHIP="$REPO_ROOT/mem-house/shipper/ship.js"
|
|
31
|
-
SERVER="$REPO_ROOT/mem-house/server/server.js"
|
|
32
|
-
ENV_DIR="$HOME/.memhouse"
|
|
33
|
-
ENV_FILE="$ENV_DIR/env"
|
|
34
|
-
|
|
35
|
-
die() { echo "install.sh: $*" >&2; exit 1; }
|
|
36
|
-
info() { echo "== $*"; }
|
|
37
|
-
|
|
38
|
-
# ---------- 0. defaults: existing env file, then environment ----------------
|
|
39
|
-
if [ -f "$ENV_FILE" ]; then
|
|
40
|
-
# shellcheck disable=SC1090
|
|
41
|
-
set -a; . "$ENV_FILE"; set +a
|
|
42
|
-
fi
|
|
43
|
-
CH_URL="${MEMHOUSE_URL:-http://localhost:8123}"
|
|
44
|
-
CH_USER="${MEMHOUSE_USER:-memhouse_root}"
|
|
45
|
-
CH_PASSWORD="${MEMHOUSE_PASSWORD:-}"
|
|
46
|
-
CH_DB="${MEMHOUSE_DB:-mem}"
|
|
47
|
-
DASH_PORT="${MEMHOUSE_PORT:-4640}"
|
|
48
|
-
|
|
49
|
-
ASSUME_YES=0
|
|
50
|
-
NO_SHIP=0
|
|
51
|
-
URL_SET=0; USER_SET=0; PASS_SET=0; DB_SET=0
|
|
52
|
-
|
|
53
|
-
while [ $# -gt 0 ]; do
|
|
54
|
-
case "$1" in
|
|
55
|
-
--url) CH_URL="${2:?--url needs a value}"; URL_SET=1; shift 2;;
|
|
56
|
-
--user) CH_USER="${2:?--user needs a value}"; USER_SET=1; shift 2;;
|
|
57
|
-
--password) CH_PASSWORD="${2:?--password needs a value}"; PASS_SET=1; shift 2;;
|
|
58
|
-
--db) CH_DB="${2:?--db needs a value}"; DB_SET=1; shift 2;;
|
|
59
|
-
--port) DASH_PORT="${2:?--port needs a value}"; shift 2;;
|
|
60
|
-
--yes|-y) ASSUME_YES=1; shift;;
|
|
61
|
-
--no-ship) NO_SHIP=1; shift;;
|
|
62
|
-
-h|--help) sed -n '2,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0;;
|
|
63
|
-
*) die "unknown flag: $1 (try --help)";;
|
|
64
|
-
esac
|
|
65
|
-
done
|
|
66
|
-
|
|
67
|
-
# ---------- 1. node >= 20 ---------------------------------------------------
|
|
68
|
-
command -v node >/dev/null 2>&1 || die "node not found (need node >= 20; https://nodejs.org)"
|
|
69
|
-
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
|
|
70
|
-
[ "$NODE_MAJOR" -ge 20 ] 2>/dev/null || die "node >= 20 required (found $(node -v))"
|
|
71
|
-
info "node $(node -v) ok"
|
|
72
|
-
|
|
73
|
-
[ -f "$SHIP" ] || die "shipper not found at $SHIP (incomplete checkout?)"
|
|
74
|
-
|
|
75
|
-
# ---------- 2. repo dependencies -------------------------------------------
|
|
76
|
-
if [ ! -d "$REPO_ROOT/node_modules" ]; then
|
|
77
|
-
info "installing npm dependencies (first run)"
|
|
78
|
-
(cd "$REPO_ROOT" && npm install --no-audit --no-fund)
|
|
79
|
-
fi
|
|
80
|
-
|
|
81
|
-
# ---------- 3. gather connection values -------------------------------------
|
|
82
|
-
prompt() { # prompt <question> <current> -> stdout
|
|
83
|
-
local ans
|
|
84
|
-
read -r -p "$1 [$2]: " ans
|
|
85
|
-
echo "${ans:-$2}"
|
|
86
|
-
}
|
|
87
|
-
if [ "$ASSUME_YES" -eq 0 ] && [ -t 0 ]; then
|
|
88
|
-
[ "$URL_SET" -eq 1 ] || CH_URL="$(prompt "ClickHouse URL" "$CH_URL")"
|
|
89
|
-
[ "$USER_SET" -eq 1 ] || CH_USER="$(prompt "ClickHouse user" "$CH_USER")"
|
|
90
|
-
if [ "$PASS_SET" -eq 0 ]; then
|
|
91
|
-
read -r -s -p "ClickHouse password [keep current/empty]: " ans; echo
|
|
92
|
-
[ -n "$ans" ] && CH_PASSWORD="$ans"
|
|
93
|
-
fi
|
|
94
|
-
[ "$DB_SET" -eq 1 ] || CH_DB="$(prompt "House (database)" "$CH_DB")"
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
# ---------- 4. ClickHouse reachable? ----------------------------------------
|
|
98
|
-
info "checking ClickHouse at $CH_URL (user $CH_USER)"
|
|
99
|
-
ONE="$(curl -sS --connect-timeout 5 --fail-with-body \
|
|
100
|
-
--user "$CH_USER:$CH_PASSWORD" \
|
|
101
|
-
--data-binary "SELECT 1" "$CH_URL")" \
|
|
102
|
-
|| die "ClickHouse not reachable at $CH_URL with those credentials"
|
|
103
|
-
[ "$ONE" = "1" ] || die "unexpected reply from $CH_URL: $ONE"
|
|
104
|
-
info "ClickHouse ok"
|
|
105
|
-
|
|
106
|
-
# Ensure the house exists before ensure-schema (the room DDL is unqualified and the
|
|
107
|
-
# shipper binds to MEMHOUSE_DB — on a virgin standalone server the database must
|
|
108
|
-
# be created first). On a kernel realm the house is already provisioned; if the
|
|
109
|
-
# user lacks CREATE DATABASE but the house is reachable, that's fine too.
|
|
110
|
-
case "$CH_DB" in
|
|
111
|
-
(*[!A-Za-z0-9_]*|'') die "invalid database name '$CH_DB' — use letters, digits, underscore" ;;
|
|
112
|
-
esac
|
|
113
|
-
if ! curl -fsS --max-time 20 --user "$CH_USER:$CH_PASSWORD" \
|
|
114
|
-
--data-binary "CREATE DATABASE IF NOT EXISTS $CH_DB" "$CH_URL" >/dev/null 2>&1; then
|
|
115
|
-
curl -fsS --max-time 20 --user "$CH_USER:$CH_PASSWORD" \
|
|
116
|
-
--data-binary "SELECT 1" "$CH_URL/?database=$CH_DB" >/dev/null 2>&1 \
|
|
117
|
-
|| die "house '$CH_DB' does not exist and cannot be created as $CH_USER"
|
|
118
|
-
fi
|
|
119
|
-
info "house '$CH_DB' ready"
|
|
120
|
-
|
|
121
|
-
# ---------- 5. write ~/.memhouse/env ----------------------------------------
|
|
122
|
-
mkdir -p "$ENV_DIR"; chmod 700 "$ENV_DIR"
|
|
123
|
-
# Values are single-quoted (embedded ' escaped) — this file is sourced by shells
|
|
124
|
-
# (docs/skills use `. ~/.memhouse/env`), so unquoted metacharacters in a password
|
|
125
|
-
# would corrupt the value or execute on source.
|
|
126
|
-
shq() { printf "%s" "$1" | sed "s/'/'\\\\''/g"; }
|
|
127
|
-
cat > "$ENV_FILE" <<EOF
|
|
128
|
-
# mem-house connection (written by install.sh $(date +%Y-%m-%d-%H_%M)). NEVER commit.
|
|
129
|
-
MEMHOUSE_URL='$(shq "$CH_URL")'
|
|
130
|
-
MEMHOUSE_USER='$(shq "$CH_USER")'
|
|
131
|
-
MEMHOUSE_PASSWORD='$(shq "$CH_PASSWORD")'
|
|
132
|
-
MEMHOUSE_DB='$(shq "$CH_DB")'
|
|
133
|
-
MEMHOUSE_PORT='$(shq "$DASH_PORT")'
|
|
134
|
-
EOF
|
|
135
|
-
chmod 600 "$ENV_FILE"
|
|
136
|
-
info "wrote $ENV_FILE"
|
|
137
|
-
|
|
138
|
-
export MEMHOUSE_URL="$CH_URL" MEMHOUSE_USER="$CH_USER" \
|
|
139
|
-
MEMHOUSE_PASSWORD="$CH_PASSWORD" MEMHOUSE_DB="$CH_DB" \
|
|
140
|
-
MEMHOUSE_PORT="$DASH_PORT"
|
|
141
|
-
|
|
142
|
-
# ---------- 6. schema + first ship ------------------------------------------
|
|
143
|
-
info "applying house schema (ship.js --ensure-schema)"
|
|
144
|
-
node "$SHIP" --ensure-schema \
|
|
145
|
-
|| die "ensure-schema failed. If $CH_USER cannot CREATE DATABASE, create '$CH_DB' first (or, on a kernel realm, provision the agency: see delivery/kernel-install.md), then re-run."
|
|
146
|
-
|
|
147
|
-
if [ "$NO_SHIP" -eq 0 ]; then
|
|
148
|
-
info "first ship (this parses local editor sessions and may take a while)"
|
|
149
|
-
node "$SHIP"
|
|
150
|
-
else
|
|
151
|
-
info "skipping first ship (--no-ship)"
|
|
152
|
-
fi
|
|
153
|
-
|
|
154
|
-
# ---------- 7. next steps ----------------------------------------------------
|
|
155
|
-
cat <<EOF
|
|
156
|
-
|
|
157
|
-
mem-house installed.
|
|
158
|
-
|
|
159
|
-
env file $ENV_FILE
|
|
160
|
-
house $CH_DB @ $CH_URL
|
|
161
|
-
|
|
162
|
-
Next steps:
|
|
163
|
-
|
|
164
|
-
# dashboard (agentlytics UI over the house) -> http://localhost:$DASH_PORT
|
|
165
|
-
node "$SERVER"
|
|
166
|
-
|
|
167
|
-
# keep shipping continuously (re-ships incrementally; Ctrl-C to stop)
|
|
168
|
-
node "$SHIP" --loop
|
|
169
|
-
# background it:
|
|
170
|
-
nohup node "$SHIP" --loop >> "$ENV_DIR/ship.log" 2>&1 &
|
|
171
|
-
|
|
172
|
-
# agent skills (search/sessions/sql from inside Claude Code):
|
|
173
|
-
node "$REPO_ROOT/bin/memhouse.js" plugins install claude
|
|
174
|
-
# -> installs memhouse-search / memhouse-sessions / memhouse-sql into
|
|
175
|
-
# \${CLAUDE_CONFIG_DIR:-\$HOME/.claude}/skills (loads next session)
|
|
176
|
-
|
|
177
|
-
Verify anytime (rooms are named for your ClickHouse user):
|
|
178
|
-
set -a; . "$ENV_FILE"; set +a
|
|
179
|
-
ME=\$(curl -sS --user "\$MEMHOUSE_USER:\$MEMHOUSE_PASSWORD" \\
|
|
180
|
-
--data-binary "SELECT currentUser() FORMAT TabSeparated" "\$MEMHOUSE_URL/")
|
|
181
|
-
curl -sS --user "\$MEMHOUSE_USER:\$MEMHOUSE_PASSWORD" --data-binary \\
|
|
182
|
-
"SELECT count() FROM sessions_\$ME" "\$MEMHOUSE_URL/?database=\$MEMHOUSE_DB&final=1"
|
|
183
|
-
EOF
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: memhouse-sessions
|
|
3
|
-
description: List recent agent sessions from memhouse conversation memory — names, projects, editors, hosts, times, message and token counts across all 17 supported editors. Use for "what was I working on", "show my recent sessions", "sessions from last week", "what did I do in project X", "list my cursor sessions", or to orient before drilling into one session.
|
|
4
|
-
user-invocable: true
|
|
5
|
-
argument-hint: "[N] [project <name>] [from <editor>] [today|week|month]"
|
|
6
|
-
allowed-tools: Bash(set -a*), Bash(. *), Bash(curl*)
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# memhouse-sessions — browse session history
|
|
10
|
-
|
|
11
|
-
List/filter sessions from the session rollup — a saved query over your own rooms, not an
|
|
12
|
-
object; `memhouse sessions-query` prints it (session metadata +
|
|
13
|
-
message aggregates) in the memhouse house.
|
|
14
|
-
|
|
15
|
-
## When to use
|
|
16
|
-
|
|
17
|
-
- The user wants an overview of recent activity rather than a specific quote.
|
|
18
|
-
- As a first step before digging into one session, when they don't have an
|
|
19
|
-
id/name yet.
|
|
20
|
-
|
|
21
|
-
## Connection
|
|
22
|
-
|
|
23
|
-
Credentials from `~/.memhouse/env` (or exported `MEMHOUSE_*`). Always read with
|
|
24
|
-
`final=1` (collapses ReplacingMergeTree duplicates to latest-wins) and
|
|
25
|
-
`join_use_nulls=1` (see below — the session rollup needs it):
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
set -a; [ -f ~/.memhouse/env ] && . ~/.memhouse/env; set +a
|
|
29
|
-
curl -sS --fail-with-body --user "${MEMHOUSE_USER:-memhouse_root}:${MEMHOUSE_PASSWORD:-}" \
|
|
30
|
-
--data-binary @- "${MEMHOUSE_URL:-http://localhost:8123}/?database=${MEMHOUSE_DB:-mem}&final=1&join_use_nulls=1" <<'SQL'
|
|
31
|
-
<the query>
|
|
32
|
-
FORMAT PrettyCompact
|
|
33
|
-
SQL
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Room names — always suffixed
|
|
37
|
-
|
|
38
|
-
Every member owns their own rooms, named for their ClickHouse user:
|
|
39
|
-
`messages_<you>`, `sessions_<you>`, `tool_calls_<you>`. There are no unsuffixed rooms;
|
|
40
|
-
the bare names fail with `UNKNOWN_TABLE` rather than returning nothing. You hold no grant
|
|
41
|
-
on anyone else's rooms, so isolation is not something a query can work around.
|
|
42
|
-
|
|
43
|
-
Resolve your own name once and substitute it into every table name below:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
MEM_ME="$(curl -sS --fail-with-body --user "$MEMHOUSE_USER:$MEMHOUSE_PASSWORD" \
|
|
47
|
-
--data-binary "SELECT currentUser() FORMAT TabSeparated" \
|
|
48
|
-
"${MEMHOUSE_URL:-http://localhost:8123}/" | tr -d '\r\n')"
|
|
49
|
-
# rooms: messages_$MEM_ME, sessions_$MEM_ME, tool_calls_$MEM_ME
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
**There is no `sessions_v` object.** The session rollup is a saved query over those same
|
|
53
|
-
rooms — `memhouse sessions-query` prints it for whoever you are connected as, ready to
|
|
54
|
-
paste into a `FROM (...) AS c` position.
|
|
55
|
-
|
|
56
|
-
**Read the rollup with `join_use_nulls=1`.** The connection recipe above sets it. Without
|
|
57
|
-
it, ClickHouse gives an unmatched `m.seq` a default instead of NULL, so a session with no
|
|
58
|
-
messages reports `total_msgs = 1` rather than 0 — measured, not theoretical. A subquery
|
|
59
|
-
has no `SETTINGS` clause of its own, so the setting has to come from the caller.
|
|
60
|
-
|
|
61
|
-
If you hold `SELECT` on them, `all_messages` / `all_sessions` / `all_tool_calls` read
|
|
62
|
-
across every member at once, narrowed to whatever grants you actually have — a Merge room
|
|
63
|
-
reduces to the rooms the caller can read, so it fails closed rather than denying outright.
|
|
64
|
-
|
|
65
|
-
## Default listing
|
|
66
|
-
|
|
67
|
-
```sql
|
|
68
|
-
SELECT session_id, name, source, host, project,
|
|
69
|
-
started, ended, total_msgs, user_msgs, assistant_msgs,
|
|
70
|
-
output_tokens,
|
|
71
|
-
substring(first_prompt, 1, 80) AS first_prompt
|
|
72
|
-
FROM (the rollup from `memhouse sessions-query`)
|
|
73
|
-
ORDER BY ended DESC
|
|
74
|
-
LIMIT 20
|
|
75
|
-
FORMAT PrettyCompact
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
- Default `LIMIT 20`; `[N]` overrides.
|
|
79
|
-
- `today` / `week` / `month` → `WHERE ended > now() - INTERVAL 1 DAY`
|
|
80
|
-
(`7 DAY` / `30 DAY`).
|
|
81
|
-
- `project <name>` → `WHERE project ILIKE '%<name>%'`.
|
|
82
|
-
- `from <editor>` → `WHERE source = '<id>'` (`claude-code`, `codex`, `cursor`,
|
|
83
|
-
`vscode`, `zed`, `opencode`, `gemini-cli`, `windsurf`, …).
|
|
84
|
-
- Unnamed sessions (`name = ''`): fall back to `first_prompt` as the label.
|
|
85
|
-
|
|
86
|
-
## More cuts
|
|
87
|
-
|
|
88
|
-
Per-editor rollup ("where has my work been happening?"):
|
|
89
|
-
|
|
90
|
-
```sql
|
|
91
|
-
SELECT source, count() AS sessions, sum(total_msgs) AS msgs,
|
|
92
|
-
sum(output_tokens) AS out_tokens, max(ended) AS last_activity
|
|
93
|
-
FROM (the rollup from `memhouse sessions-query`)
|
|
94
|
-
GROUP BY source
|
|
95
|
-
ORDER BY sessions DESC
|
|
96
|
-
FORMAT PrettyCompact
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Busiest projects this month:
|
|
100
|
-
|
|
101
|
-
```sql
|
|
102
|
-
SELECT project, count() AS sessions, sum(assistant_msgs) AS assistant_msgs
|
|
103
|
-
FROM (the rollup from `memhouse sessions-query`)
|
|
104
|
-
WHERE ended > now() - INTERVAL 30 DAY AND project != ''
|
|
105
|
-
GROUP BY project
|
|
106
|
-
ORDER BY sessions DESC
|
|
107
|
-
LIMIT 15
|
|
108
|
-
FORMAT PrettyCompact
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## Examples
|
|
112
|
-
|
|
113
|
-
| User says | What you do |
|
|
114
|
-
|---|---|
|
|
115
|
-
| "what was I working on yesterday?" | `WHERE ended > now() - INTERVAL 1 DAY` |
|
|
116
|
-
| "show my last 10 sessions in agentlytics" | `LIMIT 10 … WHERE project ILIKE '%agentlytics%'` |
|
|
117
|
-
| "list my cursor sessions this month" | `WHERE source = 'cursor' AND ended > now() - INTERVAL 30 DAY` |
|
|
118
|
-
| "which machine did I do the schema work on?" | search via memhouse-search, or filter `host` here |
|
|
119
|
-
|
|
120
|
-
## Output
|
|
121
|
-
|
|
122
|
-
A compact table: when, name (or first prompt), source, host, project, msgs,
|
|
123
|
-
tokens. Offer to pull a full transcript next
|
|
124
|
-
(`SELECT role, text FROM messages_<you> WHERE session_id = '…' ORDER BY seq`) or to
|
|
125
|
-
run memhouse-search for a specific quote.
|