memhouse 0.4.4 → 0.7.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 (63) hide show
  1. package/README.md +244 -114
  2. package/bin/memhouse.js +1162 -230
  3. package/editors/base.js +6 -0
  4. package/editors/claude.js +44 -9
  5. package/editors/goose.js +4 -0
  6. package/editors/index.js +0 -0
  7. package/editors/opencode.js +3 -0
  8. package/{mem-house → memhouse}/COMPETITION.md +5 -5
  9. package/{mem-house → memhouse}/DESIGN.md +18 -12
  10. package/memhouse/RESUME-AND-UPDATE.md +185 -0
  11. package/{mem-house → memhouse}/delivery/AGENT-INSTALL-PROMPT.md +1 -1
  12. package/{mem-house → memhouse}/delivery/AGENT-INSTALL.md +9 -7
  13. package/{mem-house → memhouse}/delivery/PROMPT.md +8 -8
  14. package/{mem-house → memhouse}/delivery/kernel-install.md +3 -3
  15. package/{mem-house → memhouse}/delivery/plugin/.claude-plugin/plugin.json +1 -1
  16. package/{mem-house/delivery/skills/memhouse-search → memhouse/delivery/plugin/skills/search}/SKILL.md +60 -18
  17. package/memhouse/delivery/plugin/skills/sessions/SKILL.md +177 -0
  18. package/memhouse/delivery/plugin/skills/sql/SKILL.md +206 -0
  19. package/memhouse/host.js +101 -0
  20. package/{mem-house → memhouse}/per-member/INSTALL.md +59 -14
  21. package/{mem-house → memhouse}/per-member/SCHEMA.md +47 -0
  22. package/memhouse/per-member/provision.js +265 -0
  23. package/memhouse/per-member/rooms.js +235 -0
  24. package/{mem-house → memhouse}/per-member/schema-member.sql.tpl +18 -14
  25. package/{mem-house → memhouse}/per-member/schema-merge.sql.tpl +3 -3
  26. package/memhouse/resume.js +150 -0
  27. package/memhouse/self-update.js +150 -0
  28. package/{mem-house → memhouse}/server/queries.js +80 -34
  29. package/memhouse/server/server.js +451 -0
  30. package/{mem-house → memhouse}/service.js +48 -29
  31. package/memhouse/shipper/ship.js +949 -0
  32. package/package.json +5 -20
  33. package/public/assets/{index-CtH-5i95.js → index-CnWzrjF3.js} +10 -7
  34. package/public/index.html +2 -2
  35. package/ui/index.html +1 -1
  36. package/ui/src/App.jsx +1 -1
  37. package/ui/src/lib/api.js +29 -13
  38. package/ui/src/pages/Dashboard.jsx +8 -5
  39. package/ui/src/pages/Sessions.jsx +3 -1
  40. package/ui/src/pages/SqlViewer.jsx +6 -23
  41. package/cache.js +0 -850
  42. package/ch-ingest.js +0 -329
  43. package/ch-schema.js +0 -152
  44. package/clickhouse.js +0 -87
  45. package/index.js +0 -425
  46. package/mcp-server.js +0 -294
  47. package/mem-house/delivery/install.sh +0 -183
  48. package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +0 -125
  49. package/mem-house/delivery/skills/memhouse-sql/SKILL.md +0 -146
  50. package/mem-house/per-member/provision.js +0 -142
  51. package/mem-house/per-member/rooms.js +0 -132
  52. package/mem-house/server/server.js +0 -228
  53. package/mem-house/shipper/ship.js +0 -566
  54. package/relay-client.js +0 -306
  55. package/relay-server.js +0 -577
  56. package/server.js +0 -860
  57. package/share-image.js +0 -367
  58. /package/{mem-house → memhouse}/deploy.js +0 -0
  59. /package/{mem-house → memhouse}/envfile.js +0 -0
  60. /package/{mem-house → memhouse}/per-member/PLAN.md +0 -0
  61. /package/{mem-house → memhouse}/per-member/PROVISIONING.md +0 -0
  62. /package/{mem-house → memhouse}/per-member/README.md +0 -0
  63. /package/{mem-house → memhouse}/per-member/SHARING.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.